diff --git a/.github/dev.yml b/.github/dev.yml index 18d39e0..c69b98d 100644 --- a/.github/dev.yml +++ b/.github/dev.yml @@ -2,7 +2,7 @@ pr: none trigger: - master -name: 1.9.4-dev.$(Rev:r) +name: 1.9.5-dev.$(Rev:r) pool: vmImage: ubuntu-latest diff --git a/src/Client.cs b/src/Client.cs index 0e8c3bf..59056d7 100644 --- a/src/Client.cs +++ b/src/Client.cs @@ -1297,8 +1297,7 @@ namespace WTelegram /// 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 + mimeType ??= Path.GetExtension(mediaFile.Name)?.ToLowerInvariant() switch { ".jpg" or ".jpeg" or ".png" or ".bmp" => "photo", ".gif" => "image/gif", @@ -1309,13 +1308,8 @@ namespace WTelegram _ => "", // 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); - var attributes = filename == null ? Array.Empty() : new[] { new DocumentAttributeFilename { file_name = filename } }; - return SendMessageAsync(peer, caption, new InputMediaUploadedDocument - { - file = mediaFile, mime_type = mimeType, attributes = attributes - }, reply_to_msg_id, entities, schedule_date); + 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 @@ -1362,6 +1356,87 @@ namespace WTelegram 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 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, InputMedia[] medias, string caption = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default) + { + System.Net.Http.HttpClient httpClient = null; + var multiMedia = new InputSingleMedia[medias.Length]; + for (int i = 0; i < medias.Length; i++) + { + var ism = multiMedia[i] = new InputSingleMedia { random_id = Helpers.RandomLong(), media = medias[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.StreamWithLength { length = length, innerStream = stream }, 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); + OnUpdate(updates); + int msgId = -1; + foreach (var update in updates.UpdateList) + { + switch (update) + { + case UpdateMessageID updMsgId when updMsgId.random_id == lastMedia.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; + } + private Peer InputToPeer(InputPeer peer) => peer switch { InputPeerSelf => new PeerUser { user_id = _session.UserId }, diff --git a/src/Helpers.cs b/src/Helpers.cs index cc0056a..71a044c 100644 --- a/src/Helpers.cs +++ b/src/Helpers.cs @@ -252,5 +252,21 @@ namespace WTelegram 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00 }; + + internal class StreamWithLength : Stream + { + public Stream innerStream; + public long length; + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => length; + public override long Position { get => innerStream.Position; set => throw new NotSupportedException(); } + public override void Flush() { } + public override int Read(byte[] buffer, int offset, int count) => innerStream.Read(buffer, offset, count); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } } } diff --git a/src/TL.Helpers.cs b/src/TL.Helpers.cs index a9d6930..c74cee4 100644 --- a/src/TL.Helpers.cs +++ b/src/TL.Helpers.cs @@ -36,6 +36,11 @@ namespace TL public override InputSecureFileBase ToInputSecureFile(byte[] file_hash, byte[] secret) => new InputSecureFileUploaded { id = id, parts = parts, file_hash = file_hash, secret = secret }; } + partial class InputPhoto + { + public static implicit operator InputMediaPhoto(InputPhoto photo) => new() { id = photo }; + } + partial class Peer { public abstract long ID { get; } @@ -174,6 +179,7 @@ namespace TL public abstract long ID { get; } protected abstract InputPhoto ToInputPhoto(); public static implicit operator InputPhoto(PhotoBase photo) => photo.ToInputPhoto(); + public static implicit operator InputMediaPhoto(PhotoBase photo) => photo.ToInputPhoto(); } partial class PhotoEmpty { @@ -247,7 +253,18 @@ namespace TL } } - partial class Contacts_Blocked { public IPeerInfo UserOrChat(PeerBlocked peer) => peer.peer_id.UserOrChat(users, chats); } + public partial class InputMediaUploadedDocument + { + public InputMediaUploadedDocument() { } + public InputMediaUploadedDocument(InputFileBase inputFile, string mimeType) + { + file = inputFile; + mime_type = mimeType; + if (inputFile.Name is string filename) attributes = new[] { new DocumentAttributeFilename { file_name = filename } }; + } + } + + partial class Contacts_Blocked { public IPeerInfo UserOrChat(PeerBlocked peer) => peer.peer_id.UserOrChat(users, chats); } partial class Messages_DialogsBase { public IPeerInfo UserOrChat(DialogBase dialog) => UserOrChat(dialog.Peer); public abstract int TotalCount { get; } } partial class Messages_Dialogs { public override int TotalCount => dialogs.Length; } @@ -318,11 +335,17 @@ namespace TL public InputEncryptedFileLocation ToFileLocation() => new() { id = id, access_hash = access_hash }; } + partial class InputDocument + { + public static implicit operator InputMediaDocument(InputDocument document) => new() { id = document }; + } + partial class DocumentBase { public abstract long ID { get; } protected abstract InputDocument ToInputDocument(); public static implicit operator InputDocument(DocumentBase document) => document.ToInputDocument(); + public static implicit operator InputMediaDocument(DocumentBase document) => document.ToInputDocument(); } partial class DocumentEmpty { diff --git a/src/TL.Schema.cs b/src/TL.Schema.cs index d5bc671..09e026d 100644 --- a/src/TL.Schema.cs +++ b/src/TL.Schema.cs @@ -245,7 +245,7 @@ namespace TL } /// New document See [TLDef(0x5B38C6C1)] - public class InputMediaUploadedDocument : InputMedia + public partial class InputMediaUploadedDocument : InputMedia { /// Flags, see TL conditional fields public Flags flags; @@ -502,7 +502,7 @@ namespace TL /// Defines a photo for further interaction. See /// a null value means inputPhotoEmpty [TLDef(0x3BB3B94A)] - public class InputPhoto : IObject + public partial class InputPhoto : IObject { /// Photo identifier public long id; @@ -4952,7 +4952,7 @@ namespace TL /// Defines a video for subsequent interaction. See /// a null value means inputDocumentEmpty [TLDef(0x1ABFB575)] - public class InputDocument : IObject + public partial class InputDocument : IObject { /// Document ID public long id;