mirror of
https://github.com/wiz0u/WTelegramClient.git
synced 2025-12-06 06:52:01 +01:00
- Markdown:  - Html: <tg-emoji emoji-id="..."> Previous syntaxes are still accepted Also, changed Github examples links with ts=4
698 lines
39 KiB
C#
698 lines
39 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using System.Security.Cryptography;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using TL;
|
||
|
||
// necessary for .NET Standard 2.0 compilation:
|
||
#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
|
||
|
||
namespace WTelegram
|
||
{
|
||
partial class Client
|
||
{
|
||
#region Collect Access Hash system
|
||
/// <summary>Enable the collection of id/access_hash pairs (experimental)<br/>See <see href="https://github.com/wiz0u/WTelegramClient/blob/master/FAQ.md#access-hash"/></summary>
|
||
public bool CollectAccessHash { get; set; }
|
||
public IEnumerable<KeyValuePair<long, long>> AllAccessHashesFor<T>() where T : IObject => _accessHashes.GetValueOrDefault(typeof(T));
|
||
private readonly Dictionary<Type, Dictionary<long, long>> _accessHashes = new();
|
||
private static readonly FieldInfo userFlagsField = typeof(User).GetField("flags");
|
||
private static readonly FieldInfo channelFlagsField = typeof(Channel).GetField("flags");
|
||
|
||
/// <summary>Retrieve the access_hash associated with this id (for a TL class) if it was collected</summary>
|
||
/// <remarks>This requires <see cref="CollectAccessHash"/> to be set to <see langword="true"/> first.
|
||
/// <para>See <see href="https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_CollectAccessHash.cs?ts=4#L22">Examples/Program_CollectAccessHash.cs</see> for how to use this</para></remarks>
|
||
/// <typeparam name="T">a TL object class. For example User, Channel or Photo</typeparam>
|
||
public long GetAccessHashFor<T>(long id) where T : IObject
|
||
{
|
||
if (!CollectAccessHash) Helpers.Log(4, "GetAccessHashFor doesn't do what you think. See https://github.com/wiz0u/WTelegramClient/blob/master/FAQ.md#access-hash");
|
||
lock (_accessHashes)
|
||
return _accessHashes.GetOrCreate(typeof(T)).TryGetValue(id, out var access_hash) ? access_hash : 0;
|
||
}
|
||
public void SetAccessHashFor<T>(long id, long access_hash) where T : IObject
|
||
{
|
||
lock (_accessHashes)
|
||
_accessHashes.GetOrCreate(typeof(T))[id] = access_hash;
|
||
}
|
||
internal void CollectField(FieldInfo fieldInfo, object obj, object access_hash)
|
||
{
|
||
if (fieldInfo.Name != "access_hash") return;
|
||
if (access_hash is not long accessHash) return;
|
||
var type = fieldInfo.ReflectedType;
|
||
if ((type == typeof(User) && ((User.Flags)userFlagsField.GetValue(obj)).HasFlag(User.Flags.min)) ||
|
||
(type == typeof(Channel) && ((Channel.Flags)channelFlagsField.GetValue(obj)).HasFlag(Channel.Flags.min)))
|
||
return; // access_hash from Min constructors are mostly useless. see https://core.telegram.org/api/min
|
||
if (type.GetField("id") is not FieldInfo idField) return;
|
||
if (idField.GetValue(obj) is not long id)
|
||
if (idField.GetValue(obj) is not int idInt) return;
|
||
else id = idInt;
|
||
lock (_accessHashes)
|
||
_accessHashes.GetOrCreate(type)[id] = accessHash;
|
||
}
|
||
#endregion
|
||
|
||
#region Client TL Helpers
|
||
/// <summary>Used to indicate progression of file download/upload</summary>
|
||
/// <param name="transmitted">transmitted bytes</param>
|
||
/// <param name="totalSize">total size of file in bytes, or 0 if unknown</param>
|
||
public delegate void ProgressCallback(long transmitted, long totalSize);
|
||
|
||
/// <summary>Helper function to upload a file to Telegram</summary>
|
||
/// <param name="pathname">Path to the file to upload</param>
|
||
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
|
||
/// <returns>an <see cref="InputFile"/> or <see cref="InputFileBig"/> than can be used in various requests</returns>
|
||
public Task<InputFileBase> UploadFileAsync(string pathname, ProgressCallback progress = null)
|
||
=> UploadFileAsync(File.OpenRead(pathname), Path.GetFileName(pathname), progress);
|
||
|
||
/// <summary>Helper function to upload a file to Telegram</summary>
|
||
/// <param name="stream">Content of the file to upload. This method close/dispose the stream</param>
|
||
/// <param name="filename">Name of the file</param>
|
||
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
|
||
/// <returns>an <see cref="InputFile"/> or <see cref="InputFileBig"/> than can be used in various requests</returns>
|
||
public async Task<InputFileBase> UploadFileAsync(Stream stream, string filename, ProgressCallback progress = null)
|
||
{
|
||
using var md5 = MD5.Create();
|
||
using (stream)
|
||
{
|
||
long transmitted = 0, length = stream.Length;
|
||
var isBig = length >= 10 * 1024 * 1024;
|
||
int file_total_parts = (int)((length - 1) / FilePartSize) + 1;
|
||
long file_id = Helpers.RandomLong();
|
||
int file_part = 0, read;
|
||
var tasks = new Dictionary<int, Task>();
|
||
bool abort = false;
|
||
for (long bytesLeft = length; !abort && bytesLeft != 0; file_part++)
|
||
{
|
||
var bytes = new byte[Math.Min(FilePartSize, bytesLeft)];
|
||
read = await stream.FullReadAsync(bytes, bytes.Length, default);
|
||
await _parallelTransfers.WaitAsync();
|
||
bytesLeft -= read;
|
||
var task = SavePart(file_part, bytes);
|
||
lock (tasks) tasks[file_part] = task;
|
||
if (!isBig)
|
||
md5.TransformBlock(bytes, 0, read, null, 0);
|
||
if (read < FilePartSize && bytesLeft != 0) throw new ApplicationException($"Failed to fully read stream ({read},{bytesLeft})");
|
||
|
||
async Task SavePart(int file_part, byte[] bytes)
|
||
{
|
||
try
|
||
{
|
||
if (isBig)
|
||
await this.Upload_SaveBigFilePart(file_id, file_part, file_total_parts, bytes);
|
||
else
|
||
await this.Upload_SaveFilePart(file_id, file_part, bytes);
|
||
lock (tasks) { transmitted += bytes.Length; tasks.Remove(file_part); }
|
||
progress?.Invoke(transmitted, length);
|
||
}
|
||
catch (Exception)
|
||
{
|
||
abort = true;
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
_parallelTransfers.Release();
|
||
}
|
||
}
|
||
}
|
||
Task[] remainingTasks;
|
||
lock (tasks) remainingTasks = tasks.Values.ToArray();
|
||
await Task.WhenAll(remainingTasks); // wait completion and eventually propagate any task exception
|
||
if (!isBig) md5.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||
return isBig ? new InputFileBig { id = file_id, parts = file_total_parts, name = filename }
|
||
: new InputFile { id = file_id, parts = file_total_parts, name = filename, md5_checksum = md5.Hash };
|
||
}
|
||
}
|
||
|
||
/// <summary>Search messages in chat with <see href="https://corefork.telegram.org/type/MessagesFilter">filter</see> and text <para>See <a href="https://corefork.telegram.org/method/messages.search"/></para></summary>
|
||
/// <typeparam name="T">See <see cref="MessagesFilter"/> for a list of possible filter types</typeparam>
|
||
/// <param name="peer">User or chat, histories with which are searched, or <see langword="null"/> constructor for global search</param>
|
||
/// <param name="text">Text search request</param>
|
||
/// <param name="offset_id">Only return messages starting from the specified message ID</param>
|
||
/// <param name="limit"><a href="https://corefork.telegram.org/api/offsets">Number of results to return</a></param>
|
||
public Task<Messages_MessagesBase> Messages_Search<T>(InputPeer peer, string text = null, int offset_id = 0, int limit = int.MaxValue) where T : MessagesFilter, new()
|
||
=> this.Messages_Search(peer, text, new T(), offset_id: offset_id, limit: limit);
|
||
|
||
/// <summary>Search messages globally with <see href="https://corefork.telegram.org/type/MessagesFilter">filter</see> and text <para>See <a href="https://corefork.telegram.org/method/messages.searchGlobal"/></para></summary>
|
||
/// <typeparam name="T">See <see cref="MessagesFilter"/> for a list of possible filter types</typeparam>
|
||
/// <param name="text">Text search request</param>
|
||
/// <param name="offset_id">Only return messages starting from the specified message ID</param>
|
||
/// <param name="limit"><a href="https://corefork.telegram.org/api/offsets">Number of results to return</a></param>
|
||
public Task<Messages_MessagesBase> Messages_SearchGlobal<T>(string text = null, int offset_id = 0, int limit = int.MaxValue) where T : MessagesFilter, new()
|
||
=> this.Messages_SearchGlobal(text, new T(), offset_id: offset_id, limit: limit);
|
||
|
||
/// <summary>Helper function to send a media message more easily</summary>
|
||
/// <param name="peer">Destination of message (chat group, channel, user chat, etc..) </param>
|
||
/// <param name="caption">Caption for the media <i>(in plain text)</i> or <see langword="null"/></param>
|
||
/// <param name="mediaFile">Media file already uploaded to TG <i>(see <see cref="UploadFileAsync">UploadFileAsync</see>)</i></param>
|
||
/// <param name="mimeType"><see langword="null"/> for automatic detection, <c>"photo"</c> for an inline photo, or a MIME type to send as a document</param>
|
||
/// <param name="reply_to_msg_id">Your message is a reply to an existing message with this ID, in the same chat</param>
|
||
/// <param name="entities">Text formatting entities for the caption. You can use <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
|
||
/// <param name="schedule_date">UTC timestamp when the message should be sent</param>
|
||
/// <returns>The transmitted message confirmed by Telegram</returns>
|
||
public Task<Message> SendMediaAsync(InputPeer peer, string caption, InputFileBase mediaFile, string mimeType = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default)
|
||
{
|
||
mimeType ??= Path.GetExtension(mediaFile.Name)?.ToLowerInvariant() switch
|
||
{
|
||
".jpg" or ".jpeg" or ".png" or ".bmp" => "photo",
|
||
".gif" => "image/gif",
|
||
".webp" => "image/webp",
|
||
".mp4" => "video/mp4",
|
||
".mp3" => "audio/mpeg",
|
||
".wav" => "audio/x-wav",
|
||
_ => "", // send as generic document with undefined MIME type
|
||
};
|
||
if (mimeType == "photo")
|
||
return SendMessageAsync(peer, caption, new InputMediaUploadedPhoto { file = mediaFile }, reply_to_msg_id, entities, schedule_date);
|
||
return SendMessageAsync(peer, caption, new InputMediaUploadedDocument(mediaFile, mimeType), reply_to_msg_id, entities, schedule_date);
|
||
}
|
||
|
||
/// <summary>Helper function to send a text or media message easily</summary>
|
||
/// <param name="peer">Destination of message (chat group, channel, user chat, etc..) </param>
|
||
/// <param name="text">The plain text of the message (or media caption)</param>
|
||
/// <param name="media">An instance of <see cref="InputMedia">InputMedia</see>-derived class, or <see langword="null"/> if there is no associated media</param>
|
||
/// <param name="reply_to_msg_id">Your message is a reply to an existing message with this ID, in the same chat</param>
|
||
/// <param name="entities">Text formatting entities. You can use <see cref="HtmlText.HtmlToEntities">HtmlToEntities</see> or <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
|
||
/// <param name="schedule_date">UTC timestamp when the message should be sent</param>
|
||
/// <param name="disable_preview">Should website/media preview be shown or not, for URLs in your message</param>
|
||
/// <returns>The transmitted message as confirmed by Telegram</returns>
|
||
public async Task<Message> SendMessageAsync(InputPeer peer, string text, InputMedia media = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, bool disable_preview = false)
|
||
{
|
||
UpdatesBase updates;
|
||
long random_id = Helpers.RandomLong();
|
||
if (media == null)
|
||
updates = await this.Messages_SendMessage(peer, text, random_id, no_webpage: disable_preview, entities: entities,
|
||
reply_to_msg_id: reply_to_msg_id == 0 ? null : reply_to_msg_id, schedule_date: schedule_date == default ? null : schedule_date);
|
||
else
|
||
updates = await this.Messages_SendMedia(peer, media, text, random_id, entities: entities,
|
||
reply_to_msg_id: reply_to_msg_id == 0 ? null : reply_to_msg_id, schedule_date: schedule_date == default ? null : schedule_date);
|
||
RaiseUpdate(updates);
|
||
int msgId = -1;
|
||
foreach (var update in updates.UpdateList)
|
||
{
|
||
switch (update)
|
||
{
|
||
case UpdateMessageID updMsgId when updMsgId.random_id == random_id: msgId = updMsgId.id; break;
|
||
case UpdateNewMessage { message: Message message } when message.id == msgId: return message;
|
||
case UpdateNewScheduledMessage { message: Message schedMsg } when schedMsg.id == msgId: return schedMsg;
|
||
}
|
||
}
|
||
if (updates is UpdateShortSentMessage sent)
|
||
{
|
||
return new Message
|
||
{
|
||
flags = (Message.Flags)sent.flags | (reply_to_msg_id == 0 ? 0 : Message.Flags.has_reply_to) | (peer is InputPeerSelf ? 0 : Message.Flags.has_from_id),
|
||
id = sent.id, date = sent.date, message = text, entities = sent.entities, media = sent.media, ttl_period = sent.ttl_period,
|
||
reply_to = reply_to_msg_id == 0 ? null : new MessageReplyHeader { reply_to_msg_id = reply_to_msg_id },
|
||
from_id = peer is InputPeerSelf ? null : new PeerUser { user_id = _session.UserId },
|
||
peer_id = InputToPeer(peer)
|
||
};
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>Helper function to send an album (media group) of photos or documents more easily</summary>
|
||
/// <param name="peer">Destination of message (chat group, channel, user chat, etc..) </param>
|
||
/// <param name="medias">An array or List of <see cref="InputMedia">InputMedia</see>-derived class</param>
|
||
/// <param name="caption">Caption for the media <i>(in plain text)</i> or <see langword="null"/></param>
|
||
/// <param name="reply_to_msg_id">Your message is a reply to an existing message with this ID, in the same chat</param>
|
||
/// <param name="entities">Text formatting entities for the caption. You can use <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
|
||
/// <param name="schedule_date">UTC timestamp when the message should be sent</param>
|
||
/// <returns>The last of the media group messages, confirmed by Telegram</returns>
|
||
/// <remarks>
|
||
/// * The caption/entities are set on the last media<br/>
|
||
/// * <see cref="InputMediaDocumentExternal"/> and <see cref="InputMediaPhotoExternal"/> are supported by downloading the file from the web via HttpClient and sending it to Telegram.
|
||
/// WTelegramClient proxy settings don't apply to HttpClient<br/>
|
||
/// * You may run into errors if you mix, in the same album, photos and file documents having no thumbnails/video attributes
|
||
/// </remarks>
|
||
public async Task<Message[]> SendAlbumAsync(InputPeer peer, ICollection<InputMedia> medias, string caption = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default)
|
||
{
|
||
System.Net.Http.HttpClient httpClient = null;
|
||
int i = 0, length = medias.Count;
|
||
var multiMedia = new InputSingleMedia[length];
|
||
var random_id = Helpers.RandomLong();
|
||
foreach (var media in medias)
|
||
{
|
||
var ism = multiMedia[i] = new InputSingleMedia { random_id = random_id + i, media = media };
|
||
i++;
|
||
retry:
|
||
switch (ism.media)
|
||
{
|
||
case InputMediaUploadedPhoto imup:
|
||
var mmp = (MessageMediaPhoto)await this.Messages_UploadMedia(peer, imup);
|
||
ism.media = mmp.photo;
|
||
break;
|
||
case InputMediaUploadedDocument imud:
|
||
var mmd = (MessageMediaDocument)await this.Messages_UploadMedia(peer, imud);
|
||
ism.media = mmd.document;
|
||
break;
|
||
case InputMediaDocumentExternal imde:
|
||
string mimeType = null;
|
||
var inputFile = await UploadFromUrl(imde.url);
|
||
ism.media = new InputMediaUploadedDocument(inputFile, mimeType);
|
||
goto retry;
|
||
case InputMediaPhotoExternal impe:
|
||
inputFile = await UploadFromUrl(impe.url);
|
||
ism.media = new InputMediaUploadedPhoto { file = inputFile };
|
||
goto retry;
|
||
|
||
async Task<InputFileBase> UploadFromUrl(string url)
|
||
{
|
||
var filename = Path.GetFileName(new Uri(url).LocalPath);
|
||
httpClient ??= new();
|
||
var response = await httpClient.GetAsync(url);
|
||
using var stream = await response.Content.ReadAsStreamAsync();
|
||
mimeType = response.Content.Headers.ContentType?.MediaType;
|
||
if (response.Content.Headers.ContentLength is long length)
|
||
return await UploadFileAsync(new Helpers.IndirectStream(stream) { ContentLength = length }, filename);
|
||
else
|
||
{
|
||
using var ms = new MemoryStream();
|
||
await stream.CopyToAsync(ms);
|
||
ms.Position = 0;
|
||
return await UploadFileAsync(ms, filename);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
var lastMedia = multiMedia[^1];
|
||
lastMedia.message = caption;
|
||
lastMedia.entities = entities;
|
||
if (entities != null) lastMedia.flags = InputSingleMedia.Flags.has_entities;
|
||
|
||
var updates = await this.Messages_SendMultiMedia(peer, multiMedia, reply_to_msg_id: reply_to_msg_id, schedule_date: schedule_date);
|
||
RaiseUpdate(updates);
|
||
var msgIds = new int[length];
|
||
var result = new Message[length];
|
||
foreach (var update in updates.UpdateList)
|
||
{
|
||
switch (update)
|
||
{
|
||
case UpdateMessageID updMsgId: msgIds[updMsgId.random_id - random_id] = updMsgId.id; break;
|
||
case UpdateNewMessage { message: Message message }: result[Array.IndexOf(msgIds, message.id)] = message; break;
|
||
case UpdateNewScheduledMessage { message: Message schedMsg }: result[Array.IndexOf(msgIds, schedMsg.id)] = schedMsg; break;
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private Peer InputToPeer(InputPeer peer) => peer switch
|
||
{
|
||
InputPeerSelf => new PeerUser { user_id = _session.UserId },
|
||
InputPeerUser ipu => new PeerUser { user_id = ipu.user_id },
|
||
InputPeerChat ipc => new PeerChat { chat_id = ipc.chat_id },
|
||
InputPeerChannel ipch => new PeerChannel { channel_id = ipch.channel_id },
|
||
InputPeerUserFromMessage ipufm => new PeerUser { user_id = ipufm.user_id },
|
||
InputPeerChannelFromMessage ipcfm => new PeerChannel { channel_id = ipcfm.channel_id },
|
||
_ => null,
|
||
};
|
||
|
||
/// <summary>Download a photo from Telegram into the outputStream</summary>
|
||
/// <param name="photo">The photo to download</param>
|
||
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
|
||
/// <param name="photoSize">A specific size/version of the photo, or <see langword="null"/> to download the largest version of the photo</param>
|
||
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
|
||
/// <returns>The file type of the photo</returns>
|
||
public async Task<Storage_FileType> DownloadFileAsync(Photo photo, Stream outputStream, PhotoSizeBase photoSize = null, ProgressCallback progress = null)
|
||
{
|
||
if (photoSize is PhotoStrippedSize psp)
|
||
return InflateStrippedThumb(outputStream, psp.bytes) ? Storage_FileType.jpeg : 0;
|
||
photoSize ??= photo.LargestPhotoSize;
|
||
var fileLocation = photo.ToFileLocation(photoSize);
|
||
return await DownloadFileAsync(fileLocation, outputStream, photo.dc_id, photoSize.FileSize, progress);
|
||
}
|
||
|
||
/// <summary>Download a document from Telegram into the outputStream</summary>
|
||
/// <param name="document">The document to download</param>
|
||
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
|
||
/// <param name="thumbSize">A specific size/version of the document thumbnail to download, or <see langword="null"/> to download the document itself</param>
|
||
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
|
||
/// <returns>MIME type of the document/thumbnail</returns>
|
||
public async Task<string> DownloadFileAsync(Document document, Stream outputStream, PhotoSizeBase thumbSize = null, ProgressCallback progress = null)
|
||
{
|
||
if (thumbSize is PhotoStrippedSize psp)
|
||
return InflateStrippedThumb(outputStream, psp.bytes) ? "image/jpeg" : null;
|
||
var fileLocation = document.ToFileLocation(thumbSize);
|
||
var fileType = await DownloadFileAsync(fileLocation, outputStream, document.dc_id, thumbSize?.FileSize ?? document.size, progress);
|
||
return thumbSize == null ? document.mime_type : "image/" + fileType;
|
||
}
|
||
|
||
/// <summary>Download a file from Telegram into the outputStream</summary>
|
||
/// <param name="fileLocation">Telegram file identifier, typically obtained with a .ToFileLocation() call</param>
|
||
/// <param name="outputStream">Stream to write file content to. This method does not close/dispose the stream</param>
|
||
/// <param name="dc_id">(optional) DC on which the file is stored</param>
|
||
/// <param name="fileSize">(optional) Expected file size</param>
|
||
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
|
||
/// <returns>The file type</returns>
|
||
public async Task<Storage_FileType> DownloadFileAsync(InputFileLocationBase fileLocation, Stream outputStream, int dc_id = 0, long fileSize = 0, ProgressCallback progress = null)
|
||
{
|
||
Storage_FileType fileType = Storage_FileType.unknown;
|
||
var client = dc_id == 0 ? this : await GetClientForDC(dc_id, true);
|
||
using var writeSem = new SemaphoreSlim(1);
|
||
bool canSeek = outputStream.CanSeek;
|
||
long streamStartPos = outputStream.Position;
|
||
long fileOffset = 0, maxOffsetSeen = 0;
|
||
long transmitted = 0;
|
||
var tasks = new Dictionary<long, Task>();
|
||
progress?.Invoke(0, fileSize);
|
||
bool abort = false;
|
||
while (!abort)
|
||
{
|
||
await _parallelTransfers.WaitAsync();
|
||
var task = LoadPart(fileOffset);
|
||
lock (tasks) tasks[fileOffset] = task;
|
||
if (dc_id == 0) { await task; dc_id = client._dcSession.DcID; }
|
||
if (!canSeek) await task;
|
||
fileOffset += FilePartSize;
|
||
if (fileSize != 0 && fileOffset >= fileSize)
|
||
{
|
||
if (await task != ((fileSize - 1) % FilePartSize) + 1)
|
||
throw new ApplicationException("Downloaded file size does not match expected file size");
|
||
break;
|
||
}
|
||
|
||
async Task<int> LoadPart(long offset)
|
||
{
|
||
Upload_FileBase fileBase;
|
||
try
|
||
{
|
||
fileBase = await client.Upload_GetFile(fileLocation, offset, FilePartSize);
|
||
}
|
||
catch (RpcException ex) when (ex.Code == 303 && ex.Message == "FILE_MIGRATE_X")
|
||
{
|
||
client = await GetClientForDC(ex.X, true);
|
||
fileBase = await client.Upload_GetFile(fileLocation, offset, FilePartSize);
|
||
}
|
||
catch (RpcException ex) when (ex.Code == 400 && ex.Message == "OFFSET_INVALID")
|
||
{
|
||
abort = true;
|
||
return 0;
|
||
}
|
||
catch (Exception)
|
||
{
|
||
abort = true;
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
_parallelTransfers.Release();
|
||
}
|
||
if (fileBase is not Upload_File fileData)
|
||
throw new ApplicationException("Upload_GetFile returned unsupported " + fileBase?.GetType().Name);
|
||
if (fileData.bytes.Length != FilePartSize) abort = true;
|
||
if (fileData.bytes.Length != 0)
|
||
{
|
||
fileType = fileData.type;
|
||
await writeSem.WaitAsync();
|
||
try
|
||
{
|
||
if (streamStartPos + offset != outputStream.Position) // if we're about to write out of order
|
||
{
|
||
await outputStream.FlushAsync(); // async flush, otherwise Seek would do a sync flush
|
||
outputStream.Seek(streamStartPos + offset, SeekOrigin.Begin);
|
||
}
|
||
await outputStream.WriteAsync(fileData.bytes, 0, fileData.bytes.Length);
|
||
maxOffsetSeen = Math.Max(maxOffsetSeen, offset + fileData.bytes.Length);
|
||
transmitted += fileData.bytes.Length;
|
||
}
|
||
catch (Exception)
|
||
{
|
||
abort = true;
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
writeSem.Release();
|
||
progress?.Invoke(transmitted, fileSize);
|
||
}
|
||
}
|
||
lock (tasks) tasks.Remove(offset);
|
||
return fileData.bytes.Length;
|
||
}
|
||
}
|
||
Task[] remainingTasks;
|
||
lock (tasks) remainingTasks = tasks.Values.ToArray();
|
||
await Task.WhenAll(remainingTasks); // wait completion and eventually propagate any task exception
|
||
await outputStream.FlushAsync();
|
||
if (canSeek) outputStream.Seek(streamStartPos + maxOffsetSeen, SeekOrigin.Begin);
|
||
return fileType;
|
||
}
|
||
|
||
/// <summary>Download the profile photo for a given peer into the outputStream</summary>
|
||
/// <param name="peer">User, Chat or Channel</param>
|
||
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
|
||
/// <param name="big">Whether to download the high-quality version of the picture</param>
|
||
/// <param name="miniThumb">Whether to extract the embedded very low-res thumbnail (synchronous, no actual download needed)</param>
|
||
/// <returns>The file type of the photo, or 0 if no photo available</returns>
|
||
public async Task<Storage_FileType> DownloadProfilePhotoAsync(IPeerInfo peer, Stream outputStream, bool big = false, bool miniThumb = false)
|
||
{
|
||
int dc_id;
|
||
long photo_id;
|
||
byte[] stripped_thumb;
|
||
switch (peer)
|
||
{
|
||
case User user:
|
||
if (user.photo == null) return 0;
|
||
dc_id = user.photo.dc_id;
|
||
photo_id = user.photo.photo_id;
|
||
stripped_thumb = user.photo.stripped_thumb;
|
||
break;
|
||
case ChatBase { Photo: var photo }:
|
||
if (photo == null) return 0;
|
||
dc_id = photo.dc_id;
|
||
photo_id = photo.photo_id;
|
||
stripped_thumb = photo.stripped_thumb;
|
||
break;
|
||
default:
|
||
return 0;
|
||
}
|
||
if (miniThumb && !big)
|
||
return InflateStrippedThumb(outputStream, stripped_thumb) ? Storage_FileType.jpeg : 0;
|
||
var fileLocation = new InputPeerPhotoFileLocation { peer = peer.ToInputPeer(), photo_id = photo_id };
|
||
if (big) fileLocation.flags |= InputPeerPhotoFileLocation.Flags.big;
|
||
return await DownloadFileAsync(fileLocation, outputStream, dc_id);
|
||
}
|
||
|
||
private static bool InflateStrippedThumb(Stream outputStream, byte[] stripped_thumb)
|
||
{
|
||
if (stripped_thumb == null || stripped_thumb.Length <= 3 || stripped_thumb[0] != 1)
|
||
return false;
|
||
var header = Helpers.StrippedThumbJPG;
|
||
outputStream.Write(header, 0, 164);
|
||
outputStream.WriteByte(stripped_thumb[1]);
|
||
outputStream.WriteByte(0);
|
||
outputStream.WriteByte(stripped_thumb[2]);
|
||
outputStream.Write(header, 167, header.Length - 167);
|
||
outputStream.Write(stripped_thumb, 3, stripped_thumb.Length - 3);
|
||
outputStream.WriteByte(0xff);
|
||
outputStream.WriteByte(0xd9);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>Returns the current user dialog list. <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/messages.getDialogs#possible-errors">details</a>)</para></summary>
|
||
/// <param name="folder_id"><a href="https://corefork.telegram.org/api/folders#peer-folders">Peer folder ID, for more info click here</a></param>
|
||
/// <returns>See <a href="https://corefork.telegram.org/constructor/messages.dialogs"/></returns>
|
||
public async Task<Messages_Dialogs> Messages_GetAllDialogs(int? folder_id = null)
|
||
{
|
||
var dialogs = await this.Messages_GetDialogs(folder_id: folder_id);
|
||
switch (dialogs)
|
||
{
|
||
case Messages_DialogsSlice mds:
|
||
var dialogList = new List<DialogBase>();
|
||
var messageList = new List<MessageBase>();
|
||
while (dialogs.Dialogs.Length != 0)
|
||
{
|
||
dialogList.AddRange(dialogs.Dialogs);
|
||
messageList.AddRange(dialogs.Messages);
|
||
var lastDialog = dialogs.Dialogs[^1];
|
||
var lastMsg = dialogs.Messages.LastOrDefault(m => m.Peer.ID == lastDialog.Peer.ID && m.ID == lastDialog.TopMessage);
|
||
var offsetPeer = dialogs.UserOrChat(lastDialog).ToInputPeer();
|
||
dialogs = await this.Messages_GetDialogs(lastMsg?.Date ?? default, lastDialog.TopMessage, offsetPeer, folder_id: folder_id);
|
||
if (dialogs is not Messages_Dialogs md) break;
|
||
foreach (var (key, value) in md.chats) mds.chats[key] = value;
|
||
foreach (var (key, value) in md.users) mds.users[key] = value;
|
||
}
|
||
mds.dialogs = dialogList.ToArray();
|
||
mds.messages = messageList.ToArray();
|
||
return mds;
|
||
case Messages_Dialogs md: return md;
|
||
default: throw new ApplicationException("Messages_GetDialogs returned unexpected " + dialogs?.GetType().Name);
|
||
}
|
||
}
|
||
|
||
/// <summary>Helper method that tries to fetch all participants from a Channel (beyond Telegram server-side limitations)</summary>
|
||
/// <param name="channel">The channel to query</param>
|
||
/// <param name="includeKickBan">Also fetch the kicked/banned members?</param>
|
||
/// <param name="alphabet1">first letters used to search for in participants names<br/>(default values crafted with ♥ to find most latin and cyrillic names)</param>
|
||
/// <param name="alphabet2">second (and further) letters used to search for in participants names</param>
|
||
/// <param name="cancellationToken">Can be used to abort the work of this method</param>
|
||
/// <returns>Field count indicates the total count of members. Field participants contains those that were successfully fetched</returns>
|
||
/// <remarks>⚠ This method can take a few minutes to complete on big broadcast channels. It likely won't be able to obtain the full total count of members</remarks>
|
||
public async Task<Channels_ChannelParticipants> Channels_GetAllParticipants(InputChannelBase channel, bool includeKickBan = false, string alphabet1 = "АБCДЕЄЖФГHИІJКЛМНОПQРСТУВWХЦЧШЩЫЮЯЗ", string alphabet2 = "АCЕHИJЛМНОРСТУВWЫ", CancellationToken cancellationToken = default)
|
||
{
|
||
alphabet2 ??= alphabet1;
|
||
var result = new Channels_ChannelParticipants { chats = new(), users = new() };
|
||
var user_ids = new HashSet<long>();
|
||
var participants = new List<ChannelParticipantBase>();
|
||
|
||
var mcf = await this.Channels_GetFullChannel(channel);
|
||
result.count = mcf.full_chat.ParticipantsCount;
|
||
if (result.count > 2000 && ((Channel)mcf.chats[channel.ChannelId]).IsChannel)
|
||
Helpers.Log(2, "Fetching all participants on a big channel can take several minutes...");
|
||
await GetWithFilter(new ChannelParticipantsAdmins());
|
||
await GetWithFilter(new ChannelParticipantsBots());
|
||
await GetWithFilter(new ChannelParticipantsSearch { q = "" }, (f, c) => new ChannelParticipantsSearch { q = f.q + c }, alphabet1);
|
||
if (includeKickBan)
|
||
{
|
||
await GetWithFilter(new ChannelParticipantsKicked { q = "" }, (f, c) => new ChannelParticipantsKicked { q = f.q + c }, alphabet1);
|
||
await GetWithFilter(new ChannelParticipantsBanned { q = "" }, (f, c) => new ChannelParticipantsBanned { q = f.q + c }, alphabet1);
|
||
}
|
||
result.participants = participants.ToArray();
|
||
return result;
|
||
|
||
async Task GetWithFilter<T>(T filter, Func<T, char, T> recurse = null, string alphabet = null) where T : ChannelParticipantsFilter
|
||
{
|
||
Channels_ChannelParticipants ccp;
|
||
int maxCount = 0;
|
||
for (int offset = 0; ;)
|
||
{
|
||
cancellationToken.ThrowIfCancellationRequested();
|
||
ccp = await this.Channels_GetParticipants(channel, filter, offset, 1024, 0);
|
||
if (ccp.count > maxCount) maxCount = ccp.count;
|
||
foreach (var kvp in ccp.chats) result.chats[kvp.Key] = kvp.Value;
|
||
foreach (var kvp in ccp.users) result.users[kvp.Key] = kvp.Value;
|
||
lock (participants)
|
||
foreach (var participant in ccp.participants)
|
||
if (user_ids.Add(participant.UserID))
|
||
participants.Add(participant);
|
||
offset += ccp.participants.Length;
|
||
if (offset >= ccp.count || ccp.participants.Length == 0) break;
|
||
}
|
||
Helpers.Log(0, $"GetParticipants({(filter as ChannelParticipantsSearch)?.q}) returned {ccp.count}/{maxCount}.\tAccumulated count: {participants.Count}");
|
||
if (recurse != null && (ccp.count < maxCount - 100 || ccp.count == 200 || ccp.count == 1000))
|
||
foreach (var c in alphabet)
|
||
await GetWithFilter(recurse(filter, c), recurse, c == 'А' ? alphabet : alphabet2);
|
||
}
|
||
}
|
||
|
||
private const string OnlyChatChannel = "This method works on Chat & Channel only";
|
||
/// <summary>Generic helper: Adds a single user to a Chat or Channel <para>See <a href="https://corefork.telegram.org/method/messages.addChatUser"/><br/> and <a href="https://corefork.telegram.org/method/channels.inviteToChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
|
||
/// <param name="peer">Chat/Channel</param>
|
||
/// <param name="user">User to be added</param>
|
||
public Task<UpdatesBase> AddChatUser(InputPeer peer, InputUserBase user) => peer switch
|
||
{
|
||
InputPeerChat chat => this.Messages_AddChatUser(chat.chat_id, user, int.MaxValue),
|
||
InputPeerChannel channel => this.Channels_InviteToChannel(channel, user),
|
||
_ => throw new ArgumentException(OnlyChatChannel),
|
||
};
|
||
|
||
/// <summary>Generic helper: Kick a user from a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/channels.editBanned"/><br/> and <a href="https://corefork.telegram.org/method/messages.deleteChatUser"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
|
||
/// <param name="peer">Chat/Channel</param>
|
||
/// <param name="user">User to be removed</param>
|
||
public Task<UpdatesBase> DeleteChatUser(InputPeer peer, InputUser user) => peer switch
|
||
{
|
||
InputPeerChat chat => this.Messages_DeleteChatUser(chat.chat_id, user, true),
|
||
InputPeerChannel channel => this.Channels_EditBanned(channel, user, new ChatBannedRights { flags = ChatBannedRights.Flags.view_messages }),
|
||
_ => throw new ArgumentException(OnlyChatChannel),
|
||
};
|
||
|
||
/// <summary>Generic helper: Leave a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.deleteChatUser"/><br/> and <a href="https://corefork.telegram.org/method/channels.leaveChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
|
||
/// <param name="peer">Chat/Channel to leave</param>
|
||
public Task<UpdatesBase> LeaveChat(InputPeer peer) => peer switch
|
||
{
|
||
InputPeerChat chat => this.Messages_DeleteChatUser(chat.chat_id, InputUser.Self, true),
|
||
InputPeerChannel channel => this.Channels_LeaveChannel(channel),
|
||
_ => throw new ArgumentException(OnlyChatChannel),
|
||
};
|
||
|
||
/// <summary>Generic helper: Make a user admin in a Chat or Channel <para>See <a href="https://corefork.telegram.org/method/messages.editChatAdmin"/><br/> and <a href="https://corefork.telegram.org/method/channels.editAdmin"/> [bots: ✓]</para> <para>Possible <see cref="RpcException"/> codes: 400,403,406</para></summary>
|
||
/// <param name="peer">Chat/Channel</param>
|
||
/// <param name="user">The user to make admin</param>
|
||
/// <param name="is_admin">Whether to make them admin</param>
|
||
public async Task<UpdatesBase> EditChatAdmin(InputPeer peer, InputUserBase user, bool is_admin)
|
||
{
|
||
switch (peer)
|
||
{
|
||
case InputPeerChat chat:
|
||
await this.Messages_EditChatAdmin(chat.chat_id, user, is_admin);
|
||
return new Updates { date = DateTime.UtcNow, users = new(), updates = Array.Empty<Update>(),
|
||
chats = (await this.Messages_GetChats(chat.chat_id)).chats };
|
||
case InputPeerChannel channel:
|
||
return await this.Channels_EditAdmin(channel, user,
|
||
new ChatAdminRights { flags = is_admin ? (ChatAdminRights.Flags)0x8BF : 0 }, null);
|
||
default:
|
||
throw new ArgumentException(OnlyChatChannel);
|
||
}
|
||
}
|
||
|
||
/// <summary>Generic helper: Change the photo of a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.editChatPhoto"/><br/> and <a href="https://corefork.telegram.org/method/channels.editPhoto"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
|
||
/// <param name="peer">Chat/Channel</param>
|
||
/// <param name="photo">New photo</param>
|
||
public Task<UpdatesBase> EditChatPhoto(InputPeer peer, InputChatPhotoBase photo) => peer switch
|
||
{
|
||
InputPeerChat chat => this.Messages_EditChatPhoto(chat.chat_id, photo),
|
||
InputPeerChannel channel => this.Channels_EditPhoto(channel, photo),
|
||
_ => throw new ArgumentException(OnlyChatChannel),
|
||
};
|
||
|
||
/// <summary>Generic helper: Edit the name of a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.editChatTitle"/><br/> and <a href="https://corefork.telegram.org/method/channels.editTitle"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
|
||
/// <param name="peer">Chat/Channel</param>
|
||
/// <param name="title">New name</param>
|
||
public Task<UpdatesBase> EditChatTitle(InputPeer peer, string title) => peer switch
|
||
{
|
||
InputPeerChat chat => this.Messages_EditChatTitle(chat.chat_id, title),
|
||
InputPeerChannel channel => this.Channels_EditTitle(channel, title),
|
||
_ => throw new ArgumentException(OnlyChatChannel),
|
||
};
|
||
|
||
/// <summary>Get full info about a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.getFullChat"/><br/> and <a href="https://corefork.telegram.org/method/channels.getFullChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403,406</para></summary>
|
||
/// <param name="peer">Chat/Channel</param>
|
||
public Task<Messages_ChatFull> GetFullChat(InputPeer peer) => peer switch
|
||
{
|
||
InputPeerChat chat => this.Messages_GetFullChat(chat.chat_id),
|
||
InputPeerChannel channel => this.Channels_GetFullChannel(channel),
|
||
_ => throw new ArgumentException(OnlyChatChannel),
|
||
};
|
||
|
||
/// <summary>Generic helper: Delete a Chat or Channel <para>See <a href="https://corefork.telegram.org/method/messages.deleteChat"/><br/> and <a href="https://corefork.telegram.org/method/channels.deleteChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403,406</para></summary>
|
||
/// <param name="peer">Chat/Channel to delete</param>
|
||
public async Task<UpdatesBase> DeleteChat(InputPeer peer)
|
||
{
|
||
switch (peer)
|
||
{
|
||
case InputPeerChat chat:
|
||
await this.Messages_DeleteChat(chat.chat_id);
|
||
return new Updates { date = DateTime.UtcNow, users = new(), updates = Array.Empty<Update>(),
|
||
chats = (await this.Messages_GetChats(chat.chat_id)).chats };
|
||
case InputPeerChannel channel:
|
||
return await this.Channels_DeleteChannel(channel);
|
||
default:
|
||
throw new ArgumentException(OnlyChatChannel);
|
||
}
|
||
}
|
||
|
||
/// <summary>Generic helper: Get individual messages by IDs [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.getMessages"/><br/> and <a href="https://corefork.telegram.org/method/channels.getMessages"/></para> <para>Possible <see cref="RpcException"/> codes: 400</para></summary>
|
||
/// <param name="peer">User/Chat/Channel</param>
|
||
/// <param name="id">IDs of messages to get</param>
|
||
public Task<Messages_MessagesBase> GetMessages(InputPeer peer, params InputMessage[] id)
|
||
=> peer is InputPeerChannel channel ? this.Channels_GetMessages(channel, id) : this.Messages_GetMessages(id);
|
||
|
||
/// <summary>Generic helper: Delete messages by IDs [bots: ✓]<br/>Messages are deleted for all participants <para>See <a href="https://corefork.telegram.org/method/messages.deleteMessages"/><br/> and <a href="https://corefork.telegram.org/method/channels.deleteMessages"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
|
||
/// <param name="peer">User/Chat/Channel</param>
|
||
/// <param name="id">IDs of messages to delete</param>
|
||
public Task<Messages_AffectedMessages> DeleteMessages(InputPeer peer, params int[] id)
|
||
=> peer is InputPeerChannel channel ? this.Channels_DeleteMessages(channel, id) : this.Messages_DeleteMessages(id, true);
|
||
|
||
/// <summary>Generic helper: Marks message history as read. <para>See <a href="https://corefork.telegram.org/method/messages.readHistory"/><br/> and <a href="https://corefork.telegram.org/method/channels.readHistory"/></para> <para>Possible <see cref="RpcException"/> codes: 400</para></summary>
|
||
/// <param name="peer">User/Chat/Channel</param>
|
||
/// <param name="max_id">If a positive value is passed, only messages with identifiers less or equal than the given one will be marked read</param>
|
||
public async Task<bool> ReadHistory(InputPeer peer, int max_id = default)
|
||
=> peer is InputPeerChannel channel ? await this.Channels_ReadHistory(channel, max_id) : (await this.Messages_ReadHistory(peer, max_id)) != null;
|
||
#endregion
|
||
}
|
||
}
|