diff --git a/.github/dev.yml b/.github/dev.yml index 44fe595..5e87822 100644 --- a/.github/dev.yml +++ b/.github/dev.yml @@ -2,7 +2,7 @@ pr: none trigger: - master -name: 1.6.5-dev.$(Rev:r) +name: 1.7.1-dev.$(Rev:r) pool: vmImage: ubuntu-latest diff --git a/.github/release.yml b/.github/release.yml index eebf040..4ff32b6 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,7 +1,7 @@ pr: none trigger: none -name: 1.6.$(Rev:r) +name: 1.7.$(Rev:r) pool: vmImage: ubuntu-latest diff --git a/EXAMPLES.md b/EXAMPLES.md index 9a82e7b..48a5cc1 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -83,21 +83,20 @@ await client.SendMediaAsync(peer, "Here is the photo", inputFile); ```csharp using var client = new WTelegram.Client(Environment.GetEnvironmentVariable); await client.LoginUserIfNeeded(); -var dialogsBase = await client.Messages_GetDialogs(default, 0, null, 0, 0); -if (dialogsBase is Messages_Dialogs dialogs) - while (dialogs.dialogs.Length != 0) - { - foreach (var dialog in dialogs.dialogs) - switch (dialogs.UserOrChat(dialog)) - { - case UserBase user when user.IsActive: Console.WriteLine("User " + user); break; - case ChatBase chat when chat.IsActive: Console.WriteLine(chat); break; - } - 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 = (Messages_Dialogs)await client.Messages_GetDialogs(lastMsg?.Date ?? default, lastDialog.TopMessage, offsetPeer, 500, 0); - } +var dialogs = await client.Messages_GetDialogs(default, 0, null, 0, 0); +while (dialogs.Dialogs.Length != 0) +{ + foreach (var dialog in dialogs.Dialogs) + switch (dialogs.UserOrChat(dialog)) + { + case UserBase user when user.IsActive: Console.WriteLine("User " + user); break; + case ChatBase chat when chat.IsActive: Console.WriteLine(chat); break; + } + 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 client.Messages_GetDialogs(lastMsg?.Date ?? default, lastDialog.TopMessage, offsetPeer, 500, 0); +} ``` *Note: the lists returned by Messages_GetDialogs contains the `access_hash` for those chats and users.* @@ -138,13 +137,12 @@ var chats = await client.Messages_GetAllChats(null); InputPeer peer = chats.chats[1234567890]; // the chat we want for (int offset = 0; ;) { - var messagesBase = await client.Messages_GetHistory(peer, 0, default, offset, 1000, 0, 0, 0); - if (messagesBase is not Messages_ChannelMessages channelMessages) break; - foreach (var msgBase in channelMessages.messages) + var messages = await client.Messages_GetHistory(peer, 0, default, offset, 1000, 0, 0, 0); + foreach (var msgBase in messages.Messages) if (msgBase is Message msg) Console.WriteLine(msg.message); - offset += channelMessages.messages.Length; - if (offset >= channelMessages.count) break; + offset += messages.Messages.Length; + if (offset >= messages.Count) break; } ``` ### Monitor all Telegram events happening for the user diff --git a/README.md b/README.md index bbe31b5..f3686ec 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,8 @@ This library can be used for any Telegram scenarios including: - Download/upload of files/media - etc... +It has been tested in a Console app, WinForms app, ASP.NET webservice. + Secret chats (end-to-end encryption, PFS) and connection to CDN DCs have not been tested yet. Please don't use this library for Spam or Scam. Respect Telegram [Terms of Service](https://telegram.org/tos) or you might get banned from Telegram servers. diff --git a/src/Client.cs b/src/Client.cs index 7e807d4..3b518a0 100644 --- a/src/Client.cs +++ b/src/Client.cs @@ -577,7 +577,7 @@ namespace WTelegram writer.Write(msgId); // int64 message_id writer.Write(0); // int32 message_data_length (to be patched) writer.WriteTLObject(msg); // bytes message_data - Helpers.Log(1, $"{_dcSession.DcID}>Sending {msg.GetType().Name}..."); + Helpers.Log(1, $"{_dcSession.DcID}>Sending {msg.GetType().Name.TrimEnd('_')}..."); BinaryPrimitives.WriteInt32LittleEndian(memStream.GetBuffer().AsSpan(20), (int)memStream.Length - 24); // patch message_data_length } else @@ -597,9 +597,9 @@ namespace WTelegram clearWriter.Write(0); // int32 message_data_length (to be patched) clearWriter.WriteTLObject(msg); // bytes message_data if ((seqno & 1) != 0) - Helpers.Log(1, $"{_dcSession.DcID}>Sending {msg.GetType().Name,-40} #{(short)msgId.GetHashCode():X4}"); + Helpers.Log(1, $"{_dcSession.DcID}>Sending {msg.GetType().Name.TrimEnd('_'),-40} #{(short)msgId.GetHashCode():X4}"); else - Helpers.Log(1, $"{_dcSession.DcID}>Sending {msg.GetType().Name,-40} {MsgIdToStamp(msgId):u} (svc)"); + Helpers.Log(1, $"{_dcSession.DcID}>Sending {msg.GetType().Name.TrimEnd('_'),-40} {MsgIdToStamp(msgId):u} (svc)"); int clearLength = (int)clearStream.Length - prepend; // length before padding (= 32 + message_data_length) int padding = (0x7FFFFFF0 - clearLength) % 16; #if !MTPROTO1 @@ -1092,7 +1092,8 @@ namespace WTelegram /// 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 - 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) + /// 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) { var filename = mediaFile is InputFile iFile ? iFile.name : (mediaFile as InputFileBig)?.name; mimeType ??= Path.GetExtension(filename).ToLowerInvariant() switch @@ -1123,16 +1124,53 @@ namespace WTelegram /// Text formatting entities. You can use 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 - public 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) + /// 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) - return this.Messages_SendMessage(peer, text, Helpers.RandomLong(), - no_webpage: disable_preview, reply_to_msg_id: reply_to_msg_id, entities: entities, schedule_date: schedule_date); - else - return this.Messages_SendMedia(peer, media, text, Helpers.RandomLong(), + updates = await this.Messages_SendMessage(peer, text, random_id, no_webpage: disable_preview, reply_to_msg_id: reply_to_msg_id, entities: entities, schedule_date: schedule_date); + else + updates = await this.Messages_SendMedia(peer, media, text, random_id, + reply_to_msg_id: reply_to_msg_id, entities: entities, schedule_date: schedule_date); + OnUpdate(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; } + 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 diff --git a/src/TL.Helpers.cs b/src/TL.Helpers.cs index 4ecf5ca..f36082c 100644 --- a/src/TL.Helpers.cs +++ b/src/TL.Helpers.cs @@ -120,6 +120,8 @@ namespace TL public static implicit operator InputChannel(Channel channel) => new() { channel_id = channel.id, access_hash = channel.access_hash }; public override string ToString() => (flags.HasFlag(Flags.broadcast) ? "Channel " : "Group ") + (username != null ? '@' + username : $"\"{title}\""); + public bool IsChannel => (flags & Flags.broadcast) != 0; + public bool IsGroup => (flags & Flags.broadcast) == 0; } partial class ChannelForbidden { @@ -213,7 +215,7 @@ namespace TL } partial class Contacts_Blocked { public IPeerInfo UserOrChat(PeerBlocked peer) => peer.peer_id.UserOrChat(users, chats); } - partial class Messages_Dialogs { public IPeerInfo UserOrChat(DialogBase dialog) => dialog.Peer.UserOrChat(users, chats); } + partial class Messages_DialogsBase { public IPeerInfo UserOrChat(DialogBase dialog) => UserOrChat(dialog.Peer); } partial class Messages_MessagesBase { public abstract int Count { get; } } partial class Messages_Messages { public override int Count => messages.Length; } @@ -310,6 +312,35 @@ namespace TL public override int Timeout => timeout; } + partial class UpdatesBase { public abstract Update[] UpdateList { get; } } + partial class UpdatesTooLong { public override Update[] UpdateList => Array.Empty(); } + partial class UpdateShort { public override Update[] UpdateList => new[] { update }; } + partial class UpdatesCombined { public override Update[] UpdateList => updates; } + partial class Updates { public override Update[] UpdateList => updates; } + partial class UpdateShortSentMessage { public override Update[] UpdateList => Array.Empty(); } + partial class UpdateShortMessage { public override Update[] UpdateList => new[] { new UpdateNewMessage + { + message = new Message + { + flags = (Message.Flags)flags, id = id, date = date, + message = message, entities = entities, reply_to = reply_to, + from_id = new PeerUser { user_id = user_id }, + peer_id = new PeerUser { user_id = user_id }, + fwd_from = fwd_from, via_bot_id = via_bot_id, ttl_period = ttl_period + }, pts = pts, pts_count = pts_count + } }; } + partial class UpdateShortChatMessage { public override Update[] UpdateList => new[] { new UpdateNewMessage + { + message = new Message + { + flags = (Message.Flags)flags, id = id, date = date, + message = message, entities = entities, reply_to = reply_to, + from_id = new PeerUser { user_id = from_id }, + peer_id = new PeerChat { chat_id = chat_id }, + fwd_from = fwd_from, via_bot_id = via_bot_id, ttl_period = ttl_period + }, pts = pts, pts_count = pts_count + } }; } + partial class Messages_PeerDialogs { public IPeerInfo UserOrChat(DialogBase dialog) => dialog.Peer.UserOrChat(users, chats); } partial class SecureFile diff --git a/src/TL.Schema.cs b/src/TL.Schema.cs index 0f47962..79670b2 100644 --- a/src/TL.Schema.cs +++ b/src/TL.Schema.cs @@ -4250,7 +4250,7 @@ namespace TL /// returns a or for the given Peer public IPeerInfo UserOrChat(Peer peer) => peer.UserOrChat(users, chats); } - /// See + /// Full constructor of updates See [TLDef(0x74AE4240)] public partial class Updates : UpdatesBase { @@ -4819,7 +4819,7 @@ namespace TL public override byte[] Bytes => bytes; } - /// Derived classes: , See + /// Contains info on cofiguring parameters for key generation by Diffie-Hellman protocol. Derived classes: , See public abstract partial class Messages_DhConfigBase : IObject { } /// Configuring parameters did not change. See [TLDef(0xC0E24635)] @@ -7323,7 +7323,7 @@ namespace TL public TopPeer[] peers; } - /// Derived classes: , See + /// Top peers Derived classes: , See /// a null value means contacts.topPeersNotModified public abstract partial class Contacts_TopPeersBase : IObject { } /// Top peers See @@ -7386,7 +7386,7 @@ namespace TL } } - /// Derived classes: , See + /// Featured stickers Derived classes: , See public abstract partial class Messages_FeaturedStickersBase : IObject { } /// Featured stickers haven't changed See [TLDef(0xC6DC0C66)] @@ -7434,7 +7434,7 @@ namespace TL public StickerSetCoveredBase[] sets; } - /// Derived classes: , See + /// Result of stickerset installation process Derived classes: , See public abstract partial class Messages_StickerSetInstallResult : IObject { } /// The stickerset was installed successfully See [TLDef(0x38641628)] @@ -8320,7 +8320,7 @@ namespace TL } } - /// See + /// Validated user-provided info See [TLDef(0xD1451883)] public partial class Payments_ValidatedRequestedInfo : IObject { @@ -8340,7 +8340,7 @@ namespace TL } } - /// Derived classes: , See + /// Payment result Derived classes: , See public abstract partial class Payments_PaymentResultBase : IObject { } /// Payment result See [TLDef(0x4E5F810D)] @@ -9481,7 +9481,7 @@ namespace TL public StickerSetCoveredBase[] sets; } - /// See + /// SHA256 Hash of an uploaded file, to be checked for validity after download See [TLDef(0x6242C773)] public partial class FileHash : IObject { @@ -9503,7 +9503,7 @@ namespace TL public int port; } - /// Derived classes: , See + /// Update of Telegram's terms of service Derived classes: , See public abstract partial class Help_TermsOfServiceUpdateBase : IObject { } /// No changes were made to telegram's terms of service See [TLDef(0xE3309F7F)] @@ -18461,7 +18461,7 @@ namespace TL bytes = bytes, }); - /// See + /// Returns content of an HTTP file or a part, by proxying the request through telegram. See [TLDef(0x24E6818D)] public partial class Upload_GetWebFile_ : IMethod { @@ -18472,7 +18472,7 @@ namespace TL /// Number of bytes to be returned public int limit; } - /// See Possible codes: 400 (details) + /// Returns content of an HTTP file or a part, by proxying the request through telegram. See Possible codes: 400 (details) /// The file to download /// Number of bytes to be skipped /// Number of bytes to be returned diff --git a/src/TL.cs b/src/TL.cs index afc4bfc..ce45015 100644 --- a/src/TL.cs +++ b/src/TL.cs @@ -36,7 +36,7 @@ namespace TL var tlDef = type.GetCustomAttribute(); var ctorNb = tlDef.CtorNb; writer.Write(ctorNb); - IEnumerable fields = type.GetFields(); + IEnumerable fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!tlDef.inheritAfter) fields = fields.GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g); int flags = 0; IfFlagAttribute ifFlag; @@ -57,7 +57,7 @@ namespace TL if (type == null) return null; // nullable ctor (class meaning is associated with null) var tlDef = type.GetCustomAttribute(); var obj = Activator.CreateInstance(type); - IEnumerable fields = type.GetFields(); + IEnumerable fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!tlDef.inheritAfter) fields = fields.GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g); int flags = 0; IfFlagAttribute ifFlag; @@ -177,9 +177,9 @@ namespace TL writer.Write(0); // patched below writer.WriteTLObject(msg.body); if ((msg.seqno & 1) != 0) - WTelegram.Helpers.Log(1, $" Sending → {msg.body.GetType().Name,-40} #{(short)msg.msg_id.GetHashCode():X4}"); + WTelegram.Helpers.Log(1, $" Sending → {msg.body.GetType().Name.TrimEnd('_'),-40} #{(short)msg.msg_id.GetHashCode():X4}"); else - WTelegram.Helpers.Log(1, $" Sending → {msg.body.GetType().Name,-40}"); + WTelegram.Helpers.Log(1, $" Sending → {msg.body.GetType().Name.TrimEnd('_'),-40}"); writer.BaseStream.Position = patchPos; writer.Write((int)(writer.BaseStream.Length - patchPos - 4)); // patch bytes field writer.Seek(0, SeekOrigin.End);