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 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 var md5 = MD5.Create(); using (stream) { bool hasLength = stream.CanSeek; long transmitted = 0, length = hasLength ? stream.Length : -1; bool isBig = hasLength ? length >= 10 * 1024 * 1024 : true; 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 (!isBig) md5.TransformBlock(bytes, 0, read, null, 0); 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 if (!isBig) md5.TransformFinalBlock(Array.Empty(), 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 }; } } /// 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, 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 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); } /// 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 or not, for URLs 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, 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: 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); RaiseUpdate(updates); 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 }, 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 /// 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) { 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 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: reply_to_msg_id == 0 ? null : new InputReplyToMessage { 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, }; /// 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 = outputStream.Position; 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 (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 https://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); if (start == 0 || slash == -1) throw new ArgumentException("Invalid URL"); int end = url.IndexOfAny(UrlSeparator, slash + 1); if (end == -1) end = url.Length; int msgId = int.Parse(url[(slash + 1)..end]); ChatBase chat; if (url[start] is 'c' or 'C' && url[start + 1] == '/') { long chatId = long.Parse(url[(start + 2)..slash]); 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 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 } }