using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
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 Client TL Helpers
/// Used to indicate progression of file download/upload
/// transmitted bytes
/// total size of file in bytes, or 0 if unknown
public delegate void ProgressCallback(long transmitted, long totalSize);
/// Helper function to upload a file to Telegram
/// Path to the file to upload
/// (optional) Callback for tracking the progression of the transfer
/// an or than can be used in various requests
public Task UploadFileAsync(string pathname, ProgressCallback progress = null)
=> UploadFileAsync(File.OpenRead(pathname), Path.GetFileName(pathname), progress);
/// Helper function to upload a file to Telegram
/// Content of the file to upload. This method close/dispose the stream
/// Name of the file
/// (optional) Callback for tracking the progression of the transfer
/// an or than can be used in various requests
public async Task UploadFileAsync(Stream stream, string filename, ProgressCallback progress = null)
{
using (stream)
{
bool hasLength = stream.CanSeek;
long transmitted = 0, length = hasLength ? stream.Length : -1;
bool isBig = !hasLength || length >= 10 * 1024 * 1024;
int file_total_parts = hasLength ? (int)((length - 1) / FilePartSize) + 1 : -1;
long file_id = Helpers.RandomLong();
int file_part = 0, read;
var tasks = new Dictionary();
bool abort = false;
for (long bytesLeft = hasLength ? length : long.MaxValue; !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;
if (!hasLength && read < bytes.Length)
{
file_total_parts = file_part;
if (read == 0) break; else file_total_parts++;
bytes = bytes[..read];
bytesLeft = 0;
}
var task = SavePart(file_part, bytes);
lock (tasks) tasks[file_part] = task;
if (read < FilePartSize && bytesLeft != 0) throw new WTException($"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
return isBig ? new InputFileBig { id = file_id, parts = file_total_parts, name = filename }
: new InputFile { id = file_id, parts = file_total_parts, name = filename };
}
}
/// Search messages in chat with filter and text See
/// See for a list of possible filter types
/// User or chat, histories with which are searched, or constructor for global search
/// Text search request
/// Only return messages starting from the specified message ID
/// Number of results to return
public Task Messages_Search(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);
/// Search messages globally with filter and text See
/// See for a list of possible filter types
/// Text search request
/// Only return messages starting from the specified message ID
/// Number of results to return
public Task Messages_SearchGlobal(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);
/// Helper function to send a media message more easily
/// Destination of message (chat group, channel, user chat, etc..)
/// Caption for the media (in plain text) or
/// Media file already uploaded to TG (see UploadFileAsync)
/// for automatic detection, "photo" for an inline photo, "video" for a streamable MP4 video, or a MIME type to send as a document
/// Your message is a reply to an existing message with this ID, in the same chat
/// Text formatting entities for the caption. You can use MarkdownToEntities to create these
/// UTC timestamp when the message should be sent
/// The transmitted message confirmed by Telegram
public Task SendMediaAsync(InputPeer peer, string caption, InputFileBase uploadedFile, string mimeType = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default)
{
mimeType ??= Path.GetExtension(uploadedFile.Name)?.ToLowerInvariant() switch
{
".jpg" or ".jpeg" or ".png" or ".bmp" => "photo",
".mp4" => "video",
".gif" => "image/gif",
".webp" => "image/webp",
".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 = uploadedFile }, reply_to_msg_id, entities, schedule_date);
else if (mimeType == "video")
return SendMessageAsync(peer, caption, new InputMediaUploadedDocument(uploadedFile, "video/mp4", new DocumentAttributeVideo { flags = DocumentAttributeVideo.Flags.supports_streaming }), reply_to_msg_id, entities, schedule_date);
else
return SendMessageAsync(peer, caption, new InputMediaUploadedDocument(uploadedFile, mimeType), reply_to_msg_id, entities, schedule_date);
}
public enum LinkPreview { Disabled = 0, BelowText = 1, AboveText = 2 };
/// Helper function to send a text or media message easily
/// Destination of message (chat group, channel, user chat, etc..)
/// The plain text of the message (or media caption)
/// An instance of InputMedia-derived class, or if there is no associated media
/// Your message is a reply to an existing message with this ID, in the same chat
/// Text formatting entities. You can use HtmlToEntities or MarkdownToEntities to create these
/// UTC timestamp when the message should be sent
/// Should website/media preview be shown below, above or not, for URL links in your message
/// The transmitted message as confirmed by Telegram
public async Task SendMessageAsync(InputPeer peer, string text, InputMedia media = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, LinkPreview preview = LinkPreview.BelowText)
{
UpdatesBase updates;
long random_id = Helpers.RandomLong();
if (media == null)
updates = await this.Messages_SendMessage(peer, text, random_id, entities: entities,
no_webpage: preview == LinkPreview.Disabled, invert_media: preview == LinkPreview.AboveText,
reply_to: reply_to_msg_id == 0 ? null : new InputReplyToMessage { reply_to_msg_id = 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: reply_to_msg_id == 0 ? null : new InputReplyToMessage { reply_to_msg_id = reply_to_msg_id }, schedule_date: schedule_date == default ? null : schedule_date);
int msgId = -1;
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, flags = MessageReplyHeader.Flags.has_reply_to_msg_id },
from_id = peer is InputPeerSelf ? null : new PeerUser { user_id = _session.UserId },
peer_id = InputToPeer(peer)
};
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;
}
}
return null;
}
/// Helper function to send an album (media group) of photos or documents more easily
/// Destination of message (chat group, channel, user chat, etc..)
/// An array or List of InputMedia-derived class
/// Caption for the media (in plain text) or
/// Your message is a reply to an existing message with this ID, in the same chat
/// Text formatting entities for the caption. You can use MarkdownToEntities to create these
/// UTC timestamp when the message should be sent
/// Any URL pointing to a video should be considered as non-streamable
/// The last of the media group messages, confirmed by Telegram
///
/// * The caption/entities are set on the last media
/// * and are supported by downloading the file from the web via HttpClient and sending it to Telegram.
/// WTelegramClient proxy settings don't apply to HttpClient
/// * You may run into errors if you mix, in the same album, photos and file documents having no thumbnails/video attributes
///
public async Task SendAlbumAsync(InputPeer peer, ICollection medias, string caption = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, bool videoUrlAsFile = false)
{
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);
if (mimeType?.StartsWith("video/") == true)
ism.media = new InputMediaUploadedDocument(inputFile, mimeType, new DocumentAttributeVideo { flags = DocumentAttributeVideo.Flags.supports_streaming });
else
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 UploadFromUrl(string url)
{
var filename = Path.GetFileName(new Uri(url).LocalPath);
httpClient ??= new();
using var response = await httpClient.GetAsync(url, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
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
return await UploadFileAsync(stream, filename);
}
}
}
var firstMedia = multiMedia[0];
firstMedia.message = caption;
firstMedia.entities = entities;
if (entities != null) firstMedia.flags = InputSingleMedia.Flags.has_entities;
var updates = await this.Messages_SendMultiMedia(peer, multiMedia, reply_to: reply_to_msg_id == 0 ? null : new InputReplyToMessage { reply_to_msg_id = reply_to_msg_id }, schedule_date: schedule_date);
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,
};
/// Download a photo from Telegram into the outputStream
/// The photo to download
/// Stream to write the file content to. This method does not close/dispose the stream
/// A specific size/version of the photo, or to download the largest version of the photo
/// (optional) Callback for tracking the progression of the transfer
/// The file type of the photo
public async Task 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);
}
/// Download a document from Telegram into the outputStream
/// The document to download
/// Stream to write the file content to. This method does not close/dispose the stream
/// A specific size/version of the document thumbnail to download, or to download the document itself
/// (optional) Callback for tracking the progression of the transfer
/// MIME type of the document/thumbnail
public async Task 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;
}
/// Download a file from Telegram into the outputStream
/// Telegram file identifier, typically obtained with a .ToFileLocation() call
/// Stream to write file content to. This method does not close/dispose the stream
/// (optional) DC on which the file is stored
/// (optional) Expected file size
/// (optional) Callback for tracking the progression of the transfer
/// The file type
public async Task 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 = canSeek ? outputStream.Position : 0;
long fileOffset = 0, maxOffsetSeen = 0;
long transmitted = 0;
var tasks = new Dictionary();
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 WTException("Downloaded file size does not match expected file size");
break;
}
async Task 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 WTException("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 (canSeek && 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;
progress?.Invoke(transmitted, fileSize);
}
catch (Exception)
{
abort = true;
throw;
}
finally
{
writeSem.Release();
}
}
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;
}
/// Download the profile photo for a given peer into the outputStream
/// User, Chat or Channel
/// Stream to write the file content to. This method does not close/dispose the stream
/// Whether to download the high-quality version of the picture
/// Whether to extract the embedded very low-res thumbnail (synchronous, no actual download needed)
/// The file type of the photo, or 0 if no photo available
public async Task 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;
}
/// Get all chats, channels and supergroups
public async Task Messages_GetAllChats()
{
var dialogs = await Messages_GetAllDialogs();
return new Messages_Chats { chats = dialogs.chats };
}
/// Returns the current user dialog list. Possible codes: 400 (details)
/// Peer folder ID, for more info click here
/// See
public async Task 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();
var messageList = new List();
while (dialogs.Dialogs.Length != 0)
{
dialogList.AddRange(dialogs.Dialogs);
messageList.AddRange(dialogs.Messages);
int last = dialogs.Dialogs.Length - 1;
var lastDialog = dialogs.Dialogs[last];
var lastPeer = dialogs.UserOrChat(lastDialog).ToInputPeer();
var lastMsgId = lastDialog.TopMessage;
retryDate:
var lastDate = dialogs.Messages.LastOrDefault(m => m.Peer.ID == lastDialog.Peer.ID && m.ID == lastDialog.TopMessage)?.Date ?? default;
if (lastDate == default)
if (--last < 0) break; else { lastDialog = dialogs.Dialogs[last]; goto retryDate; }
dialogs = await this.Messages_GetDialogs(lastDate, lastMsgId, lastPeer, 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 WTException("Messages_GetDialogs returned unexpected " + dialogs?.GetType().Name);
}
}
/// Helper method that tries to fetch all participants from a Channel (beyond Telegram server-side limitations)
/// The channel to query
/// Also fetch the kicked/banned members?
/// first letters used to search for in participants names
(default values crafted with ♥ to find most latin and cyrillic names)
/// second (and further) letters used to search for in participants names
/// Can be used to abort the work of this method
/// Field count indicates the total count of members. Field participants contains those that were successfully fetched
/// ⚠ 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
public async Task 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();
var participants = new List();
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 filter, Func 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);
}
}
/// Helper simplified method: Get the admin log of a channel/supergroup See Possible codes: 400,403 (details)
/// Channel
/// Search query, can be empty
/// Event filter
/// Only show events from this admin
public async Task Channels_GetAdminLog(InputChannelBase channel, ChannelAdminLogEventsFilter.Flags events_filter = 0, string q = null, InputUserBase admin = null)
{
var admins = admin == null ? null : new[] { admin };
var result = await this.Channels_GetAdminLog(channel, q, events_filter: events_filter, admins: admins);
var resultFull = result;
var events = new List(result.events);
while (result.events.Length > 0)
{
result = await this.Channels_GetAdminLog(channel, q, max_id: result.events[^1].id, events_filter: events_filter, admins: admins);
events.AddRange(result.events);
foreach (var kvp in result.chats) resultFull.chats[kvp.Key] = kvp.Value;
foreach (var kvp in result.users) resultFull.users[kvp.Key] = kvp.Value;
}
resultFull.events = events.ToArray();
return resultFull;
}
private const string OnlyChatChannel = "This method works on Chat & Channel only";
/// Generic helper: Adds a single user to a Chat or Channel See
and Possible codes: 400,403
/// Chat/Channel
/// User to be added
public Task 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),
};
/// Generic helper: Kick a user from a Chat or Channel [bots: ✓] See
and Possible codes: 400,403
/// Chat/Channel
/// User to be removed
public Task 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),
};
/// Generic helper: Leave a Chat or Channel [bots: ✓] See
and Possible codes: 400,403
/// Chat/Channel to leave
public Task 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),
};
/// Generic helper: Make a user admin in a Chat or Channel See
and [bots: ✓] Possible codes: 400,403,406
/// Chat/Channel
/// The user to make admin
/// Whether to make them admin
public async Task 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(),
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);
}
}
/// Generic helper: Change the photo of a Chat or Channel [bots: ✓] See
and Possible codes: 400,403
/// Chat/Channel
/// New photo
public Task 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),
};
/// Generic helper: Edit the name of a Chat or Channel [bots: ✓] See
and Possible codes: 400,403
/// Chat/Channel
/// New name
public Task 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),
};
/// Get full info about a Chat or Channel [bots: ✓] See
and Possible codes: 400,403,406
/// Chat/Channel
public Task GetFullChat(InputPeer peer) => peer switch
{
InputPeerChat chat => this.Messages_GetFullChat(chat.chat_id),
InputPeerChannel channel => this.Channels_GetFullChannel(channel),
_ => throw new ArgumentException(OnlyChatChannel),
};
/// Generic helper: Delete a Chat or Channel See
and Possible codes: 400,403,406
/// Chat/Channel to delete
public async Task 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(),
chats = (await this.Messages_GetChats(chat.chat_id)).chats };
case InputPeerChannel channel:
return await this.Channels_DeleteChannel(channel);
default:
throw new ArgumentException(OnlyChatChannel);
}
}
/// If you want to get all messages from a chat, use method Messages_GetHistory
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822")]
public Task GetMessages(InputPeer peer)
=> throw new WTException("If you want to get all messages from a chat, use method Messages_GetHistory");
/// Generic helper: Get individual messages by IDs [bots: ✓] See
and Possible codes: 400
/// User/Chat/Channel
/// IDs of messages to get
public Task GetMessages(InputPeer peer, params InputMessage[] id)
=> peer is InputPeerChannel channel ? this.Channels_GetMessages(channel, id) : this.Messages_GetMessages(id);
/// Generic helper: Delete messages by IDs [bots: ✓]
Messages are deleted for all participants See
and Possible codes: 400,403
/// User/Chat/Channel
/// IDs of messages to delete
public Task DeleteMessages(InputPeer peer, params int[] id)
=> peer is InputPeerChannel channel ? this.Channels_DeleteMessages(channel, id) : this.Messages_DeleteMessages(id, true);
/// Generic helper: Marks message history as read. See
and Possible codes: 400
/// User/Chat/Channel
/// If a positive value is passed, only messages with identifiers less or equal than the given one will be marked read
public async Task 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;
private static readonly char[] UrlSeparator = new[] { '?', '#', '/' };
/// Return information about a chat/channel based on Invite Link or Public Link
/// Public link or Invite link, like https://t.me/+InviteHash, https://t.me/joinchat/InviteHash or https://t.me/channelname
Works also without https:// prefix
/// to also join the chat/channel
/// previously collected chats, to prevent unnecessary ResolveUsername
/// a Chat or Channel, possibly partial Channel information only (with flag )
public async Task AnalyzeInviteLink(string url, bool join = false, IDictionary chats = null)
{
int start = url.IndexOf("//");
start = url.IndexOf('/', start + 2) + 1;
int end = url.IndexOfAny(UrlSeparator, start);
if (end == -1) end = url.Length;
if (start == 0 || end == start) throw new ArgumentException("Invalid URL");
string hash;
if (url[start] == '+')
hash = url[(start + 1)..end];
else if (string.Compare(url, start, "joinchat/", 0, 9, StringComparison.OrdinalIgnoreCase) == 0)
hash = url[(end + 1)..];
else
{
var chat = await CachedOrResolveUsername(url[start..end], chats);
if (join && chat is Channel channel)
try
{
var res = await this.Channels_JoinChannel(channel);
chat = res.Chats[channel.id];
}
catch (RpcException ex) when (ex.Code == 400 && ex.Message == "INVITE_REQUEST_SENT") { }
return chat;
}
var chatInvite = await this.Messages_CheckChatInvite(hash);
if (join)
try
{
var res = await this.Messages_ImportChatInvite(hash);
if (res.Chats.Values.FirstOrDefault() is ChatBase chat) return chat;
}
catch (RpcException ex) when (ex.Code == 400 && ex.Message == "INVITE_REQUEST_SENT") { }
switch (chatInvite)
{
case ChatInviteAlready cia: return cia.chat;
case ChatInvitePeek cip: return cip.chat;
case ChatInvite ci:
ChatPhoto chatPhoto = null;
if (ci.photo is Photo photo)
{
var stripped_thumb = photo.sizes.OfType().FirstOrDefault()?.bytes;
chatPhoto = new ChatPhoto
{
dc_id = photo.dc_id,
photo_id = photo.id,
stripped_thumb = stripped_thumb,
flags = (stripped_thumb != null ? ChatPhoto.Flags.has_stripped_thumb : 0) |
(photo.flags.HasFlag(Photo.Flags.has_video_sizes) ? ChatPhoto.Flags.has_video : 0),
};
}
var rrAbout = ci.about == null ? null : new RestrictionReason[] { new() { text = ci.about } };
return !ci.flags.HasFlag(ChatInvite.Flags.channel)
? new Chat { title = ci.title, photo = chatPhoto, participants_count = ci.participants_count }
: new Channel { title = ci.title, photo = chatPhoto, participants_count = ci.participants_count,
restriction_reason = rrAbout, flags = Channel.Flags.min |
(ci.flags.HasFlag(ChatInvite.Flags.broadcast) ? Channel.Flags.broadcast : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.public_) ? Channel.Flags.has_username : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.megagroup) ? Channel.Flags.megagroup : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.verified) ? Channel.Flags.verified : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.scam) ? Channel.Flags.scam : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.fake) ? Channel.Flags.fake : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.request_needed) ? Channel.Flags.join_request : 0) };
}
return null;
}
/// Return chat and message details based on a Message Link (URL)
/// Message Link, like https://t.me/c/1234567890/1234 or t.me/channelname/1234
/// previously collected chats, to prevent unnecessary ResolveUsername
/// Structure containing the message, chat and user details
/// If link is for private group (t.me/c/..), user must have joined that group
public async Task GetMessageByLink(string url, IDictionary chats = null)
{
int start = url.IndexOf("//");
start = url.IndexOf('/', start + 2) + 1;
int slash = url.IndexOf('/', start + 2);
int msgStart = slash + 1;
int end = url.IndexOfAny(UrlSeparator, msgStart);
if (end == -1) end = url.Length;
else if (url[end] == '/' && char.IsDigit(url[msgStart]) && url.Length > end + 1 && char.IsDigit(url[end + 1]))
{
end = url.IndexOfAny(UrlSeparator, msgStart = end + 1);
if (end == -1) end = url.Length;
}
if (start == 0 || slash == -1 || end <= slash + 1 || !char.IsDigit(url[msgStart])) throw new ArgumentException("Invalid URL");
int msgId = int.Parse(url[msgStart..end]);
ChatBase chat;
if (url[start] is 'c' or 'C' && url[start + 1] == '/')
{
long chatId = long.Parse(url[(start + 2)..slash]);
if (chats?.TryGetValue(chatId, out chat) != true)
{
var mc = await this.Channels_GetChannels(new InputChannel(chatId, 0));
if (!mc.chats.TryGetValue(chatId, out chat))
throw new WTException($"Channel {chatId} not found");
else if (chats != null)
chats[chatId] = chat;
}
}
else
chat = await CachedOrResolveUsername(url[start..slash], chats);
if (chat is not Channel channel) throw new WTException($"URL does not identify a valid Channel");
return await this.Channels_GetMessages(channel, msgId) as Messages_ChannelMessages;
}
private async Task CachedOrResolveUsername(string username, IDictionary chats = null)
{
if (chats == null)
return (await this.Contacts_ResolveUsername(username)).Chat;
ChatBase chat;
lock (chats)
chat = chats.Values.OfType().FirstOrDefault(ch => ch.ActiveUsernames.Contains(username, StringComparer.OrdinalIgnoreCase));
if (chat == null)
{
chat = (await this.Contacts_ResolveUsername(username)).Chat;
if (chat != null) lock (chats) chats[chat.ID] = chat;
}
return chat;
}
#endregion
}
}