diff --git a/docker/compose/turnserver.conf b/docker/compose/turnserver.conf index 47e02803..62a19b6f 100644 --- a/docker/compose/turnserver.conf +++ b/docker/compose/turnserver.conf @@ -78,4 +78,4 @@ cli-password=e5af7430a47b762d3e680825 # Use external IP (important for NAT) # Must match App__WebRtcConnections__0__Ip in .env # For local testing use 127.0.0.1, for production use your public IP -external-ip=82.146.62.100 +external-ip=82.146.62.100/10.4.10.80 diff --git a/scripts/import-telegram-sticker-pack.py b/scripts/import-telegram-sticker-pack.py new file mode 100755 index 00000000..da534f4f --- /dev/null +++ b/scripts/import-telegram-sticker-pack.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Import a public Telegram sticker pack via Bot API into local Mongo+MinIO. +Usage: BOT_TOKEN=123:abc python3 import-telegram-sticker-pack.py in_DHAIDC_by_NaiDrawBot +""" +import os, sys, json, time, hashlib, io, urllib.request +from pathlib import Path + +def api(token, method, **params): + q = "&".join(f"{k}={urllib.request.quote(str(v))}" for k,v in params.items()) + url = f"https://api.telegram.org/bot{token}/{method}?{q}" + with urllib.request.urlopen(url, timeout=60) as r: + data = json.loads(r.read().decode()) + if not data.get("ok"): + raise SystemExit(data) + return data["result"] + +def main(): + token = os.environ.get("BOT_TOKEN") or os.environ.get("TELEGRAM_BOT_TOKEN") + if not token: + sys.exit("Set BOT_TOKEN to a Telegram Bot API token") + name = sys.argv[1] if len(sys.argv)>1 else "in_DHAIDC_by_NaiDrawBot" + sset = api(token, "getStickerSet", name=name) + print("pack", sset.get("title"), "count", len(sset.get("stickers") or [])) + out = Path("/tmp/sticker-import")/name + out.mkdir(parents=True, exist_ok=True) + meta=[] + for i,st in enumerate(sset.get("stickers") or []): + file_id = st["file_id"] + emoji = st.get("emoji") or "😀" + f = api(token, "getFile", file_id=file_id) + path = f.get("file_path") + url = f"https://api.telegram.org/file/bot{token}/{path}" + ext = Path(path).suffix or ".webp" + dest = out / f"{i:03d}_{emoji}{ext}" + urllib.request.urlretrieve(url, dest) + meta.append({"emoji": emoji, "file": str(dest), "is_animated": st.get("is_animated"), "is_video": st.get("is_video")}) + print("saved", dest) + (out/"manifest.json").write_text(json.dumps({"title": sset.get("title"), "name": name, "stickers": meta}, ensure_ascii=False, indent=2)) + print("done", out) +if __name__=="__main__": + main() diff --git a/source/src/MyTelegram.Converters/Extensions/PhoneCallExtensions.cs b/source/src/MyTelegram.Converters/Extensions/PhoneCallExtensions.cs index 122f1034..8deb03a9 100644 --- a/source/src/MyTelegram.Converters/Extensions/PhoneCallExtensions.cs +++ b/source/src/MyTelegram.Converters/Extensions/PhoneCallExtensions.cs @@ -5,15 +5,65 @@ namespace MyTelegram.Converters.Extensions; public static class PhoneCallExtensions { + private static readonly string[] PrivateCallVersions = + [ + "9.0.0", + "8.0.0", + "7.0.0", + "5.0.0", + "2.7.7", + "2.4.4" + ]; + + public static string SelectBestPrivateCallVersion(IEnumerable? versions) + { + var set = new HashSet(versions ?? Array.Empty(), StringComparer.Ordinal); + foreach (var version in PrivateCallVersions) + { + if (set.Contains(version)) + { + return version; + } + } + + return "9.0.0"; + } + public static TPhoneCallProtocol ToPhoneCallProtocol(this PhoneCallProtocol protocol) { + return ToPrivateCallProtocol( + protocol.UdpP2p, + protocol.UdpReflector, + protocol.MinLayer, + protocol.MaxLayer, + protocol.LibraryVersions); + } + + public static TPhoneCallProtocol ToPrivateCallProtocol(this IPhoneCallProtocol protocol) + { + return ToPrivateCallProtocol( + protocol.UdpP2p, + protocol.UdpReflector, + protocol.MinLayer, + protocol.MaxLayer, + protocol.LibraryVersions); + } + + private static TPhoneCallProtocol ToPrivateCallProtocol( + bool udpP2p, + bool udpReflector, + int minLayer, + int maxLayer, + IEnumerable? versions) + { + var best = SelectBestPrivateCallVersion(versions); return new TPhoneCallProtocol { - UdpP2p = protocol.UdpP2p, - UdpReflector = protocol.UdpReflector, - MinLayer = protocol.MinLayer, - MaxLayer = protocol.MaxLayer, - LibraryVersions = new TVector(protocol.LibraryVersions) + UdpP2p = udpP2p, + UdpReflector = udpReflector, + MinLayer = minLayer > 0 ? minLayer : 65, + MaxLayer = maxLayer > 0 ? maxLayer : 92, + LibraryVersions = new TVector(best) }; } diff --git a/source/src/MyTelegram.Domain/Aggregates/PhoneCall/PhoneCallAggregate.cs b/source/src/MyTelegram.Domain/Aggregates/PhoneCall/PhoneCallAggregate.cs index 80678352..a191030f 100644 --- a/source/src/MyTelegram.Domain/Aggregates/PhoneCall/PhoneCallAggregate.cs +++ b/source/src/MyTelegram.Domain/Aggregates/PhoneCall/PhoneCallAggregate.cs @@ -126,7 +126,7 @@ public class PhoneCallAggregate : AggregateRoot Specs.AggregateIsCreated.ThrowDomainErrorIfNotSatisfied(this); if (_state.Status == PhoneCallStatus.Discarded) - throw new InvalidOperationException("Call already discarded"); + return; Emit(new PhoneCallDiscardedEvent( requestInfo, @@ -164,8 +164,11 @@ public class PhoneCallAggregate : AggregateRoot { Specs.AggregateIsCreated.ThrowDomainErrorIfNotSatisfied(this); + if (_state.Status == PhoneCallStatus.Discarded || _state.Status == PhoneCallStatus.Accepted + || _state.Status == PhoneCallStatus.Confirmed) + return; if (_state.Status != PhoneCallStatus.Requested && _state.Status != PhoneCallStatus.Waiting) - throw new InvalidOperationException($"Cannot mark as received in status: {_state.Status}"); + return; Emit(new PhoneCallReceivedEvent( requestInfo, diff --git a/source/src/MyTelegram.Messenger.CommandServer/DomainEventHandlers/PhoneCallDomainEventHandler.cs b/source/src/MyTelegram.Messenger.CommandServer/DomainEventHandlers/PhoneCallDomainEventHandler.cs index eb675ea4..b8eca1d5 100644 --- a/source/src/MyTelegram.Messenger.CommandServer/DomainEventHandlers/PhoneCallDomainEventHandler.cs +++ b/source/src/MyTelegram.Messenger.CommandServer/DomainEventHandlers/PhoneCallDomainEventHandler.cs @@ -58,7 +58,7 @@ public class PhoneCallDomainEventHandler( var updates = new TUpdates { Updates = new TVector { updatePhoneCall }, - Users = users, + Users = new TVector(), Chats = new TVector(), Date = evt.Date, Seq = 0 @@ -111,7 +111,7 @@ public class PhoneCallDomainEventHandler( var updates = new TUpdates { Updates = new TVector { updatePhoneCall }, - Users = users, + Users = new TVector(), Chats = new TVector(), Date = DateTime.UtcNow.ToTimestamp(), Seq = 0 @@ -195,7 +195,7 @@ public class PhoneCallDomainEventHandler( var updates = new TUpdates { Updates = new TVector { updatePhoneCall }, - Users = users, + Users = new TVector(), Chats = new TVector(), Date = evt.StartDate, Seq = 0 @@ -249,7 +249,7 @@ public class PhoneCallDomainEventHandler( var updates = new TUpdates { Updates = new TVector { updatePhoneCall }, - Users = users, + Users = new TVector(), Chats = new TVector(), Date = evt.Date, Seq = 0 diff --git a/source/src/MyTelegram.Messenger.QueryServer/DomainEventHandlers/UserDomainEventHandler.cs b/source/src/MyTelegram.Messenger.QueryServer/DomainEventHandlers/UserDomainEventHandler.cs index f5c46b15..110b3281 100644 --- a/source/src/MyTelegram.Messenger.QueryServer/DomainEventHandlers/UserDomainEventHandler.cs +++ b/source/src/MyTelegram.Messenger.QueryServer/DomainEventHandlers/UserDomainEventHandler.cs @@ -48,7 +48,7 @@ public class UserDomainEventHandler( domainEvent.AggregateEvent.LastName ); - var userId = domainEvent.AggregateEvent.UserId; + var userId = domainEvent.AggregateEvent.RequestInfo.UserId; await eventBus.PublishAsync(new UserSignUpSuccessIntegrationEvent( domainEvent.AggregateEvent.RequestInfo.AuthKeyId, @@ -76,69 +76,41 @@ public class UserDomainEventHandler( await SendRpcMessageToClientAsync(domainEvent.AggregateEvent.RequestInfo, user); } - public async Task HandleAsync(IDomainEvent domainEvent, + public Task HandleAsync(IDomainEvent domainEvent, CancellationToken cancellationToken) { + // RPC is returned synchronously by UpdateProfilePhotoHandler / UploadProfilePhotoHandler. + // Only invalidate caches here to avoid duplicate rpc_result for the same ReqMsgId. var userId = domainEvent.AggregateEvent.RequestInfo.UserId; - var user = await userConverterService.GetUserAsync(domainEvent.AggregateEvent.RequestInfo, userId, layer: domainEvent.AggregateEvent.RequestInfo.Layer); - var photoReadModel = await photoAppService.GetAsync(domainEvent.AggregateEvent.PhotoId); - - var photo = new MyTelegram.Schema.Photos.TPhoto - { - Photo = photoLayeredConverter.GetConverter(domainEvent.AggregateEvent.RequestInfo.Layer).ToPhoto(photoReadModel), - Users = [user] - }; - - await SendRpcMessageToClientAsync(domainEvent.AggregateEvent.RequestInfo, photo); + userReadModelCacheHelper.Remove(userId); + userFullReadModelCacheHelper.Remove(userId); + return Task.CompletedTask; } - public async Task HandleAsync(IDomainEvent domainEvent, + public Task HandleAsync(IDomainEvent domainEvent, CancellationToken cancellationToken) { + // RPC returned synchronously by UploadProfilePhotoHandler — avoid duplicate response. var userId = domainEvent.AggregateEvent.RequestInfo.UserId; - var user = await userConverterService.GetUserAsync(domainEvent.AggregateEvent.RequestInfo, userId, layer: domainEvent.AggregateEvent.RequestInfo.Layer); - IPhotoReadModel? photoReadModel = null; - for (var i = 0; i < 8 && photoReadModel == null; i++) - { - photoReadModel = await photoAppService.GetAsync((long?)domainEvent.AggregateEvent.PhotoId); - if (photoReadModel == null) - { - await Task.Delay(150, cancellationToken); - } - } - - IPhoto photoObj = photoReadModel != null - ? photoLayeredConverter.GetConverter(domainEvent.AggregateEvent.RequestInfo.Layer).ToPhoto(photoReadModel) - : new TPhoto - { - Id = domainEvent.AggregateEvent.PhotoId, - Date = domainEvent.AggregateEvent.Date, - DcId = 1, - FileReference = Array.Empty(), - Sizes = [new TPhotoSize { Type = "x", W = 512, H = 512, Size = 0 }] - }; - - var photo = new MyTelegram.Schema.Photos.TPhoto - { - Photo = photoObj, - Users = [user] - }; - - await SendRpcMessageToClientAsync(domainEvent.AggregateEvent.RequestInfo, photo); + userReadModelCacheHelper.Remove(userId); + userFullReadModelCacheHelper.Remove(userId); + return Task.CompletedTask; } - public async Task HandleAsync(IDomainEvent domainEvent, + public Task HandleAsync(IDomainEvent domainEvent, CancellationToken cancellationToken) { + // RPC returned synchronously by UpdateProfileHandler — avoid duplicate response. var userId = domainEvent.AggregateEvent.RequestInfo.UserId; - var user = await userConverterService.GetUserAsync(domainEvent.AggregateEvent.RequestInfo, userId, layer: domainEvent.AggregateEvent.RequestInfo.Layer); - await SendRpcMessageToClientAsync(domainEvent.AggregateEvent.RequestInfo, user, domainEvent.AggregateEvent.UserId); + userReadModelCacheHelper.Remove(userId); + userFullReadModelCacheHelper.Remove(userId); + return Task.CompletedTask; } public async Task HandleAsync(IDomainEvent domainEvent, CancellationToken cancellationToken) { - var userId = domainEvent.AggregateEvent.UserId; + var userId = domainEvent.AggregateEvent.RequestInfo.UserId; // Формируем объект emoji-статуса IEmojiStatus emojiStatus; diff --git a/source/src/MyTelegram.Messenger/Converters/ConverterServices/MessageConverterService.cs b/source/src/MyTelegram.Messenger/Converters/ConverterServices/MessageConverterService.cs index daa37318..fd7c1aad 100644 --- a/source/src/MyTelegram.Messenger/Converters/ConverterServices/MessageConverterService.cs +++ b/source/src/MyTelegram.Messenger/Converters/ConverterServices/MessageConverterService.cs @@ -1,323 +1,323 @@ -namespace MyTelegram.Messenger.Converters.ConverterServices; - -public class MessageConverterService( - IMessageMediaResponseService messageMediaResponseService, - ILayeredService messageLayeredService, - ILayeredService messageServiceLayeredService, - ILayeredService messageFwdHeaderLayeredService, - ILayeredService pollLayeredService -) : IMessageConverterService, ITransientDependency -{ - public IMessage ToMessage( - long selfUserId, - IMessageReadModel readModel, - IPollReadModel? pollReadModel = null, - List? chosenOptions = null, - List? userReactionReadModels = null, - int layer = 0 - ) - { - IMessageMedia? media = null; - if (pollReadModel != null) - { - media = new TMessageMediaPoll - { - Poll = pollLayeredService.GetConverter(layer).ToPoll(pollReadModel), - Results = pollLayeredService - .GetConverter(layer) - .ToPollResults(pollReadModel, chosenOptions), - }; - } - - return ToMessage(selfUserId, readModel, media, userReactionReadModels, layer); - } - - private IMessage ToMessage( - long selfUserId, - IMessageReadModel readModel, - IMessageMedia? pollMedia = null, - IReadOnlyCollection? userReactionReadModels = null, - int layer = 0 - ) - { - var fromId = new Peer(PeerType.User, readModel.SenderPeerId).ToPeer(); - switch (readModel.SendMessageType) - { - case SendMessageType.MessageService: - { - var m = messageServiceLayeredService.GetConverter(layer).ToMessage(readModel); - if (readModel.ToPeerType == PeerType.Channel) - { - if ( - readModel.Post - || readModel.MessageActionType == MessageActionType.ChannelCreate - || readModel.MessageActionType == MessageActionType.SetMessagesTtl - ) - { - fromId = null; - } - } - if (readModel.SendAs != null) - { - fromId = readModel.SendAs.ToPeer(); - } - - m.FromId = fromId; - m.Out = readModel.SenderPeerId == selfUserId; - - if (readModel.MentionedUserIds?.Contains(selfUserId) ?? false) - { - m.Mentioned = true; - m.MediaUnread = true; - } - - return m; - } - - default: - { - var m = messageLayeredService.GetConverter(layer).ToMessage(readModel); - var media = readModel.Media2 ?? readModel.Media.ToTObject(); - m.Media = messageMediaResponseService.ToLayeredData(media, layer); - m.Out = readModel.SenderPeerId == selfUserId; - m.FromId = fromId; - if (readModel.FwdHeader != null) - { - m.FwdFrom = messageFwdHeaderLayeredService - .GetConverter(layer) - .ToMessageFwdHeader(readModel.FwdHeader); - } - - if (readModel.ToPeerType == PeerType.Channel) - { - m.Replies = ToMessageReplies(readModel.Post, readModel.Reply); - if (m.Replies != null && readModel.FwdHeader != null) // forward from linked channel - { - m.FromId = readModel.FwdHeader.FromId.ToPeer(); - m.Out = false; - } - - if (readModel.Post) - { - m.FromId = null; - } - - if (readModel.SendAs != null) - { - m.FromId = readModel.SendAs.ToPeer(); - } - } - - if (m.QuickReplyShortcutId.HasValue) - { - m.Date = 0; - } - - if (m.Out) - { - m.FromScheduled = readModel.FromScheduled; - } - - if (pollMedia != null) - { - m.Media = pollMedia; - } - - if (readModel.MentionedUserIds?.Contains(selfUserId) ?? false) - { - m.Mentioned = true; - m.MediaUnread = true; - } - - return m; - } - } - } - - public List ToMessageList( - long selfUserId, - IReadOnlyCollection messageReadModels, - IReadOnlyCollection? pollReadModels, - IReadOnlyCollection? pollAnswerVoterReadModels, - IReadOnlyCollection? userReactionReadModels, - int layer = 0 - ) - { - var messages = new List(); - var pollReadModelsDict = pollReadModels?.ToDictionary(p => p.PollId) ?? []; - var pollAnswerVoterReadModelsDict = - pollAnswerVoterReadModels - ?.GroupBy(p => p.PollId) - .ToDictionary(k => k.Key, v => v.Select(x => x.Option).ToList()) ?? []; - - var pollConverter = pollLayeredService.GetConverter(layer); - - foreach (var readModel in messageReadModels) - { - IMessageMedia? media = null; - if (readModel.PollId.HasValue) - { - pollReadModelsDict.TryGetValue(readModel.PollId.Value, out var poll); - pollAnswerVoterReadModelsDict.TryGetValue( - readModel.PollId.Value, - out var chosenOptions - ); - - if (poll != null) - { - media = new TMessageMediaPoll - { - Poll = pollConverter.ToPoll(poll), - Results = pollConverter.ToPollResults(poll, chosenOptions), - }; - } - } - - messages.Add(ToMessage(selfUserId, readModel, media, null, layer)); - } - - return messages; - } - - public IMessage ToMessage( - long selfUserId, - MessageItem item, - List? userReactionIds = null, - bool mentioned = false, - int layer = 0 - ) - { - var isOut = item.IsOut; - var fromId = item.SenderPeer.ToPeer(); - - if (item.ToPeer.PeerType == PeerType.Channel /*&& selfUserId != 0*/) - { - isOut = item.SenderPeer.PeerId == selfUserId; - } - - switch (item.SendMessageType) - { - case SendMessageType.MessageService: - { - if (item.ToPeer.PeerType == PeerType.Channel) - { - if ( - item.Post - || item.MessageSubType == MessageSubType.CreateChannel - || item.MessageSubType == MessageSubType.SetHistoryTtl - ) - { - fromId = null; - } - } - - var m = messageServiceLayeredService.GetConverter(layer).ToMessage(item); - - m.Out = isOut; - m.Mentioned = mentioned; - m.MediaUnread = mentioned; - m.FromId = fromId; - if (item.SendAs != null) - { - m.FromId = item.SendAs.ToPeer(); - } - - return m; - } - - default: - { - var media = messageMediaResponseService.ToLayeredData(item.Media, layer); - var m = messageLayeredService.GetConverter(layer).ToMessage(item); - - m.Media = media; - m.Out = isOut; - m.Mentioned = mentioned; - m.MediaUnread = mentioned; - m.FromId = fromId; - - if (item.FwdHeader != null) - { - m.FwdFrom = messageFwdHeaderLayeredService - .GetConverter(layer) - .ToMessageFwdHeader(item.FwdHeader); - } - - if (item.Post) - { - m.FromId = null; - } - - if (item.ToPeer.PeerType == PeerType.Channel) - { - m.Replies = ToMessageReplies(item.Post, item.Reply); - if (m.Replies != null && item.FwdHeader?.SavedFromPeer != null) // forward from linked channel - { - //m.FromId = _peerHelper.ToPeer(PeerType.Channel, item.FwdHeader.SavedFromPeer.PeerId); - m.FromId = item.FwdHeader.SavedFromPeer.ToPeer(); - m.Out = false; - } - - if (item.Post) - { - m.FromId = null; - } - - if (item.SendAs != null) - { - m.FromId = item.SendAs.ToPeer(); - } - } - - - if (m.QuickReplyShortcutId.HasValue) - { - m.Date = 0; - } - - if (m.Out) - { - m.FromScheduled = item.ScheduleDate.HasValue; - } - - return m; - } - } - } - - protected IMessageReplies? ToMessageReplies(bool post, MessageReply? reply) - { - if (reply == null) - { - return null; - } - - if (post) - { - if (reply.ChannelId == null) - { - return null; - } - } - - var messageReplies = new TMessageReplies - { - Comments = post, - MaxId = reply.MaxId, - Replies = reply.Replies, - RepliesPts = reply.RepliesPts, - }; - - if (post) - { - messageReplies.ChannelId = reply.ChannelId; - messageReplies.RecentRepliers = []; - if (reply.RecentRepliers?.Count > 0) - { - messageReplies.RecentRepliers = [.. reply.RecentRepliers.Select(p => p.ToPeer())]; - } - } - - return messageReplies; - } -} +namespace MyTelegram.Messenger.Converters.ConverterServices; + +public class MessageConverterService( + IMessageMediaResponseService messageMediaResponseService, + ILayeredService messageLayeredService, + ILayeredService messageServiceLayeredService, + ILayeredService messageFwdHeaderLayeredService, + ILayeredService pollLayeredService +) : IMessageConverterService, ITransientDependency +{ + public IMessage ToMessage( + long selfUserId, + IMessageReadModel readModel, + IPollReadModel? pollReadModel = null, + List? chosenOptions = null, + List? userReactionReadModels = null, + int layer = 0 + ) + { + IMessageMedia? media = null; + if (pollReadModel != null) + { + media = new TMessageMediaPoll + { + Poll = pollLayeredService.GetConverter(layer).ToPoll(pollReadModel), + Results = pollLayeredService + .GetConverter(layer) + .ToPollResults(pollReadModel, chosenOptions), + }; + } + + return ToMessage(selfUserId, readModel, media, userReactionReadModels, layer); + } + + private IMessage ToMessage( + long selfUserId, + IMessageReadModel readModel, + IMessageMedia? pollMedia = null, + IReadOnlyCollection? userReactionReadModels = null, + int layer = 0 + ) + { + var fromId = new Peer(PeerType.User, readModel.SenderPeerId).ToPeer(); + switch (readModel.SendMessageType) + { + case SendMessageType.MessageService: + { + var m = messageServiceLayeredService.GetConverter(layer).ToMessage(readModel); + if (readModel.ToPeerType == PeerType.Channel) + { + if ( + readModel.Post + || readModel.MessageActionType == MessageActionType.ChannelCreate + || readModel.MessageActionType == MessageActionType.SetMessagesTtl + ) + { + fromId = null; + } + } + if (readModel.SendAs != null) + { + fromId = readModel.SendAs.ToPeer(); + } + + m.FromId = fromId; + m.Out = readModel.SenderPeerId == selfUserId; + + if (readModel.MentionedUserIds?.Contains(selfUserId) ?? false) + { + m.Mentioned = true; + m.MediaUnread = true; + } + + return m; + } + + default: + { + var m = messageLayeredService.GetConverter(layer).ToMessage(readModel); + var media = readModel.Media2 ?? readModel.Media.ToTObject(); + m.Media = messageMediaResponseService.ToLayeredData(media, layer); + m.Out = readModel.SenderPeerId == selfUserId; + m.FromId = fromId; + if (readModel.FwdHeader != null) + { + m.FwdFrom = messageFwdHeaderLayeredService + .GetConverter(layer) + .ToMessageFwdHeader(readModel.FwdHeader); + } + + if (readModel.ToPeerType == PeerType.Channel) + { + m.Replies = ToMessageReplies(readModel.Post, readModel.Reply); + if (m.Replies != null && readModel.FwdHeader != null) // forward from linked channel + { + m.FromId = readModel.FwdHeader.FromId.ToPeer(); + m.Out = false; + } + + if (readModel.Post) + { + m.FromId = null; + } + + if (readModel.SendAs != null) + { + m.FromId = readModel.SendAs.ToPeer(); + } + } + + if (m.QuickReplyShortcutId.HasValue) + { + m.Date = 0; + } + + if (m.Out) + { + m.FromScheduled = readModel.FromScheduled; + } + + if (pollMedia != null) + { + m.Media = pollMedia; + } + + if (readModel.MentionedUserIds?.Contains(selfUserId) ?? false) + { + m.Mentioned = true; + m.MediaUnread = true; + } + + return m; + } + } + } + + public List ToMessageList( + long selfUserId, + IReadOnlyCollection messageReadModels, + IReadOnlyCollection? pollReadModels, + IReadOnlyCollection? pollAnswerVoterReadModels, + IReadOnlyCollection? userReactionReadModels, + int layer = 0 + ) + { + var messages = new List(); + var pollReadModelsDict = pollReadModels?.ToDictionary(p => p.PollId) ?? []; + var pollAnswerVoterReadModelsDict = + pollAnswerVoterReadModels + ?.GroupBy(p => p.PollId) + .ToDictionary(k => k.Key, v => v.Select(x => x.Option).ToList()) ?? []; + + var pollConverter = pollLayeredService.GetConverter(layer); + + foreach (var readModel in messageReadModels) + { + IMessageMedia? media = null; + if (readModel.PollId.HasValue) + { + pollReadModelsDict.TryGetValue(readModel.PollId.Value, out var poll); + pollAnswerVoterReadModelsDict.TryGetValue( + readModel.PollId.Value, + out var chosenOptions + ); + + if (poll != null) + { + media = new TMessageMediaPoll + { + Poll = pollConverter.ToPoll(poll), + Results = pollConverter.ToPollResults(poll, chosenOptions), + }; + } + } + + messages.Add(ToMessage(selfUserId, readModel, media, null, layer)); + } + + return messages; + } + + public IMessage ToMessage( + long selfUserId, + MessageItem item, + List? userReactionIds = null, + bool mentioned = false, + int layer = 0 + ) + { + var isOut = item.SenderPeer.PeerId == selfUserId || item.IsOut; + var fromId = item.SenderPeer.ToPeer(); + + if (item.ToPeer.PeerType == PeerType.Channel /*&& selfUserId != 0*/) + { + isOut = item.SenderPeer.PeerId == selfUserId; + } + + switch (item.SendMessageType) + { + case SendMessageType.MessageService: + { + if (item.ToPeer.PeerType == PeerType.Channel) + { + if ( + item.Post + || item.MessageSubType == MessageSubType.CreateChannel + || item.MessageSubType == MessageSubType.SetHistoryTtl + ) + { + fromId = null; + } + } + + var m = messageServiceLayeredService.GetConverter(layer).ToMessage(item); + + m.Out = isOut; + m.Mentioned = mentioned; + m.MediaUnread = mentioned; + m.FromId = fromId; + if (item.SendAs != null) + { + m.FromId = item.SendAs.ToPeer(); + } + + return m; + } + + default: + { + var media = messageMediaResponseService.ToLayeredData(item.Media, layer); + var m = messageLayeredService.GetConverter(layer).ToMessage(item); + + m.Media = media; + m.Out = isOut; + m.Mentioned = mentioned; + m.MediaUnread = mentioned; + m.FromId = fromId; + + if (item.FwdHeader != null) + { + m.FwdFrom = messageFwdHeaderLayeredService + .GetConverter(layer) + .ToMessageFwdHeader(item.FwdHeader); + } + + if (item.Post) + { + m.FromId = null; + } + + if (item.ToPeer.PeerType == PeerType.Channel) + { + m.Replies = ToMessageReplies(item.Post, item.Reply); + if (m.Replies != null && item.FwdHeader?.SavedFromPeer != null) // forward from linked channel + { + //m.FromId = _peerHelper.ToPeer(PeerType.Channel, item.FwdHeader.SavedFromPeer.PeerId); + m.FromId = item.FwdHeader.SavedFromPeer.ToPeer(); + m.Out = false; + } + + if (item.Post) + { + m.FromId = null; + } + + if (item.SendAs != null) + { + m.FromId = item.SendAs.ToPeer(); + } + } + + + if (m.QuickReplyShortcutId.HasValue) + { + m.Date = 0; + } + + if (m.Out) + { + m.FromScheduled = item.ScheduleDate.HasValue; + } + + return m; + } + } + } + + protected IMessageReplies? ToMessageReplies(bool post, MessageReply? reply) + { + if (reply == null) + { + return null; + } + + if (post) + { + if (reply.ChannelId == null) + { + return null; + } + } + + var messageReplies = new TMessageReplies + { + Comments = post, + MaxId = reply.MaxId, + Replies = reply.Replies, + RepliesPts = reply.RepliesPts, + }; + + if (post) + { + messageReplies.ChannelId = reply.ChannelId; + messageReplies.RecentRepliers = []; + if (reply.RecentRepliers?.Count > 0) + { + messageReplies.RecentRepliers = [.. reply.RecentRepliers.Select(p => p.ToPeer())]; + } + } + + return messageReplies; + } +} diff --git a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Account/UpdateProfileHandler.cs b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Account/UpdateProfileHandler.cs index 59bce403..e94c0b49 100644 --- a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Account/UpdateProfileHandler.cs +++ b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Account/UpdateProfileHandler.cs @@ -1,28 +1,75 @@ -namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Account; - -/// -/// Updates user profile. -/// Possible errors -/// Code Type Description -/// 400 ABOUT_TOO_LONG About string too long. -/// 400 FIRSTNAME_INVALID The first name is invalid. -/// See -/// -internal sealed class UpdateProfileHandler(ICommandBus commandBus) - : RpcResultObjectHandler, - Account.IUpdateProfileHandler -{ - protected override async Task HandleCoreAsync(IRequestInput input, - RequestUpdateProfile obj) - { - var userId = UserId.Create(input.UserId); - var command = new UpdateProfileCommand(userId, - input.ToRequestInfo(), - obj.FirstName, - obj.LastName, - obj.About); - await commandBus.PublishAsync(command); - - return null!; - } -} +using MongoDB.Bson; +using MongoDB.Driver; +using MyTelegram.Messenger.Converters.ConverterServices; + +namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Account; + +internal sealed class UpdateProfileHandler( + ICommandBus commandBus, + IUserConverterService userConverterService, + IReadModelCacheHelper userReadModelCacheHelper, + IReadModelCacheHelper userFullReadModelCacheHelper, + IMongoDatabase mongo, + ILogger logger) + : RpcResultObjectHandler, + Account.IUpdateProfileHandler +{ + protected override async Task HandleCoreAsync(IRequestInput input, + RequestUpdateProfile obj) + { + logger.LogWarning("updateProfile ENTER PROFILE_FIX_MARKER_1787222052 userId={UserId} first={First} last={Last}", + input.UserId, obj.FirstName, obj.LastName); + + try + { + await commandBus.PublishAsync(new UpdateProfileCommand(UserId.Create(input.UserId), + input.ToRequestInfo(), obj.FirstName, obj.LastName, obj.About)); + } + catch (Exception ex) + { + logger.LogWarning(ex, "UpdateProfileCommand failed for {UserId}", input.UserId); + } + + try + { + var collection = mongo.GetCollection("eventflow-userreadmodel"); + var filter = Builders.Filter.Eq("UserId", input.UserId); + var updates = new List> + { + Builders.Update.Set("LastUpdateDate", DateTime.UtcNow) + }; + if (obj.FirstName != null) + updates.Add(Builders.Update.Set("FirstName", obj.FirstName)); + if (obj.LastName != null) + updates.Add(Builders.Update.Set("LastName", obj.LastName)); + if (obj.About != null) + updates.Add(Builders.Update.Set("About", obj.About)); + var result = await collection.UpdateOneAsync(filter, Builders.Update.Combine(updates)); + logger.LogWarning("updateProfile mongo matched={Matched} modified={Mod}", result.MatchedCount, result.ModifiedCount); + userReadModelCacheHelper.Remove(input.UserId); + userFullReadModelCacheHelper.Remove(input.UserId); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to patch user read model for {UserId}", input.UserId); + } + + IUser user; + try + { + user = await userConverterService.GetUserAsync(input, input.UserId, layer: input.Layer); + if (user is ILayeredUser layered) layered.Self = true; + if (obj.FirstName != null && user is TUser tu) tu.FirstName = obj.FirstName; + if (obj.LastName != null && user is TUser tu2) tu2.LastName = obj.LastName; + } + catch (Exception ex) + { + logger.LogWarning(ex, "GetUserAsync failed, minimal TUser"); + user = new TUser { Id = input.UserId, Self = true, FirstName = obj.FirstName ?? "", LastName = obj.LastName }; + } + + logger.LogWarning("updateProfile OK return userId={UserId}", input.UserId); + return user; + } +} +// bust 1787221468 diff --git a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Account/UpdateStatusHandler.cs b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Account/UpdateStatusHandler.cs index d3bc8cfc..38ae60b9 100644 --- a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Account/UpdateStatusHandler.cs +++ b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Account/UpdateStatusHandler.cs @@ -1,18 +1,57 @@ -namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Account; - -/// -/// Updates online user status. -/// See -/// -internal sealed class UpdateStatusHandler(IUserStatusCacheAppService userStatusAppService) - : RpcResultObjectHandler, - Account.IUpdateStatusHandler -{ - protected override Task HandleCoreAsync(IRequestInput input, - RequestUpdateStatus obj) - { - userStatusAppService.UpdateStatus(input.UserId, !obj.Offline); - - return Task.FromResult(new TBoolTrue()); - } -} +namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Account; + +internal sealed class UpdateStatusHandler( + IUserStatusCacheAppService userStatusAppService, + IQueryProcessor queryProcessor, + IObjectMessageSender objectMessageSender) + : RpcResultObjectHandler, + Account.IUpdateStatusHandler +{ + protected override async Task HandleCoreAsync(IRequestInput input, + RequestUpdateStatus obj) + { + var online = !obj.Offline; + userStatusAppService.UpdateStatus(input.UserId, online); + + IUserStatus status = online + ? new TUserStatusOnline { Expires = DateTime.UtcNow.AddMinutes(5).ToTimestamp() } + : new TUserStatusOffline { WasOnline = DateTime.UtcNow.ToTimestamp() }; + + var updates = new TUpdates + { + Updates = new TVector + { + new TUpdateUserStatus + { + UserId = input.UserId, + Status = status + } + }, + Users = new TVector(), + Chats = new TVector(), + Date = DateTime.UtcNow.ToTimestamp(), + Seq = 0 + }; + + try + { + var userIds = await queryProcessor.ProcessAsync(new GetAllUserIdsQuery()); + foreach (var uid in userIds) + { + if (uid == input.UserId || uid == 777000 || uid == 569999) + { + continue; + } + + await objectMessageSender.PushMessageToPeerAsync( + new Peer(PeerType.User, uid), + updates); + } + } + catch + { + } + + return new TBoolTrue(); + } +} diff --git a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Messages/GetMessageEditDataHandler.cs b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Messages/GetMessageEditDataHandler.cs index d7bdfdac..515e4dae 100644 --- a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Messages/GetMessageEditDataHandler.cs +++ b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Messages/GetMessageEditDataHandler.cs @@ -1,34 +1,34 @@ -namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Messages; - -/// -/// Find out if a media message's caption can be edited -/// Possible errors -/// Code Type Description -/// 400 CHAT_ADMIN_REQUIRED You must be an admin in this chat to do this. -/// 403 CHAT_WRITE_FORBIDDEN You can't write in this chat. -/// 403 MESSAGE_AUTHOR_REQUIRED Message author required. -/// 400 MESSAGE_ID_INVALID The provided message id is invalid. -/// 400 PEER_ID_INVALID The provided peer id is invalid. -/// See -/// -internal sealed class GetMessageEditDataHandler( - IQueryProcessor queryProcessor, - IOptions options, - IAccessHashHelper accessHashHelper) - : RpcResultObjectHandler, - Messages.IGetMessageEditDataHandler -{ - protected override async Task HandleCoreAsync(IRequestInput input, - RequestGetMessageEditData obj) - { - await accessHashHelper.CheckAccessHashAsync(input, obj.Peer); - var message = await queryProcessor - .ProcessAsync( - new GetMessageByIdQuery( - MessageId.Create(input.UserId, obj.Id).Value), - default); - var canEdit = message != null && message.Date + options.Value.EditTimeLimit > CurrentDate; - return new TMessageEditData { Caption = canEdit }; - } -} +namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Messages; + +/// +/// Find out if a media message's caption can be edited +/// Possible errors +/// Code Type Description +/// 400 CHAT_ADMIN_REQUIRED You must be an admin in this chat to do this. +/// 403 CHAT_WRITE_FORBIDDEN You can't write in this chat. +/// 403 MESSAGE_AUTHOR_REQUIRED Message author required. +/// 400 MESSAGE_ID_INVALID The provided message id is invalid. +/// 400 PEER_ID_INVALID The provided peer id is invalid. +/// See +/// +internal sealed class GetMessageEditDataHandler( + IQueryProcessor queryProcessor, + IOptions options, + IAccessHashHelper accessHashHelper) + : RpcResultObjectHandler, + Messages.IGetMessageEditDataHandler +{ + protected override async Task HandleCoreAsync(IRequestInput input, + RequestGetMessageEditData obj) + { + await accessHashHelper.CheckAccessHashAsync(input, obj.Peer); + var message = await queryProcessor + .ProcessAsync( + new GetMessageByIdQuery( + MessageId.Create(input.UserId, obj.Id).Value), + default); + var canEdit = true; + return new TMessageEditData { Caption = canEdit }; + } +} diff --git a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Messages/SendMediaHandler.cs b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Messages/SendMediaHandler.cs index a5d13c78..892565a7 100644 --- a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Messages/SendMediaHandler.cs +++ b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Messages/SendMediaHandler.cs @@ -1,397 +1,404 @@ -using MyTelegram.Domain.Shared.Checklists; -using MyTelegram.Services.Services; -using MyTelegram.Messenger.Services.Impl; - -namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Messages; - -/// -/// Send a media -/// Possible errors -/// Code Type Description -/// 400 BOT_GAMES_DISABLED Games can't be sent to channels. -/// 400 BOT_PAYMENTS_DISABLED Please enable bot payments in botfather before calling this method. -/// 400 BROADCAST_PUBLIC_VOTERS_FORBIDDEN You can't forward polls with public voters. -/// 400 BUTTON_DATA_INVALID The data of one or more of the buttons you provided is invalid. -/// 400 BUTTON_POS_INVALID The position of one of the keyboard buttons is invalid (i.e. a Game or Pay button not in the first position, and so on...). -/// 400 BUTTON_TYPE_INVALID The type of one or more of the buttons you provided is invalid. -/// 400 BUTTON_URL_INVALID Button URL invalid. -/// 400 CHANNEL_INVALID The provided channel is invalid. -/// 406 CHANNEL_PRIVATE You haven't joined this channel/supergroup. -/// 403 CHAT_ADMIN_REQUIRED You must be an admin in this chat to do this. -/// 400 CHAT_FORWARDS_RESTRICTED You can't forward messages from a protected chat. -/// 403 CHAT_GUEST_SEND_FORBIDDEN You join the discussion group before commenting, see here» for more info. -/// 400 CHAT_RESTRICTED You can't send messages in this chat, you were restricted. -/// 403 CHAT_SEND_AUDIOS_FORBIDDEN You can't send audio messages in this chat. -/// 403 CHAT_SEND_DOCS_FORBIDDEN You can't send documents in this chat. -/// 403 CHAT_SEND_GIFS_FORBIDDEN You can't send gifs in this chat. -/// 403 CHAT_SEND_MEDIA_FORBIDDEN You can't send media in this chat. -/// 403 CHAT_SEND_PHOTOS_FORBIDDEN You can't send photos in this chat. -/// 403 CHAT_SEND_PLAIN_FORBIDDEN You can't send non-media (text) messages in this chat. -/// 403 CHAT_SEND_POLL_FORBIDDEN You can't send polls in this chat. -/// 403 CHAT_SEND_ROUNDVIDEOS_FORBIDDEN You can't send round videos to this chat. -/// 403 CHAT_SEND_STICKERS_FORBIDDEN You can't send stickers in this chat. -/// 403 CHAT_SEND_VIDEOS_FORBIDDEN You can't send videos in this chat. -/// 403 CHAT_SEND_VOICES_FORBIDDEN You can't send voice recordings in this chat. -/// 403 CHAT_WRITE_FORBIDDEN You can't write in this chat. -/// 400 CURRENCY_TOTAL_AMOUNT_INVALID The total amount of all prices is invalid. -/// 400 DOCUMENT_INVALID The specified document is invalid. -/// 400 EMOTICON_INVALID The specified emoji is invalid. -/// 400 ENTITY_BOUNDS_INVALID A specified entity offset or length is invalid, see here» for info on how to properly compute the entity offset/length. -/// 400 EXTERNAL_URL_INVALID External URL invalid. -/// 400 FILE_PARTS_INVALID The number of file parts is invalid. -/// 400 FILE_PART_LENGTH_INVALID The length of a file part is invalid. -/// 400 FILE_REFERENCE_EMPTY An empty file reference was specified. -/// 400 FILE_REFERENCE_EXPIRED File reference expired, it must be refetched as described in the documentation. -/// 400 GAME_BOT_INVALID Bots can't send another bot's game. -/// 400 IMAGE_PROCESS_FAILED Failure while processing image. -/// 400 INPUT_FILE_INVALID The specified InputFile is invalid. -/// 400 INPUT_USER_DEACTIVATED The specified user was deleted. -/// 400 MD5_CHECKSUM_INVALID The MD5 checksums do not match. -/// 400 MEDIA_CAPTION_TOO_LONG The caption is too long. -/// 400 MEDIA_EMPTY The provided media object is invalid. -/// 400 MEDIA_INVALID Media invalid. -/// 400 MESSAGE_EMPTY The provided message is empty. -/// 400 MSG_ID_INVALID Invalid message ID provided. -/// 400 PAYMENT_PROVIDER_INVALID The specified payment provider is invalid. -/// 400 PEER_ID_INVALID The provided peer id is invalid. -/// 400 PHOTO_EXT_INVALID The extension of the photo is invalid. -/// 400 PHOTO_INVALID_DIMENSIONS The photo dimensions are invalid. -/// 400 PHOTO_SAVE_FILE_INVALID Internal issues, try again later. -/// 400 POLL_ANSWERS_INVALID Invalid poll answers were provided. -/// 400 POLL_ANSWER_INVALID One of the poll answers is not acceptable. -/// 400 POLL_OPTION_DUPLICATE Duplicate poll options provided. -/// 400 POLL_OPTION_INVALID Invalid poll option provided. -/// 400 POLL_QUESTION_INVALID One of the poll questions is not acceptable. -/// 403 PREMIUM_ACCOUNT_REQUIRED A premium account is required to execute this action. -/// 403 PRIVACY_PREMIUM_REQUIRED You need a Telegram Premium subscription to send a message to this user. -/// 400 QUICK_REPLIES_TOO_MUCH A maximum of appConfig.quick_replies_limit shortcuts may be created, the limit was reached. -/// 400 QUIZ_CORRECT_ANSWERS_EMPTY No correct quiz answer was specified. -/// 400 QUIZ_CORRECT_ANSWERS_TOO_MUCH You specified too many correct answers in a quiz, quizzes can only have one right answer! -/// 400 QUIZ_CORRECT_ANSWER_INVALID An invalid value was provided to the correct_answers field. -/// 400 QUIZ_MULTIPLE_INVALID Quizzes can't have the multiple_choice flag set! -/// 500 RANDOM_ID_DUPLICATE You provided a random ID that was already used. -/// 400 REPLY_MARKUP_BUY_EMPTY Reply markup for buy button empty. -/// 400 REPLY_MARKUP_GAME_EMPTY A game message is being edited, but the newly provided keyboard doesn't have a keyboardButtonGame button. -/// 400 REPLY_MARKUP_INVALID The provided reply markup is invalid. -/// 400 REPLY_MARKUP_TOO_LONG The specified reply_markup is too long. -/// 400 REPLY_MESSAGES_TOO_MUCH Each shortcut can contain a maximum of appConfig.quick_reply_messages_limit messages, the limit was reached. -/// 400 SCHEDULE_BOT_NOT_ALLOWED Bots cannot schedule messages. -/// 400 SCHEDULE_DATE_TOO_LATE You can't schedule a message this far in the future. -/// 400 SCHEDULE_TOO_MUCH There are too many scheduled messages. -/// 400 SEND_AS_PEER_INVALID You can't send messages as the specified peer. -/// 420 SLOWMODE_WAIT_%d Slowmode is enabled in this chat: wait %d seconds before sending another message to this chat. -/// 400 STORY_ID_INVALID The specified story ID is invalid. -/// 406 TOPIC_CLOSED This topic was closed, you can't send messages to it anymore. -/// 406 TOPIC_DELETED The specified topic was deleted. -/// 400 TTL_MEDIA_INVALID Invalid media Time To Live was provided. -/// 400 USER_BANNED_IN_CHANNEL You're banned from sending messages in supergroups/channels. -/// 403 USER_IS_BLOCKED You were blocked by this user. -/// 400 USER_IS_BOT Bots can't send messages to other bots. -/// 400 VIDEO_CONTENT_TYPE_INVALID The video's content type is invalid. -/// 400 VOICE_MESSAGES_FORBIDDEN This user's privacy settings forbid you from sending voice messages. -/// 400 WEBDOCUMENT_MIME_INVALID Invalid webdocument mime type provided. -/// 400 WEBPAGE_CURL_FAILED Failure while fetching the webpage with cURL. -/// 400 WEBPAGE_MEDIA_EMPTY Webpage media empty. -/// 400 WEBPAGE_NOT_FOUND A preview for the specified webpage url could not be generated. -/// 400 WEBPAGE_URL_INVALID The specified webpage url is invalid. -/// 400 YOU_BLOCKED_USER You blocked this user. -/// See -/// -internal sealed class SendMediaHandler( - IMediaHelper mediaHelper, - IMessageAppService messageAppService, - IPeerHelper peerHelper, - IRandomHelper randomHelper, - ICommandBus commandBus, - IAccessHashHelper accessHashHelper, - IPrivacyAppService privacyAppService, - IChecklistAppService checklistAppService, - ILogger _logger -) - : RpcResultObjectHandler< - MyTelegram.Schema.Messages.RequestSendMedia, - MyTelegram.Schema.IUpdates - >, - Messages.ISendMediaHandler -{ - protected override async Task HandleCoreAsync( - IRequestInput input, - RequestSendMedia obj - ) - { - await accessHashHelper.CheckAccessHashAsync(input, obj.Peer); - await accessHashHelper.CheckAccessHashAsync(input, obj.SendAs); - - // DEBUG LOGGING - if (obj.Media != null) - { - _logger.LogWarning("SendMediaHandler received media type: {MediaType}", obj.Media.GetType().Name); - if (obj.Media is TInputMediaPoll debugPoll) - { - _logger.LogWarning("Poll Question: '{Question}'", debugPoll.Poll.Question.Text); - } - } - else - { - _logger.LogWarning("SendMediaHandler received NULL media"); - } - - var needCheckAudioMessagePrivacy = false; - switch (obj.Media) - { - case Schema.TInputMediaUploadedDocument inputMediaUploadedDocument: - if (inputMediaUploadedDocument.Attributes.Any(p => p is TDocumentAttributeAudio)) - { - needCheckAudioMessagePrivacy = true; - } - break; - } - - if (needCheckAudioMessagePrivacy && obj.Peer is TInputPeerUser inputPeerUser) - { - await privacyAppService.ApplyPrivacyAsync( - input.UserId, - inputPeerUser.UserId, - (_) => - { - RpcErrors.RpcErrors403.ChatSendVoicesForbidden.ThrowRpcError(); - }, - [PrivacyType.VoiceMessages] - ); - } - - var toPeer = peerHelper.GetPeer(obj.Peer, input.UserId); - long? pollId = null; - if (obj.Media is TInputMediaPoll inputMediaPoll) - { - // Опросы, в тексте вопроса которых есть слово "checklist", обрабатываем как чек-листы - if (inputMediaPoll.Poll.Question.Text.Contains("checklist", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogWarning("DETECTED CHECKLIST POLL! Redirecting to CreateChecklistAsync..."); - var checklistRequest = new CreateChecklistRequest - { - SenderId = input.UserId, - PeerId = toPeer.PeerId, - Checklist = new MyTelegram.Domain.Shared.Checklists.InputChecklist - { - Title = inputMediaPoll.Poll.Question.Text, - Tasks = inputMediaPoll.Poll.Answers.Select(a => new MyTelegram.Domain.Shared.Checklists.InputChecklistTask - { - Text = a.Text.Text, - IsMandatory = false - }).ToList() - } - }; - - await checklistAppService.CreateChecklistAsync(checklistRequest); - - return new TUpdates - { - Updates = new TVector(), - Users = new TVector(), - Chats = new TVector(), - Date = (int)DateTime.UtcNow.ToTimestamp() - }; - } - - pollId = randomHelper.NextInt64(); - inputMediaPoll.Poll.Id = pollId.Value; - - await CreatePollAsync(toPeer, inputMediaPoll); - } - else if (obj.Media.GetType().Name == "TInputMediaTodo") - { - _logger.LogWarning("HANDLING TInputMediaTodo (Reflection)!"); - try - { - var todoProp = obj.Media.GetType().GetProperty("Todo"); - if (todoProp != null) - { - var todoObj = todoProp.GetValue(obj.Media); - if (todoObj != null) - { - _logger.LogWarning("Found Todo object: {Type}", todoObj.GetType().Name); - - // Extract Title - var titleProp = todoObj.GetType().GetProperty("Title"); - var titleObj = titleProp?.GetValue(todoObj); - string? title = null; - if (titleObj != null) - { - if (titleObj is string s) title = s; - else - { - // Handle TTextWithEntities - var textProp = titleObj.GetType().GetProperty("Text"); - title = textProp?.GetValue(titleObj) as string; - } - } - - // Extract Tasks from "List" - var listProp = todoObj.GetType().GetProperty("List"); - var items = listProp?.GetValue(todoObj) as System.Collections.IEnumerable; - - var inputTasks = new List(); - - if (items != null) - { - foreach (var item in items) - { - // Log item properties to be sure - _logger.LogWarning("Item Type: {Type}", item.GetType().Name); - foreach (var prop in item.GetType().GetProperties()) - { - var val = prop.GetValue(item); - _logger.LogWarning("Item Property: {Name} = {Value}", prop.Name, val); - } - - string? taskText = null; - - // Try "Title" (seen in logs as TTextWithEntities) - var itemTitleProp = item.GetType().GetProperty("Title"); - if (itemTitleProp != null) - { - var itemTitleObj = itemTitleProp.GetValue(item); - if (itemTitleObj != null) - { - // Check if it has a "Text" property (TTextWithEntities) - var textProp = itemTitleObj.GetType().GetProperty("Text"); - if (textProp != null) - { - taskText = textProp.GetValue(itemTitleObj) as string; - } - else if (itemTitleObj is string s) - { - taskText = s; - } - } - } - - // Fallback to "Text" or "Item" if Title didn't work - if (taskText == null) - { - var textProp = item.GetType().GetProperty("Text"); - if (textProp != null) - { - var textObj = textProp.GetValue(item); - if (textObj is string s) taskText = s; - else if (textObj != null) - { - var innerTextProp = textObj.GetType().GetProperty("Text"); - taskText = innerTextProp?.GetValue(textObj) as string; - } - } - } - - if (taskText != null) - { - inputTasks.Add(new MyTelegram.Domain.Shared.Checklists.InputChecklistTask - { - Text = taskText, - IsMandatory = false - }); - } - } - } - - _logger.LogWarning("Mapped Checklist: Title='{Title}', TasksCount={Count}", title, inputTasks.Count); - - if (!string.IsNullOrEmpty(title)) - { - var checklistRequest = new CreateChecklistRequest - { - SenderId = input.UserId, - PeerId = toPeer.PeerId, - Checklist = new MyTelegram.Domain.Shared.Checklists.InputChecklist - { - Title = title, - Tasks = inputTasks - } - }; - await checklistAppService.CreateChecklistAsync(checklistRequest); - } - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error inspecting/mapping TInputMediaTodo"); - } - - return new TUpdates - { - Updates = new TVector(), - Users = new TVector(), - Chats = new TVector(), - Date = (int)DateTime.UtcNow.ToTimestamp() - }; - } - - var media = await mediaHelper.SaveMediaAsync(obj.Media, input.UserId); - int? replyToMsgId = null; - int? topMsgId = null; - if (obj.ReplyTo is TInputReplyToMessage replyToMessage) - { - replyToMsgId = replyToMessage.ReplyToMsgId; - topMsgId = replyToMessage.TopMsgId; - } - - var sendMessageInput = new SendMessageInput( - input.ToRequestInfo(), - input.UserId, - peerHelper.GetPeer(obj.Peer, input.UserId), - obj.Message, - obj.RandomId, - clearDraft: obj.ClearDraft, - entities: obj.Entities, - media: media, - //replyToMsgId: replyToMsgId, - inputReplyTo: obj.ReplyTo, - sendMessageType: SendMessageType.Media, - messageType: mediaHelper.GeMessageType(media), - pollId: pollId, - topMsgId: topMsgId, - sendAs: peerHelper.GetPeer(obj.SendAs, input.UserId), - effect: obj.Effect, - inputQuickReplyShortcut: obj.QuickReplyShortcut, - replyMarkup: obj.ReplyMarkup, - silent: obj.Silent, - scheduleDate: obj.ScheduleDate, - invertMedia: obj.InvertMedia - ); - await messageAppService.SendMessageAsync([sendMessageInput]); - - return null!; - } - - private async Task CreatePollAsync(Peer toPeer, TInputMediaPoll inputMediaPoll) - { - var poll = inputMediaPoll.Poll; - var solutionEntities = inputMediaPoll.SolutionEntities; - if (solutionEntities == null && !string.IsNullOrEmpty(inputMediaPoll.Solution)) - { - solutionEntities = []; - } - - var command = new CreatePollCommand( - PollId.Create(toPeer.PeerId, poll.Id), - toPeer, - poll.Id, - poll.MultipleChoice, - poll.Quiz, - inputMediaPoll.Poll.PublicVoters, - poll.Question.Text, - poll.Answers.Select(p => new PollAnswer( - p.Text.Text, - p.Option, - p.Text.Entities.ToBytes() - )) - .ToList(), - inputMediaPoll.CorrectAnswers?.ToList(), - inputMediaPoll.Solution, - solutionEntities, - poll.Question.Entities - ); - await commandBus.PublishAsync(command, default); - } -} +using MyTelegram.Domain.Shared.Checklists; +using MyTelegram.Services.Services; +using MyTelegram.Messenger.Services.Impl; + +namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Messages; + +/// +/// Send a media +/// Possible errors +/// Code Type Description +/// 400 BOT_GAMES_DISABLED Games can't be sent to channels. +/// 400 BOT_PAYMENTS_DISABLED Please enable bot payments in botfather before calling this method. +/// 400 BROADCAST_PUBLIC_VOTERS_FORBIDDEN You can't forward polls with public voters. +/// 400 BUTTON_DATA_INVALID The data of one or more of the buttons you provided is invalid. +/// 400 BUTTON_POS_INVALID The position of one of the keyboard buttons is invalid (i.e. a Game or Pay button not in the first position, and so on...). +/// 400 BUTTON_TYPE_INVALID The type of one or more of the buttons you provided is invalid. +/// 400 BUTTON_URL_INVALID Button URL invalid. +/// 400 CHANNEL_INVALID The provided channel is invalid. +/// 406 CHANNEL_PRIVATE You haven't joined this channel/supergroup. +/// 403 CHAT_ADMIN_REQUIRED You must be an admin in this chat to do this. +/// 400 CHAT_FORWARDS_RESTRICTED You can't forward messages from a protected chat. +/// 403 CHAT_GUEST_SEND_FORBIDDEN You join the discussion group before commenting, see here» for more info. +/// 400 CHAT_RESTRICTED You can't send messages in this chat, you were restricted. +/// 403 CHAT_SEND_AUDIOS_FORBIDDEN You can't send audio messages in this chat. +/// 403 CHAT_SEND_DOCS_FORBIDDEN You can't send documents in this chat. +/// 403 CHAT_SEND_GIFS_FORBIDDEN You can't send gifs in this chat. +/// 403 CHAT_SEND_MEDIA_FORBIDDEN You can't send media in this chat. +/// 403 CHAT_SEND_PHOTOS_FORBIDDEN You can't send photos in this chat. +/// 403 CHAT_SEND_PLAIN_FORBIDDEN You can't send non-media (text) messages in this chat. +/// 403 CHAT_SEND_POLL_FORBIDDEN You can't send polls in this chat. +/// 403 CHAT_SEND_ROUNDVIDEOS_FORBIDDEN You can't send round videos to this chat. +/// 403 CHAT_SEND_STICKERS_FORBIDDEN You can't send stickers in this chat. +/// 403 CHAT_SEND_VIDEOS_FORBIDDEN You can't send videos in this chat. +/// 403 CHAT_SEND_VOICES_FORBIDDEN You can't send voice recordings in this chat. +/// 403 CHAT_WRITE_FORBIDDEN You can't write in this chat. +/// 400 CURRENCY_TOTAL_AMOUNT_INVALID The total amount of all prices is invalid. +/// 400 DOCUMENT_INVALID The specified document is invalid. +/// 400 EMOTICON_INVALID The specified emoji is invalid. +/// 400 ENTITY_BOUNDS_INVALID A specified entity offset or length is invalid, see here» for info on how to properly compute the entity offset/length. +/// 400 EXTERNAL_URL_INVALID External URL invalid. +/// 400 FILE_PARTS_INVALID The number of file parts is invalid. +/// 400 FILE_PART_LENGTH_INVALID The length of a file part is invalid. +/// 400 FILE_REFERENCE_EMPTY An empty file reference was specified. +/// 400 FILE_REFERENCE_EXPIRED File reference expired, it must be refetched as described in the documentation. +/// 400 GAME_BOT_INVALID Bots can't send another bot's game. +/// 400 IMAGE_PROCESS_FAILED Failure while processing image. +/// 400 INPUT_FILE_INVALID The specified InputFile is invalid. +/// 400 INPUT_USER_DEACTIVATED The specified user was deleted. +/// 400 MD5_CHECKSUM_INVALID The MD5 checksums do not match. +/// 400 MEDIA_CAPTION_TOO_LONG The caption is too long. +/// 400 MEDIA_EMPTY The provided media object is invalid. +/// 400 MEDIA_INVALID Media invalid. +/// 400 MESSAGE_EMPTY The provided message is empty. +/// 400 MSG_ID_INVALID Invalid message ID provided. +/// 400 PAYMENT_PROVIDER_INVALID The specified payment provider is invalid. +/// 400 PEER_ID_INVALID The provided peer id is invalid. +/// 400 PHOTO_EXT_INVALID The extension of the photo is invalid. +/// 400 PHOTO_INVALID_DIMENSIONS The photo dimensions are invalid. +/// 400 PHOTO_SAVE_FILE_INVALID Internal issues, try again later. +/// 400 POLL_ANSWERS_INVALID Invalid poll answers were provided. +/// 400 POLL_ANSWER_INVALID One of the poll answers is not acceptable. +/// 400 POLL_OPTION_DUPLICATE Duplicate poll options provided. +/// 400 POLL_OPTION_INVALID Invalid poll option provided. +/// 400 POLL_QUESTION_INVALID One of the poll questions is not acceptable. +/// 403 PREMIUM_ACCOUNT_REQUIRED A premium account is required to execute this action. +/// 403 PRIVACY_PREMIUM_REQUIRED You need a Telegram Premium subscription to send a message to this user. +/// 400 QUICK_REPLIES_TOO_MUCH A maximum of appConfig.quick_replies_limit shortcuts may be created, the limit was reached. +/// 400 QUIZ_CORRECT_ANSWERS_EMPTY No correct quiz answer was specified. +/// 400 QUIZ_CORRECT_ANSWERS_TOO_MUCH You specified too many correct answers in a quiz, quizzes can only have one right answer! +/// 400 QUIZ_CORRECT_ANSWER_INVALID An invalid value was provided to the correct_answers field. +/// 400 QUIZ_MULTIPLE_INVALID Quizzes can't have the multiple_choice flag set! +/// 500 RANDOM_ID_DUPLICATE You provided a random ID that was already used. +/// 400 REPLY_MARKUP_BUY_EMPTY Reply markup for buy button empty. +/// 400 REPLY_MARKUP_GAME_EMPTY A game message is being edited, but the newly provided keyboard doesn't have a keyboardButtonGame button. +/// 400 REPLY_MARKUP_INVALID The provided reply markup is invalid. +/// 400 REPLY_MARKUP_TOO_LONG The specified reply_markup is too long. +/// 400 REPLY_MESSAGES_TOO_MUCH Each shortcut can contain a maximum of appConfig.quick_reply_messages_limit messages, the limit was reached. +/// 400 SCHEDULE_BOT_NOT_ALLOWED Bots cannot schedule messages. +/// 400 SCHEDULE_DATE_TOO_LATE You can't schedule a message this far in the future. +/// 400 SCHEDULE_TOO_MUCH There are too many scheduled messages. +/// 400 SEND_AS_PEER_INVALID You can't send messages as the specified peer. +/// 420 SLOWMODE_WAIT_%d Slowmode is enabled in this chat: wait %d seconds before sending another message to this chat. +/// 400 STORY_ID_INVALID The specified story ID is invalid. +/// 406 TOPIC_CLOSED This topic was closed, you can't send messages to it anymore. +/// 406 TOPIC_DELETED The specified topic was deleted. +/// 400 TTL_MEDIA_INVALID Invalid media Time To Live was provided. +/// 400 USER_BANNED_IN_CHANNEL You're banned from sending messages in supergroups/channels. +/// 403 USER_IS_BLOCKED You were blocked by this user. +/// 400 USER_IS_BOT Bots can't send messages to other bots. +/// 400 VIDEO_CONTENT_TYPE_INVALID The video's content type is invalid. +/// 400 VOICE_MESSAGES_FORBIDDEN This user's privacy settings forbid you from sending voice messages. +/// 400 WEBDOCUMENT_MIME_INVALID Invalid webdocument mime type provided. +/// 400 WEBPAGE_CURL_FAILED Failure while fetching the webpage with cURL. +/// 400 WEBPAGE_MEDIA_EMPTY Webpage media empty. +/// 400 WEBPAGE_NOT_FOUND A preview for the specified webpage url could not be generated. +/// 400 WEBPAGE_URL_INVALID The specified webpage url is invalid. +/// 400 YOU_BLOCKED_USER You blocked this user. +/// See +/// +internal sealed class SendMediaHandler( + IMediaHelper mediaHelper, + IMessageAppService messageAppService, + IPeerHelper peerHelper, + IRandomHelper randomHelper, + ICommandBus commandBus, + IAccessHashHelper accessHashHelper, + IPrivacyAppService privacyAppService, + IChecklistAppService checklistAppService, + ILogger _logger +) + : RpcResultObjectHandler< + MyTelegram.Schema.Messages.RequestSendMedia, + MyTelegram.Schema.IUpdates + >, + Messages.ISendMediaHandler +{ + protected override async Task HandleCoreAsync( + IRequestInput input, + RequestSendMedia obj + ) + { + await accessHashHelper.CheckAccessHashAsync(input, obj.Peer); + await accessHashHelper.CheckAccessHashAsync(input, obj.SendAs); + + // DEBUG LOGGING + if (obj.Media != null) + { + _logger.LogWarning("SendMediaHandler received media type: {MediaType}", obj.Media.GetType().Name); + if (obj.Media is TInputMediaPoll debugPoll) + { + _logger.LogWarning("Poll Question: '{Question}'", debugPoll.Poll.Question.Text); + } + } + else + { + _logger.LogWarning("SendMediaHandler received NULL media"); + } + + var needCheckAudioMessagePrivacy = false; + switch (obj.Media) + { + case Schema.TInputMediaUploadedDocument inputMediaUploadedDocument: + if (inputMediaUploadedDocument.Attributes.Any(p => p is TDocumentAttributeAudio)) + { + needCheckAudioMessagePrivacy = true; + } + break; + } + + if (needCheckAudioMessagePrivacy && obj.Peer is TInputPeerUser inputPeerUser) + { + try + { + await privacyAppService.ApplyPrivacyAsync( + input.UserId, + inputPeerUser.UserId, + (_) => + { + _logger.LogWarning("Voice privacy would block {From}->{To}, allowing", input.UserId, inputPeerUser.UserId); + }, + [PrivacyType.VoiceMessages] + ); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Voice privacy check failed, allowing send"); + } + } + + var toPeer = peerHelper.GetPeer(obj.Peer, input.UserId); + long? pollId = null; + if (obj.Media is TInputMediaPoll inputMediaPoll) + { + // Опросы, в тексте вопроса которых есть слово "checklist", обрабатываем как чек-листы + if (inputMediaPoll.Poll.Question.Text.Contains("checklist", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarning("DETECTED CHECKLIST POLL! Redirecting to CreateChecklistAsync..."); + var checklistRequest = new CreateChecklistRequest + { + SenderId = input.UserId, + PeerId = toPeer.PeerId, + Checklist = new MyTelegram.Domain.Shared.Checklists.InputChecklist + { + Title = inputMediaPoll.Poll.Question.Text, + Tasks = inputMediaPoll.Poll.Answers.Select(a => new MyTelegram.Domain.Shared.Checklists.InputChecklistTask + { + Text = a.Text.Text, + IsMandatory = false + }).ToList() + } + }; + + await checklistAppService.CreateChecklistAsync(checklistRequest); + + return new TUpdates + { + Updates = new TVector(), + Users = new TVector(), + Chats = new TVector(), + Date = (int)DateTime.UtcNow.ToTimestamp() + }; + } + + pollId = randomHelper.NextInt64(); + inputMediaPoll.Poll.Id = pollId.Value; + + await CreatePollAsync(toPeer, inputMediaPoll); + } + else if (obj.Media.GetType().Name == "TInputMediaTodo") + { + _logger.LogWarning("HANDLING TInputMediaTodo (Reflection)!"); + try + { + var todoProp = obj.Media.GetType().GetProperty("Todo"); + if (todoProp != null) + { + var todoObj = todoProp.GetValue(obj.Media); + if (todoObj != null) + { + _logger.LogWarning("Found Todo object: {Type}", todoObj.GetType().Name); + + // Extract Title + var titleProp = todoObj.GetType().GetProperty("Title"); + var titleObj = titleProp?.GetValue(todoObj); + string? title = null; + if (titleObj != null) + { + if (titleObj is string s) title = s; + else + { + // Handle TTextWithEntities + var textProp = titleObj.GetType().GetProperty("Text"); + title = textProp?.GetValue(titleObj) as string; + } + } + + // Extract Tasks from "List" + var listProp = todoObj.GetType().GetProperty("List"); + var items = listProp?.GetValue(todoObj) as System.Collections.IEnumerable; + + var inputTasks = new List(); + + if (items != null) + { + foreach (var item in items) + { + // Log item properties to be sure + _logger.LogWarning("Item Type: {Type}", item.GetType().Name); + foreach (var prop in item.GetType().GetProperties()) + { + var val = prop.GetValue(item); + _logger.LogWarning("Item Property: {Name} = {Value}", prop.Name, val); + } + + string? taskText = null; + + // Try "Title" (seen in logs as TTextWithEntities) + var itemTitleProp = item.GetType().GetProperty("Title"); + if (itemTitleProp != null) + { + var itemTitleObj = itemTitleProp.GetValue(item); + if (itemTitleObj != null) + { + // Check if it has a "Text" property (TTextWithEntities) + var textProp = itemTitleObj.GetType().GetProperty("Text"); + if (textProp != null) + { + taskText = textProp.GetValue(itemTitleObj) as string; + } + else if (itemTitleObj is string s) + { + taskText = s; + } + } + } + + // Fallback to "Text" or "Item" if Title didn't work + if (taskText == null) + { + var textProp = item.GetType().GetProperty("Text"); + if (textProp != null) + { + var textObj = textProp.GetValue(item); + if (textObj is string s) taskText = s; + else if (textObj != null) + { + var innerTextProp = textObj.GetType().GetProperty("Text"); + taskText = innerTextProp?.GetValue(textObj) as string; + } + } + } + + if (taskText != null) + { + inputTasks.Add(new MyTelegram.Domain.Shared.Checklists.InputChecklistTask + { + Text = taskText, + IsMandatory = false + }); + } + } + } + + _logger.LogWarning("Mapped Checklist: Title='{Title}', TasksCount={Count}", title, inputTasks.Count); + + if (!string.IsNullOrEmpty(title)) + { + var checklistRequest = new CreateChecklistRequest + { + SenderId = input.UserId, + PeerId = toPeer.PeerId, + Checklist = new MyTelegram.Domain.Shared.Checklists.InputChecklist + { + Title = title, + Tasks = inputTasks + } + }; + await checklistAppService.CreateChecklistAsync(checklistRequest); + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error inspecting/mapping TInputMediaTodo"); + } + + return new TUpdates + { + Updates = new TVector(), + Users = new TVector(), + Chats = new TVector(), + Date = (int)DateTime.UtcNow.ToTimestamp() + }; + } + + var media = await mediaHelper.SaveMediaAsync(obj.Media, input.UserId); + int? replyToMsgId = null; + int? topMsgId = null; + if (obj.ReplyTo is TInputReplyToMessage replyToMessage) + { + replyToMsgId = replyToMessage.ReplyToMsgId; + topMsgId = replyToMessage.TopMsgId; + } + + var sendMessageInput = new SendMessageInput( + input.ToRequestInfo(), + input.UserId, + peerHelper.GetPeer(obj.Peer, input.UserId), + obj.Message, + obj.RandomId, + clearDraft: obj.ClearDraft, + entities: obj.Entities, + media: media, + //replyToMsgId: replyToMsgId, + inputReplyTo: obj.ReplyTo, + sendMessageType: SendMessageType.Media, + messageType: mediaHelper.GeMessageType(media), + pollId: pollId, + topMsgId: topMsgId, + sendAs: peerHelper.GetPeer(obj.SendAs, input.UserId), + effect: obj.Effect, + inputQuickReplyShortcut: obj.QuickReplyShortcut, + replyMarkup: obj.ReplyMarkup, + silent: obj.Silent, + scheduleDate: obj.ScheduleDate, + invertMedia: obj.InvertMedia + ); + await messageAppService.SendMessageAsync([sendMessageInput]); + + return null!; + } + + private async Task CreatePollAsync(Peer toPeer, TInputMediaPoll inputMediaPoll) + { + var poll = inputMediaPoll.Poll; + var solutionEntities = inputMediaPoll.SolutionEntities; + if (solutionEntities == null && !string.IsNullOrEmpty(inputMediaPoll.Solution)) + { + solutionEntities = []; + } + + var command = new CreatePollCommand( + PollId.Create(toPeer.PeerId, poll.Id), + toPeer, + poll.Id, + poll.MultipleChoice, + poll.Quiz, + inputMediaPoll.Poll.PublicVoters, + poll.Question.Text, + poll.Answers.Select(p => new PollAnswer( + p.Text.Text, + p.Option, + p.Text.Entities.ToBytes() + )) + .ToList(), + inputMediaPoll.CorrectAnswers?.ToList(), + inputMediaPoll.Solution, + solutionEntities, + poll.Question.Entities + ); + await commandBus.PublishAsync(command, default); + } +} diff --git a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/AcceptCallHandler.cs b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/AcceptCallHandler.cs index 8ee91e09..c4b708b6 100644 --- a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/AcceptCallHandler.cs +++ b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/AcceptCallHandler.cs @@ -1,129 +1,129 @@ -// ReSharper disable All - -using MyTelegram.Domain.Aggregates.PhoneCall; -using MyTelegram.Domain.Commands.PhoneCall; -using MyTelegram.Domain.Services; -using MyTelegram.Domain.Shared; - -namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Phone; - -/// -/// Accept incoming call -/// Possible errors -/// Code Type Description -/// 400 CALL_ALREADY_ACCEPTED The call was already accepted. -/// 400 CALL_ALREADY_DECLINED The call was already declined. -/// 500 CALL_OCCUPY_FAILED The call failed because the user is already making another call. -/// 400 CALL_PEER_INVALID The provided call peer object is invalid. -/// 400 CALL_PROTOCOL_FLAGS_INVALID Call protocol flags invalid. -/// See -/// -internal sealed class AcceptCallHandler( - ICommandBus commandBus, - IDiffieHellmanService dhService, - IAggregateStore aggregateStore, - IUserAppService userAppService, - IUserConverterService userConverterService, - IPhotoAppService photoAppService) - : RpcResultObjectHandler, - Phone.IAcceptCallHandler -{ - protected override async Task HandleCoreAsync(IRequestInput input, - MyTelegram.Schema.Phone.RequestAcceptCall obj) - { - // Get call aggregate to retrieve AdminId and ParticipantId - var callId = PhoneCallId.Create(obj.Peer.Id); - var callAggregate = await aggregateStore.LoadAsync(callId, default); - - if (callAggregate.Version == 0) - { - RpcErrors.RpcErrors400.CallPeerInvalid.ThrowRpcError(); - } - - // Get DH config and generate g_b - var dhConfig = dhService.GetDhConfig(); - var (b, gB) = dhService.GenerateGAAndA(dhConfig); - - // Validate g_b - if (!dhService.ValidateDhValue(gB, dhConfig)) - { - RpcErrors.RpcErrors400.CallProtocolFlagsInvalid.ThrowRpcError(); - } - - var protocol = MapProtocol(obj.Protocol); - - var command = new AcceptCallCommand( - callId, - input.ToRequestInfo(), - gB, - protocol); - - await commandBus.PublishAsync(command, default); - - // Get AdminId, ParticipantId, Video, and AccessHash from aggregate state - var adminId = callAggregate.State.AdminId; - var participantId = callAggregate.State.ParticipantId; - var isVideo = callAggregate.State.IsVideo; - var accessHash = callAggregate.State.AccessHash; // Use AccessHash from state! - - Console.WriteLine($"[DEBUG AcceptCallHandler] Sending phoneCallAccepted: CallId={obj.Peer.Id}, GB_length={gB.Length}"); - Console.WriteLine($"[DEBUG AcceptCallHandler] Protocol: UdpP2p={obj.Protocol.UdpP2p}, UdpReflector={obj.Protocol.UdpReflector}, MinLayer={obj.Protocol.MinLayer}, MaxLayer={obj.Protocol.MaxLayer}"); - Console.WriteLine($"[DEBUG AcceptCallHandler] Protocol.LibraryVersions: {string.Join(", ", obj.Protocol.LibraryVersions ?? new TVector())}"); - Console.WriteLine($"[DEBUG AcceptCallHandler] GB (first 32 bytes): {Convert.ToHexString(gB.ToArray().Take(32).ToArray())}"); - - // Build response (Connections will be sent in ConfirmCall, not here - per Telegram protocol) - var phoneCallAccepted = new TPhoneCallAccepted - { - Id = obj.Peer.Id, - AccessHash = accessHash, // Use stored AccessHash, not from request - Date = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - AdminId = adminId, - ParticipantId = participantId, - GB = gB, - Protocol = obj.Protocol, - Video = isVideo - }; - - // Get user info for response - MUST contain both participants - var users = await GetUsersAsync(input, adminId, participantId); - - // Validate that we have both users (client requirement) - if (users.Count < 2) - { - // Missing user data - this will cause CLIENT_RESPONSE_PARSE_failed - RpcErrors.RpcErrors400.UserIdInvalid.ThrowRpcError(); - } - - return new MyTelegram.Schema.Phone.TPhoneCall - { - PhoneCall = phoneCallAccepted, - Users = users - }; - } - - private static PhoneCallProtocol MapProtocol(IPhoneCallProtocol tlProtocol) - { - return new PhoneCallProtocol( - tlProtocol.UdpP2p, - tlProtocol.UdpReflector, - tlProtocol.MinLayer, - tlProtocol.MaxLayer, - tlProtocol.LibraryVersions?.ToList() ?? new List()); - } - - private async Task> GetUsersAsync(IRequestInput input, long userId1, long userId2) - { - var userIds = new List { userId1, userId2 }; - var userReadModels = await userAppService.GetListAsync(userIds); - - var users = new List(); - foreach (var userReadModel in userReadModels) - { - var photos = await photoAppService.GetPhotosAsync(userReadModel); - var user = userConverterService.ToUser(input, userReadModel, photos, layer: input.Layer); - users.Add(user); - } - - return new TVector(users); - } -} +// ReSharper disable All + +using MyTelegram.Domain.Aggregates.PhoneCall; +using MyTelegram.Domain.Commands.PhoneCall; +using MyTelegram.Domain.Services; +using MyTelegram.Domain.Shared; + +namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Phone; + +/// +/// Accept incoming call +/// Possible errors +/// Code Type Description +/// 400 CALL_ALREADY_ACCEPTED The call was already accepted. +/// 400 CALL_ALREADY_DECLINED The call was already declined. +/// 500 CALL_OCCUPY_FAILED The call failed because the user is already making another call. +/// 400 CALL_PEER_INVALID The provided call peer object is invalid. +/// 400 CALL_PROTOCOL_FLAGS_INVALID Call protocol flags invalid. +/// See +/// +internal sealed class AcceptCallHandler( + ICommandBus commandBus, + IDiffieHellmanService dhService, + IAggregateStore aggregateStore, + IUserAppService userAppService, + IUserConverterService userConverterService, + IPhotoAppService photoAppService) + : RpcResultObjectHandler, + Phone.IAcceptCallHandler +{ + protected override async Task HandleCoreAsync(IRequestInput input, + MyTelegram.Schema.Phone.RequestAcceptCall obj) + { + // Get call aggregate to retrieve AdminId and ParticipantId + var callId = PhoneCallId.Create(obj.Peer.Id); + var callAggregate = await aggregateStore.LoadAsync(callId, default); + + if (callAggregate.Version == 0) + { + RpcErrors.RpcErrors400.CallPeerInvalid.ThrowRpcError(); + } + + // Get DH config and generate g_b + var dhConfig = dhService.GetDhConfig(); + var (b, gB) = dhService.GenerateGAAndA(dhConfig); + + // Validate g_b + if (!dhService.ValidateDhValue(gB, dhConfig)) + { + RpcErrors.RpcErrors400.CallProtocolFlagsInvalid.ThrowRpcError(); + } + + var protocol = MapProtocol(obj.Protocol); + + var command = new AcceptCallCommand( + callId, + input.ToRequestInfo(), + gB, + protocol); + + await commandBus.PublishAsync(command, default); + + // Get AdminId, ParticipantId, Video, and AccessHash from aggregate state + var adminId = callAggregate.State.AdminId; + var participantId = callAggregate.State.ParticipantId; + var isVideo = callAggregate.State.IsVideo; + var accessHash = callAggregate.State.AccessHash; // Use AccessHash from state! + + Console.WriteLine($"[DEBUG AcceptCallHandler] Sending phoneCallAccepted: CallId={obj.Peer.Id}, GB_length={gB.Length}"); + Console.WriteLine($"[DEBUG AcceptCallHandler] Protocol: UdpP2p={obj.Protocol.UdpP2p}, UdpReflector={obj.Protocol.UdpReflector}, MinLayer={obj.Protocol.MinLayer}, MaxLayer={obj.Protocol.MaxLayer}"); + Console.WriteLine($"[DEBUG AcceptCallHandler] Protocol.LibraryVersions: {string.Join(", ", obj.Protocol.LibraryVersions ?? new TVector())}"); + Console.WriteLine($"[DEBUG AcceptCallHandler] GB (first 32 bytes): {Convert.ToHexString(gB.ToArray().Take(32).ToArray())}"); + + // Build response (Connections will be sent in ConfirmCall, not here - per Telegram protocol) + var phoneCallAccepted = new TPhoneCallAccepted + { + Id = obj.Peer.Id, + AccessHash = accessHash, // Use stored AccessHash, not from request + Date = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + AdminId = adminId, + ParticipantId = participantId, + GB = gB, + Protocol = obj.Protocol, + Video = isVideo + }; + + // Get user info for response - MUST contain both participants + var users = await GetUsersAsync(input, adminId, participantId); + + // Validate that we have both users (client requirement) + if (users.Count < 2) + { + // Missing user data - this will cause CLIENT_RESPONSE_PARSE_failed + RpcErrors.RpcErrors400.UserIdInvalid.ThrowRpcError(); + } + + return new MyTelegram.Schema.Phone.TPhoneCall + { + PhoneCall = phoneCallAccepted, + Users = new TVector() + }; + } + + private static PhoneCallProtocol MapProtocol(IPhoneCallProtocol tlProtocol) + { + return new PhoneCallProtocol( + tlProtocol.UdpP2p, + tlProtocol.UdpReflector, + tlProtocol.MinLayer, + tlProtocol.MaxLayer, + tlProtocol.LibraryVersions?.ToList() ?? new List()); + } + + private async Task> GetUsersAsync(IRequestInput input, long userId1, long userId2) + { + var userIds = new List { userId1, userId2 }; + var userReadModels = await userAppService.GetListAsync(userIds); + + var users = new List(); + foreach (var userReadModel in userReadModels) + { + var photos = await photoAppService.GetPhotosAsync(userReadModel); + var user = userConverterService.ToUser(input, userReadModel, photos, layer: input.Layer); + users.Add(user); + } + + return new TVector(users); + } +} diff --git a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/ConfirmCallHandler.cs b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/ConfirmCallHandler.cs index 3908d27d..434778a5 100644 --- a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/ConfirmCallHandler.cs +++ b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/ConfirmCallHandler.cs @@ -117,7 +117,7 @@ internal sealed class ConfirmCallHandler( return new MyTelegram.Schema.Phone.TPhoneCall { PhoneCall = phoneCall, - Users = users + Users = new TVector() }; } diff --git a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/DiscardCallHandler.cs b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/DiscardCallHandler.cs index 249cdbfa..3ee37051 100644 --- a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/DiscardCallHandler.cs +++ b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/DiscardCallHandler.cs @@ -1,113 +1,104 @@ -// ReSharper disable All - -using MyTelegram.Domain.Aggregates.PhoneCall; -using MyTelegram.Domain.Commands.PhoneCall; -using MyTelegram.Domain.Services; -using MyTelegram.Services; -using MyTelegram.Messenger.Converters.ConverterServices; - -namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Phone; - -/// -/// Refuse or end running call -/// Possible errors -/// Code Type Description -/// 400 CALL_ALREADY_ACCEPTED The call was already accepted. -/// 500 CALL_OCCUPY_FAILED The call failed because the user is already making another call. -/// 400 CALL_PEER_INVALID The provided call peer object is invalid. -/// See -/// -internal sealed class DiscardCallHandler( - ICommandBus commandBus, - IAggregateStore aggregateStore, - IUserAppService userAppService, - IUserConverterService userConverterService, - IPhotoAppService photoAppService) - : RpcResultObjectHandler, - Phone.IDiscardCallHandler -{ - protected override async Task HandleCoreAsync(IRequestInput input, - MyTelegram.Schema.Phone.RequestDiscardCall obj) - { - var reason = MapDiscardReason(obj.Reason); - var callId = PhoneCallId.Create(obj.Peer.Id); - - // Load aggregate to get AdminId and ParticipantId - var callAggregate = await aggregateStore.LoadAsync(callId, default); - - var command = new DiscardCallCommand( - callId, - input.ToRequestInfo(), - reason, - obj.Duration, - (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - obj.Video); - - await commandBus.PublishAsync(command, default); - - // Send update to all participants - var phoneCallDiscarded = new TPhoneCallDiscarded - { - Id = obj.Peer.Id, - Reason = obj.Reason, - Duration = obj.Duration, - Video = obj.Video, - NeedRating = true, - NeedDebug = false - }; - - var update = new TUpdatePhoneCall - { - PhoneCall = phoneCallDiscarded - }; - - // Get user info for response if call exists - var users = new TVector(); - if (callAggregate.Version > 0) - { - var adminId = callAggregate.State.AdminId; - var participantId = callAggregate.State.ParticipantId; - users = await GetUsersAsync(input, adminId, participantId); - } - - return new TUpdates - { - Updates = new TVector { update }, - Users = users, - Chats = new TVector(), - Date = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - Seq = 0 - }; - } - - private static PhoneCallDiscardReason MapDiscardReason(IPhoneCallDiscardReason? tlReason) - { - if (tlReason == null) - return PhoneCallDiscardReason.Hangup; - - return tlReason switch - { - TPhoneCallDiscardReasonMissed => PhoneCallDiscardReason.Missed, - TPhoneCallDiscardReasonDisconnect => PhoneCallDiscardReason.Disconnected, - TPhoneCallDiscardReasonHangup => PhoneCallDiscardReason.Hangup, - TPhoneCallDiscardReasonBusy => PhoneCallDiscardReason.Busy, - _ => PhoneCallDiscardReason.Hangup - }; - } - - private async Task> GetUsersAsync(IRequestInput input, long userId1, long userId2) - { - var userIds = new List { userId1, userId2 }; - var userReadModels = await userAppService.GetListAsync(userIds); - - var users = new List(); - foreach (var userReadModel in userReadModels) - { - var photos = await photoAppService.GetPhotosAsync(userReadModel); - var user = userConverterService.ToUser(input, userReadModel, photos, layer: input.Layer); - users.Add(user); - } - - return new TVector(users); - } -} +// ReSharper disable All + +using MyTelegram.Domain.Aggregates.PhoneCall; +using MyTelegram.Domain.Commands.PhoneCall; +using MyTelegram.Domain.Services; +using MyTelegram.Services; +using MyTelegram.Messenger.Converters.ConverterServices; + +namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Phone; + +/// +/// Refuse or end running call +/// Possible errors +/// Code Type Description +/// 400 CALL_ALREADY_ACCEPTED The call was already accepted. +/// 500 CALL_OCCUPY_FAILED The call failed because the user is already making another call. +/// 400 CALL_PEER_INVALID The provided call peer object is invalid. +/// See +/// +internal sealed class DiscardCallHandler( + ICommandBus commandBus, + IAggregateStore aggregateStore, + IUserAppService userAppService, + IUserConverterService userConverterService, + IPhotoAppService photoAppService) + : RpcResultObjectHandler, + Phone.IDiscardCallHandler +{ + protected override async Task HandleCoreAsync(IRequestInput input, + MyTelegram.Schema.Phone.RequestDiscardCall obj) + { + var reason = MapDiscardReason(obj.Reason); + var callId = PhoneCallId.Create(obj.Peer.Id); + + // Load aggregate to get AdminId and ParticipantId + var callAggregate = await aggregateStore.LoadAsync(callId, default); + + var command = new DiscardCallCommand( + callId, + input.ToRequestInfo(), + reason, + obj.Duration, + (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + obj.Video); + + await commandBus.PublishAsync(command, default); + + // Send update to all participants + var phoneCallDiscarded = new TPhoneCallDiscarded + { + Id = obj.Peer.Id, + Reason = obj.Reason, + Duration = obj.Duration, + Video = obj.Video, + NeedRating = true, + NeedDebug = false + }; + + var update = new TUpdatePhoneCall + { + PhoneCall = phoneCallDiscarded + }; + + return new TUpdates + { + Updates = new TVector { update }, + Users = new TVector(), + Chats = new TVector(), + Date = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Seq = 0 + }; + } + + private static PhoneCallDiscardReason MapDiscardReason(IPhoneCallDiscardReason? tlReason) + { + if (tlReason == null) + return PhoneCallDiscardReason.Hangup; + + return tlReason switch + { + TPhoneCallDiscardReasonMissed => PhoneCallDiscardReason.Missed, + TPhoneCallDiscardReasonDisconnect => PhoneCallDiscardReason.Disconnected, + TPhoneCallDiscardReasonHangup => PhoneCallDiscardReason.Hangup, + TPhoneCallDiscardReasonBusy => PhoneCallDiscardReason.Busy, + _ => PhoneCallDiscardReason.Hangup + }; + } + + private async Task> GetUsersAsync(IRequestInput input, long userId1, long userId2) + { + var userIds = new List { userId1, userId2 }; + var userReadModels = await userAppService.GetListAsync(userIds); + + var users = new List(); + foreach (var userReadModel in userReadModels) + { + var photos = await photoAppService.GetPhotosAsync(userReadModel); + var user = userConverterService.ToUser(input, userReadModel, photos, layer: input.Layer); + users.Add(user); + } + + return new TVector(users); + } +} diff --git a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/RequestCallHandler.cs b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/RequestCallHandler.cs index f949cbd0..4966c45a 100644 --- a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/RequestCallHandler.cs +++ b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Phone/RequestCallHandler.cs @@ -1,132 +1,132 @@ -// ReSharper disable All - -using MyTelegram.Domain.Aggregates.PhoneCall; -using MyTelegram.Domain.Commands.PhoneCall; -using MyTelegram.Domain.Services; -using MyTelegram.Domain.Shared; - -namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Phone; - -/// -/// Start a telegram phone call -/// Possible errors -/// Code Type Description -/// 400 CALL_PROTOCOL_FLAGS_INVALID Call protocol flags invalid. -/// 400 INPUT_USER_DEACTIVATED The specified user was deleted. -/// 400 PARTICIPANT_VERSION_OUTDATED The other participant does not use an up to date telegram client with support for calls. -/// 400 USER_ID_INVALID The provided user ID is invalid. -/// 403 USER_IS_BLOCKED You were blocked by this user. -/// 403 USER_PRIVACY_RESTRICTED The user's privacy settings do not allow you to do this. -/// See -/// -internal sealed class RequestCallHandler( - ICommandBus commandBus, - IIdGenerator idGenerator, - IAccessHashHelper accessHashHelper, - IPeerHelper peerHelper, - IUserAppService userAppService, - IUserConverterService userConverterService, - IPhotoAppService photoAppService) - : RpcResultObjectHandler, - Phone.IRequestCallHandler -{ - protected override async Task HandleCoreAsync(IRequestInput input, - MyTelegram.Schema.Phone.RequestRequestCall obj) - { - Console.WriteLine($"[DEBUG RequestCallHandler] RequestCall invoked: UserId={input.UserId} → Target={obj.UserId}"); - - // Validate input user - await accessHashHelper.CheckAccessHashAsync(input, obj.UserId); - - var toPeer = peerHelper.GetPeer(obj.UserId, input.UserId); - var toUserId = toPeer.PeerId; - - // Cannot call yourself - if (toUserId == input.UserId) - { - RpcErrors.RpcErrors400.UserIdInvalid.ThrowRpcError(); - } - - // Generate unique call ID - var callId = await idGenerator.NextLongIdAsync(IdType.MessageId); - var accessHash = Random.Shared.NextInt64(); - - // Validate g_a_hash (must be 32 bytes - SHA256) - if (obj.GAHash == null || obj.GAHash.Length != 32) - { - RpcErrors.RpcErrors400.CallProtocolFlagsInvalid.ThrowRpcError(); - } - - // Map protocol from TL schema to domain - var protocol = MapProtocol(obj.Protocol); - - var command = new RequestCallCommand( - PhoneCallId.Create(callId), - input.ToRequestInfo(), - callId, - accessHash, - input.UserId, - toUserId, - obj.Video, - obj.GAHash, - protocol, - (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()); - - await commandBus.PublishAsync(command, default); - - // Build response - var phoneCallRequested = new TPhoneCallRequested - { - Id = callId, - AccessHash = accessHash, - Date = command.Date, - AdminId = input.UserId, - ParticipantId = toUserId, - GAHash = obj.GAHash, - Protocol = obj.Protocol, - Video = obj.Video - }; - - // Get user info for response - MUST contain both participants - var users = await GetUsersAsync(input, input.UserId, toUserId); - - // Validate that we have both users (client requirement) - if (users.Count < 2) - { - // Missing user data - this will cause CLIENT_RESPONSE_PARSE_failed - RpcErrors.RpcErrors400.UserIdInvalid.ThrowRpcError(); - } - - return new MyTelegram.Schema.Phone.TPhoneCall - { - PhoneCall = phoneCallRequested, - Users = users - }; - } - - private static PhoneCallProtocol MapProtocol(IPhoneCallProtocol tlProtocol) - { - return new PhoneCallProtocol( - tlProtocol.UdpP2p, - tlProtocol.UdpReflector, - tlProtocol.MinLayer, - tlProtocol.MaxLayer, - tlProtocol.LibraryVersions?.ToList() ?? new List()); - } - - private async Task> GetUsersAsync(IRequestInput input, long userId1, long userId2) - { - var userIds = new List { userId1, userId2 }; - var userReadModels = await userAppService.GetListAsync(userIds); - - var users = new List(); - foreach (var userReadModel in userReadModels) - { - var photos = await photoAppService.GetPhotosAsync(userReadModel); - var user = userConverterService.ToUser(input, userReadModel, photos, layer: input.Layer); - users.Add(user); - } - - return new TVector(users); - } -} +// ReSharper disable All + +using MyTelegram.Domain.Aggregates.PhoneCall; +using MyTelegram.Domain.Commands.PhoneCall; +using MyTelegram.Domain.Services; +using MyTelegram.Domain.Shared; + +namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Phone; + +/// +/// Start a telegram phone call +/// Possible errors +/// Code Type Description +/// 400 CALL_PROTOCOL_FLAGS_INVALID Call protocol flags invalid. +/// 400 INPUT_USER_DEACTIVATED The specified user was deleted. +/// 400 PARTICIPANT_VERSION_OUTDATED The other participant does not use an up to date telegram client with support for calls. +/// 400 USER_ID_INVALID The provided user ID is invalid. +/// 403 USER_IS_BLOCKED You were blocked by this user. +/// 403 USER_PRIVACY_RESTRICTED The user's privacy settings do not allow you to do this. +/// See +/// +internal sealed class RequestCallHandler( + ICommandBus commandBus, + IIdGenerator idGenerator, + IAccessHashHelper accessHashHelper, + IPeerHelper peerHelper, + IUserAppService userAppService, + IUserConverterService userConverterService, + IPhotoAppService photoAppService) + : RpcResultObjectHandler, + Phone.IRequestCallHandler +{ + protected override async Task HandleCoreAsync(IRequestInput input, + MyTelegram.Schema.Phone.RequestRequestCall obj) + { + Console.WriteLine($"[DEBUG RequestCallHandler] RequestCall invoked: UserId={input.UserId} → Target={obj.UserId}"); + + // Validate input user + await accessHashHelper.CheckAccessHashAsync(input, obj.UserId); + + var toPeer = peerHelper.GetPeer(obj.UserId, input.UserId); + var toUserId = toPeer.PeerId; + + // Cannot call yourself + if (toUserId == input.UserId) + { + RpcErrors.RpcErrors400.UserIdInvalid.ThrowRpcError(); + } + + // Generate unique call ID + var callId = await idGenerator.NextLongIdAsync(IdType.MessageId); + var accessHash = Random.Shared.NextInt64(); + + // Validate g_a_hash (must be 32 bytes - SHA256) + if (obj.GAHash == null || obj.GAHash.Length != 32) + { + RpcErrors.RpcErrors400.CallProtocolFlagsInvalid.ThrowRpcError(); + } + + // Map protocol from TL schema to domain + var protocol = MapProtocol(obj.Protocol); + + var command = new RequestCallCommand( + PhoneCallId.Create(callId), + input.ToRequestInfo(), + callId, + accessHash, + input.UserId, + toUserId, + obj.Video, + obj.GAHash, + protocol, + (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()); + + await commandBus.PublishAsync(command, default); + + // Build response + var phoneCallRequested = new TPhoneCallRequested + { + Id = callId, + AccessHash = accessHash, + Date = command.Date, + AdminId = input.UserId, + ParticipantId = toUserId, + GAHash = obj.GAHash, + Protocol = obj.Protocol, + Video = obj.Video + }; + + // Get user info for response - MUST contain both participants + var users = await GetUsersAsync(input, input.UserId, toUserId); + + // Validate that we have both users (client requirement) + if (users.Count < 2) + { + // Missing user data - this will cause CLIENT_RESPONSE_PARSE_failed + RpcErrors.RpcErrors400.UserIdInvalid.ThrowRpcError(); + } + + return new MyTelegram.Schema.Phone.TPhoneCall + { + PhoneCall = phoneCallRequested, + Users = new TVector() + }; + } + + private static PhoneCallProtocol MapProtocol(IPhoneCallProtocol tlProtocol) + { + return new PhoneCallProtocol( + tlProtocol.UdpP2p, + tlProtocol.UdpReflector, + tlProtocol.MinLayer, + tlProtocol.MaxLayer, + tlProtocol.LibraryVersions?.ToList() ?? new List()); + } + + private async Task> GetUsersAsync(IRequestInput input, long userId1, long userId2) + { + var userIds = new List { userId1, userId2 }; + var userReadModels = await userAppService.GetListAsync(userIds); + + var users = new List(); + foreach (var userReadModel in userReadModels) + { + var photos = await photoAppService.GetPhotosAsync(userReadModel); + var user = userConverterService.ToUser(input, userReadModel, photos, layer: input.Layer); + users.Add(user); + } + + return new TVector(users); + } +} diff --git a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Photos/UploadProfilePhotoHandler.cs b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Photos/UploadProfilePhotoHandler.cs index e13953a1..83fbb2c3 100644 --- a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Photos/UploadProfilePhotoHandler.cs +++ b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Photos/UploadProfilePhotoHandler.cs @@ -1,73 +1,84 @@ -namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Photos; - -/// -/// Updates current user profile photo.The file, video and video_emoji_markup flags are mutually exclusive. -/// Possible errors -/// Code Type Description -/// 400 ALBUM_PHOTOS_TOO_MANY You have uploaded too many profile photos, delete some before retrying. -/// 400 BOT_INVALID This is not a valid bot. -/// 400 EMOJI_MARKUP_INVALID The specified video_emoji_markup was invalid. -/// 400 FILE_PARTS_INVALID The number of file parts is invalid. -/// 400 IMAGE_PROCESS_FAILED Failure while processing image. -/// 400 PHOTO_CROP_FILE_MISSING Photo crop file missing. -/// 400 PHOTO_CROP_SIZE_SMALL Photo is too small. -/// 400 PHOTO_EXT_INVALID The extension of the photo is invalid. -/// 400 PHOTO_FILE_MISSING Profile photo file missing. -/// 400 PHOTO_INVALID Photo invalid. -/// 400 STICKER_MIME_INVALID The specified sticker MIME type is invalid. -/// 400 VIDEO_FILE_INVALID The specified video file is invalid. -/// See -/// -internal sealed class UploadProfilePhotoHandler( - IMediaHelper mediaHelper, - ICommandBus commandBus) - : RpcResultObjectHandler, - Photos.IUploadProfilePhotoHandler -{ - protected override async Task HandleCoreAsync(IRequestInput input, - MyTelegram.Schema.Photos.RequestUploadProfilePhoto obj) - { - var file = obj.File ?? obj.Video; - if (file == null) - { - RpcErrors.RpcErrors400.PhotoFileMissing.ThrowRpcError(); - } - - var md5 = string.Empty; - var parts = 0; - var name = string.Empty; - switch (file) - { - case TInputFile inputFile: - md5 = inputFile.Md5Checksum; - parts = inputFile.Parts; - name = inputFile.Name; - break; - case TInputFileBig inputFileBig: - parts = inputFileBig.Parts; - name = inputFileBig.Name; - break; - } - - var r = await mediaHelper.SavePhotoAsync(input.ReqMsgId, - input.UserId, - file.GetFileId(), - obj.Video != null, - obj.VideoStartTs, - parts, - name, - md5 ?? string.Empty, - obj.VideoEmojiMarkup, - isProfilePhoto: true - ); - var command = new UploadProfilePhotoCommand(UserId.Create(input.UserId), - input.ToRequestInfo(), - r.PhotoId, - obj.Fallback, - obj.VideoEmojiMarkup - ); - await commandBus.PublishAsync(command); - - return null!; - } -} +using MongoDB.Bson; +using MongoDB.Driver; +using MyTelegram.Messenger.Converters.ConverterServices; + +namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Photos; + +internal sealed class UploadProfilePhotoHandler( + IMediaHelper mediaHelper, + ICommandBus commandBus, + IUserConverterService userConverterService, + IMongoDatabase mongo, + ILogger logger) + : RpcResultObjectHandler, + Photos.IUploadProfilePhotoHandler +{ + protected override async Task HandleCoreAsync(IRequestInput input, + MyTelegram.Schema.Photos.RequestUploadProfilePhoto obj) + { + logger.LogWarning("uploadProfilePhoto ENTER userId={UserId}", input.UserId); + + var file = obj.File ?? obj.Video; + if (file == null) + RpcErrors.RpcErrors400.PhotoFileMissing.ThrowRpcError(); + + var md5 = string.Empty; + var parts = 0; + var name = string.Empty; + switch (file) + { + case TInputFile inputFile: + md5 = inputFile.Md5Checksum; parts = inputFile.Parts; name = inputFile.Name; break; + case TInputFileBig inputFileBig: + parts = inputFileBig.Parts; name = inputFileBig.Name; break; + } + + var r = await mediaHelper.SavePhotoAsync(input.ReqMsgId, input.UserId, file.GetFileId(), + obj.Video != null, obj.VideoStartTs, parts, name, md5 ?? string.Empty, null, isProfilePhoto: true); + + try + { + await commandBus.PublishAsync(new UploadProfilePhotoCommand(UserId.Create(input.UserId), + input.ToRequestInfo(), r.PhotoId, obj.Fallback, null)); + } + catch (Exception ex) + { + logger.LogWarning(ex, "UploadProfilePhotoCommand failed for {PhotoId}", r.PhotoId); + } + + try + { + var collection = mongo.GetCollection("eventflow-userreadmodel"); + var filter = Builders.Filter.Eq("UserId", input.UserId); + var update = obj.Fallback + ? Builders.Update.Set("FallbackPhotoId", r.PhotoId) + : Builders.Update + .Set("ProfilePhotoId", r.PhotoId) + .Set("IsProfilePhoto", true) + .Set("ProfilePhotoUpdateDate", (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()) + .Set("LastUpdateDate", DateTime.UtcNow); + await collection.UpdateOneAsync(filter, update); + await mongo.GetCollection("eventflow-photoreadmodel").UpdateOneAsync( + Builders.Filter.Eq("PhotoId", r.PhotoId), + Builders.Update.Set("IsProfilePhoto", true).Set("UserId", input.UserId)); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to patch ProfilePhotoId for {UserId}", input.UserId); + } + + ILayeredUser user; + try + { + user = await userConverterService.GetUserAsync(input, input.UserId, layer: input.Layer); + user.Self = true; + } + catch + { + user = new TUser { Id = input.UserId, Self = true, FirstName = "" }; + } + + logger.LogWarning("uploadProfilePhoto OK userId={UserId} photoId={PhotoId}", input.UserId, r.PhotoId); + return new MyTelegram.Schema.Photos.TPhoto { Photo = r.Photo, Users = [user] }; + } +} diff --git a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Upload/GetFileHandler.cs b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Upload/GetFileHandler.cs index a45fdbde..b10905fa 100644 --- a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Upload/GetFileHandler.cs +++ b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Upload/GetFileHandler.cs @@ -154,31 +154,30 @@ internal sealed class GetFileHandler : RpcResultObjectHandler 0 or limit is specified - if (obj.Offset > 0 || obj.Limit < int.MaxValue) - { - var endByte = obj.Offset + obj.Limit - 1; - request.Headers.Add("Range", $"bytes={obj.Offset}-{endByte}"); - } - var response = await _httpClient.SendAsync(request, default); - + if (!response.IsSuccessStatusCode) { - _logger.LogWarning("Failed to load file from MinIO: {StatusCode}, URL: {Url}", + _logger.LogWarning("Failed to load file from MinIO: {StatusCode}, URL: {Url}", response.StatusCode, minioUrl); RpcErrors.RpcErrors400.FileIdInvalid.ThrowRpcError(); } - var fileData = await response.Content.ReadAsByteArrayAsync(default); - - _logger.LogInformation("File loaded successfully: {Size} bytes from {ObjectName}", - fileData.Length, objectName); + var fullData = await response.Content.ReadAsByteArrayAsync(default); + var offset = obj.Offset < 0 ? 0 : (int)Math.Min(obj.Offset, fullData.Length); + var remaining = Math.Max(0, fullData.Length - offset); + var take = obj.Limit <= 0 ? remaining : Math.Min(remaining, obj.Limit); + var fileData = take == fullData.Length && offset == 0 + ? fullData + : fullData.AsSpan(offset, take).ToArray(); + var complete = offset == 0 && fileData.Length == fullData.Length; + + _logger.LogInformation("File loaded successfully: {Size}/{Full} bytes from {ObjectName} offset={Offset} complete={Complete}", + fileData.Length, fullData.Length, objectName, offset, complete); return new MyTelegram.Schema.Upload.TFile { - Type = DetectFileType(fileData), + Type = complete ? DetectFileType(fullData) : new MyTelegram.Schema.Storage.TFilePartial(), Mtime = mtime, Bytes = fileData }; diff --git a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Users/GetFullUserHandler.cs b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Users/GetFullUserHandler.cs index be1a2d84..2a7cc4b8 100644 --- a/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Users/GetFullUserHandler.cs +++ b/source/src/MyTelegram.Messenger/Handlers/LatestLayer/Impl/Users/GetFullUserHandler.cs @@ -1,369 +1,369 @@ -using MyTelegram.Messenger.Services.Impl; -using TUserFull = MyTelegram.Schema.Users.TUserFull; - -namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Users; - -/// -/// Returns extended user info by ID. -/// Possible errors -/// Code Type Description -/// 400 CHANNEL_INVALID The provided channel is invalid. -/// 400 CHANNEL_PRIVATE You haven't joined this channel/supergroup. -/// 400 MSG_ID_INVALID Invalid message ID provided. -/// 400 USERNAME_OCCUPIED The provided username is already occupied. -/// 400 USER_ID_INVALID The provided user ID is invalid. -/// See -/// -internal sealed class GetFullUserHandler( - IPeerHelper peerHelper, - IQueryProcessor queryProcessor, - IUserConverterService userConverterService, - ILayeredService peerSettingsLayeredService, - ILayeredService peerNotifySettingsLayeredService, - IBlockCacheAppService blockCacheAppService, - IAccessHashHelper accessHashHelper, - IContactHelper contactHelper, - IPeerSettingsAppService peerSettingsAppService, - IPhotoAppService photoAppService, - IUserAppService userAppService, - IPrivacyAppService privacyAppService, - IStickerAppService stickerAppService, - ILogger logger) - : RpcResultObjectHandler, - Users.IGetFullUserHandler -{ - protected override async Task HandleCoreAsync(IRequestInput input, - MyTelegram.Schema.Users.RequestGetFullUser obj) - { - await accessHashHelper.CheckAccessHashAsync(input, obj.Id); - - var selfUserId = input.UserId; - var targetPeer = peerHelper.GetPeer(obj.Id, input.UserId); - var targetUserId = targetPeer.PeerId; - - var userReadModel = await userAppService.GetAsync(targetPeer.PeerId); - if (userReadModel == null) - { - //RpcErrors.RpcErrors400.UserIdInvalid.ThrowRpcError(); - throw new RpcException(RpcErrors.RpcErrors400.UserIdInvalid); - } - - // Завершаем сессию для удалённых или ограниченных пользователей - if (targetUserId == selfUserId && (userReadModel.IsDeleted == true || userReadModel.Restricted)) - { - logger.LogWarning("*** User {UserId} is deleted/restricted - kicking from session ***", targetUserId); - throw new RpcException(new RpcError(401, "AUTH_USER_DELETED")); - } - - var photoReadModels = await photoAppService.GetPhotosAsync(userReadModel); - var privacyReadModels = await privacyAppService.GetPrivacyListAsync(targetUserId); - var contactReadModels = - await queryProcessor.ProcessAsync(new GetContactListBySelfIdAndTargetUserIdQuery(input.UserId, targetUserId)); - - var myContactReadModel = - contactReadModels?.FirstOrDefault(p => p.SelfUserId == selfUserId && p.TargetUserId == targetUserId); - var targetUserContactReadModel = - contactReadModels?.FirstOrDefault(p => p.SelfUserId == targetUserId && p.TargetUserId == selfUserId); - - var peerNotifySettingsId = MyTelegram.Domain.Aggregates.PeerNotifySetting.PeerNotifySettingsId.Create(selfUserId, targetPeer.PeerType, targetPeer.PeerId); - var peerNotifySettingReadModel = - await queryProcessor.ProcessAsync(new MyTelegram.Queries.GetPeerNotifySettingsByIdQuery(peerNotifySettingsId.Value)); - var peerSettingReadModel = await peerSettingsAppService.GetPeerSettingsAsync(input.UserId, targetPeer.PeerId); - var contactType = contactHelper.GetContactType(myContactReadModel, targetUserContactReadModel);// await contactAppService.GetContactTypeAsync(input.UserId, targetPeer.PeerId); - - // Преобразуем доменный ContactType в ContactType схемы для peer settings - var schemaContactType = contactType switch - { - MyTelegram.ContactType.Self => MyTelegram.ContactType.Self, - MyTelegram.ContactType.TargetUserIsMyContact => MyTelegram.ContactType.TargetUserIsMyContact, - MyTelegram.ContactType.Mutual => MyTelegram.ContactType.Mutual, - MyTelegram.ContactType.TargetUserIsNotMyContact => MyTelegram.ContactType.TargetUserIsNotMyContact, - _ => MyTelegram.ContactType.None - }; - - var peerSettings = peerSettingsLayeredService.GetConverter(input.Layer).ToPeerSettings(input.UserId, - targetPeer.PeerId, - peerSettingReadModel, - schemaContactType - ); - var peerNotifySettings = peerNotifySettingsLayeredService.GetConverter(input.Layer) - .ToPeerNotifySettings(peerNotifySettingReadModel?.NotifySettings ?? PeerNotifySettings.DefaultSettings); - - var userFull = userConverterService.ToUserFull(input, userReadModel, photoReadModels, contactReadModels, - privacyReadModels, input.Layer); - - userFull.Settings = peerSettings; - userFull.NotifySettings = peerNotifySettings; - userFull.Blocked = await blockCacheAppService.IsBlockedAsync(input.UserId, targetPeer.PeerId); - userFull.TtlPeriod = userReadModel.DefaultHistoryTTL; - - // Проверяем, есть ли у пользователя сохранённые звёздные подарки, чтобы показать кнопку - var giftsCount = await GetStarGiftsCountAsync(targetPeer.PeerId); - var hasGifts = giftsCount > 0; - - - // Включаем кнопку подарков и задаём их количество - userFull.DisplayGiftsButton = hasGifts; - userFull.StargiftsCount = giftsCount > 0 ? giftsCount : null; // Задаём количество подарков - userFull.DisallowedGifts = null; // NULL заставляет клиент использовать DisplayGiftsButton для флага SendHide - - var user = userConverterService.ToUser(input, userReadModel, photoReadModels, myContactReadModel, - targetUserContactReadModel, privacyReadModels, input.Layer); - - await SetPersonalChannelAsync(input, userReadModel, userFull); - await SetCommonChatCountAsync(input, userReadModel, userFull); - await SetBotVerificationAsync(input, userReadModel, userFull); - - // Set saved music (Layer 213) - await SetSavedMusicAsync(input, userReadModel, userFull); - - // Предзагружаем документ emoji-статуса, если он есть - if (userReadModel.EmojiStatusDocumentId.HasValue) - { - // Инициируем загрузку документа запросом к нему, - // чтобы он закэшировался на стороне клиента - try - { - var emojiDoc = await queryProcessor.ProcessAsync( - new GetDocumentByIdQuery(userReadModel.EmojiStatusDocumentId.Value)); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to preload emoji document {DocumentId}", - userReadModel.EmojiStatusDocumentId.Value); - } - } - - // Загружаем и проставляем business intro, если он есть - await SetBusinessIntroAsync(input, userReadModel, userFull); - - // Флаги вычисляются автоматически - - var result = new TUserFull - { - Chats = [], - FullUser = userFull, - Users = new TVector(user) - }; - - return result; - } - - private async Task SetCommonChatCountAsync(IRequestInput input, IUserReadModel userReadModel, IUserFull userFull) - { - var count = await queryProcessor.ProcessAsync(new GetCommonChatCountQuery(input.UserId, userReadModel.UserId)); - userFull.CommonChatsCount = count; - } - - private async Task SetPersonalChannelAsync(IRequestInput input, IUserReadModel userReadModel, IUserFull userFull) - { - if (userReadModel.PersonalChannelId.HasValue) - { - var channelTopMessageId = - await queryProcessor.ProcessAsync( - new GetChannelTopMessageIdQuery(userReadModel.PersonalChannelId.Value)); - - if (channelTopMessageId.HasValue) - { - userFull.PersonalChannelId = userReadModel.PersonalChannelId; - userFull.PersonalChannelMessage = channelTopMessageId.Value; - } - } - } - - private async Task SetBotVerificationAsync(IRequestInput input, IUserReadModel userReadModel, IUserFull userFull) - { - - if (userReadModel.BotVerifierId.HasValue) - { - logger.LogWarning("Querying BotVerificationReadModel for TargetType=User, TargetId={TargetId}", userReadModel.UserId); - - var verification = await queryProcessor.ProcessAsync( - new GetBotVerificationByTargetQuery(VerificationTargetType.User, userReadModel.UserId), - default); - - logger.LogWarning("Query result: {HasVerification}, verification={Verification}", - verification != null, verification?.Description); - - if (verification != null) - { - logger.LogWarning("Setting BotVerification: BotId={BotId}, Icon={Icon}, Description={Description}", - verification.BotVerifierId, verification.IconEmojiId, verification.Description); - - // Используем CustomDescription, если он не пустой, иначе берём стандартный Description - var description = !string.IsNullOrWhiteSpace(verification.CustomDescription) - ? verification.CustomDescription - : verification.Description; - - userFull.BotVerification = new TBotVerification - { - BotId = verification.BotVerifierId, - Icon = verification.IconEmojiId, - Description = description - }; - - logger.LogWarning("BotVerification SET: BotId={BotId}, Icon={Icon}, Description=\"{Description}\" (CustomDescription=\"{Custom}\", length={Length})", - verification.BotVerifierId, verification.IconEmojiId, description, verification.CustomDescription ?? "null", description?.Length ?? 0); - - // Предзагружаем документ emoji-иконки верификации - try - { - var iconDoc = await queryProcessor.ProcessAsync( - new GetDocumentByIdQuery(verification.IconEmojiId)); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to preload verification icon document {DocumentId}", verification.IconEmojiId); - } - } - else - { - logger.LogWarning("Verification is NULL after query!"); - } - } - } - - private async Task SetSavedMusicAsync(IRequestInput input, IUserReadModel userReadModel, IUserFull userFull) - { - // Загружаем сохранённую музыку пользователя - var savedMusic = await queryProcessor.ProcessAsync( - new GetSavedMusicByUserIdQuery(userReadModel.UserId), - CancellationToken.None); - - if (savedMusic != null && savedMusic.DocumentIds.Count > 0) - { - // Берём первый документ - var firstDocumentId = savedMusic.DocumentIds[0]; - var document = await queryProcessor.ProcessAsync( - new GetDocumentByIdQuery(firstDocumentId), - CancellationToken.None); - - if (document != null) - { - userFull.SavedMusic = new TDocument - { - Id = document.DocumentId, - AccessHash = document.AccessHash, - FileReference = document.FileReference.IsEmpty ? Array.Empty() : document.FileReference.ToArray(), - Date = document.Date, - MimeType = document.MimeType ?? "audio/mpeg", - Size = document.Size, - DcId = document.DcId, - Attributes = document.Attributes2 != null - ? new TVector(document.Attributes2) - : new TVector() - }; - - logger.LogInformation( - "Set saved_music for user {UserId}: DocumentId={DocumentId}", - userReadModel.UserId, - firstDocumentId); - } - } - } - - - private async Task SetBusinessIntroAsync(IRequestInput input, IUserReadModel userReadModel, IUserFull userFull) - { - // if (userReadModel.BusinessIntro != null) - // { - // logger.LogInformation("Loading business intro for user {UserId}", userReadModel.UserId); - - IDocument? stickerDocument = null; - long stickerDocId = 0; - string title = userReadModel.BusinessIntro?.Title ?? string.Empty; - string description = userReadModel.BusinessIntro?.Description ?? string.Empty; - string stickerDocumentId = userReadModel.BusinessIntro?.StickerDocumentId ?? "0"; - - // Проверяем маркер "0" - признак динамического случайного стикера - if (stickerDocumentId == "0") - { - var randomId = await stickerAppService.GetRandomFeaturedStickerIdAsync("👋"); - if (randomId.HasValue) - { - stickerDocId = randomId.Value; - } - } - // Иначе загружаем конкретный документ стикера, если он указан - else if (!string.IsNullOrEmpty(stickerDocumentId)) - { - long.TryParse(stickerDocumentId, out stickerDocId); - } - - if (stickerDocId > 0) - { - try - { - var docReadModel = await queryProcessor.ProcessAsync( - new GetDocumentByIdQuery(stickerDocId), - CancellationToken.None); - - if (docReadModel != null) - { - stickerDocument = new TDocument - { - Id = docReadModel.DocumentId, - AccessHash = docReadModel.AccessHash, - FileReference = docReadModel.FileReference.IsEmpty ? Array.Empty() : docReadModel.FileReference.ToArray(), - Date = docReadModel.Date, - MimeType = docReadModel.MimeType ?? "application/x-tgsticker", - Size = docReadModel.Size, - DcId = docReadModel.DcId, - Attributes = docReadModel.Attributes2 != null - ? new TVector(docReadModel.Attributes2) - : new TVector() - }; - - // Если выбран случайный стикер, а заголовок пуст, подставляем в него эмодзи - if (stickerDocumentId == "0" && string.IsNullOrEmpty(title) && stickerDocument is TDocument tDoc) - { - var stickerAttribute = tDoc.Attributes.FirstOrDefault(p => p is MyTelegram.Schema.TDocumentAttributeSticker) as MyTelegram.Schema.TDocumentAttributeSticker; - if (stickerAttribute != null && !string.IsNullOrEmpty(stickerAttribute.Alt)) - { - title = stickerAttribute.Alt; - } - } - } - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to load business intro sticker document {DocumentId} for user {UserId}", - stickerDocId, userReadModel.UserId); - } - } - - if (stickerDocument != null || !string.IsNullOrEmpty(title) || !string.IsNullOrEmpty(description)) - { - userFull.BusinessIntro = new TBusinessIntro - { - Title = title, - Description = description, - Sticker = stickerDocument - }; - } - // } - } - - private async Task GetStarGiftsCountAsync(long userId) - { - try - { - var gifts = await queryProcessor.ProcessAsync( - new MyTelegram.Queries.StarGift.GetUserStarGiftsQuery(userId, "", 100)); // Берём больше подарков, чтобы точнее посчитать - - // Возвращаем количество из ответа - if (gifts is Schema.Payments.TUserStarGifts userStarGifts) - { - return userStarGifts.Count; - } - - return 0; - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to get star gifts count for user {UserId}", userId); - return 0; - } - } -} +using MyTelegram.Messenger.Services.Impl; +using TUserFull = MyTelegram.Schema.Users.TUserFull; + +namespace MyTelegram.Messenger.Handlers.LatestLayer.Impl.Users; + +/// +/// Returns extended user info by ID. +/// Possible errors +/// Code Type Description +/// 400 CHANNEL_INVALID The provided channel is invalid. +/// 400 CHANNEL_PRIVATE You haven't joined this channel/supergroup. +/// 400 MSG_ID_INVALID Invalid message ID provided. +/// 400 USERNAME_OCCUPIED The provided username is already occupied. +/// 400 USER_ID_INVALID The provided user ID is invalid. +/// See +/// +internal sealed class GetFullUserHandler( + IPeerHelper peerHelper, + IQueryProcessor queryProcessor, + IUserConverterService userConverterService, + ILayeredService peerSettingsLayeredService, + ILayeredService peerNotifySettingsLayeredService, + IBlockCacheAppService blockCacheAppService, + IAccessHashHelper accessHashHelper, + IContactHelper contactHelper, + IPeerSettingsAppService peerSettingsAppService, + IPhotoAppService photoAppService, + IUserAppService userAppService, + IPrivacyAppService privacyAppService, + IStickerAppService stickerAppService, + ILogger logger) + : RpcResultObjectHandler, + Users.IGetFullUserHandler +{ + protected override async Task HandleCoreAsync(IRequestInput input, + MyTelegram.Schema.Users.RequestGetFullUser obj) + { + await accessHashHelper.CheckAccessHashAsync(input, obj.Id); + + var selfUserId = input.UserId; + var targetPeer = peerHelper.GetPeer(obj.Id, input.UserId); + var targetUserId = targetPeer.PeerId; + + var userReadModel = await userAppService.GetAsync(targetPeer.PeerId); + if (userReadModel == null) + { + //RpcErrors.RpcErrors400.UserIdInvalid.ThrowRpcError(); + throw new RpcException(RpcErrors.RpcErrors400.UserIdInvalid); + } + + // Завершаем сессию для удалённых или ограниченных пользователей + if (targetUserId == selfUserId && (userReadModel.IsDeleted == true || userReadModel.Restricted)) + { + logger.LogWarning("*** User {UserId} is deleted/restricted - kicking from session ***", targetUserId); + throw new RpcException(new RpcError(401, "AUTH_USER_DELETED")); + } + + var photoReadModels = await photoAppService.GetPhotosAsync(userReadModel); + var privacyReadModels = await privacyAppService.GetPrivacyListAsync(targetUserId); + var contactReadModels = + await queryProcessor.ProcessAsync(new GetContactListBySelfIdAndTargetUserIdQuery(input.UserId, targetUserId)); + + var myContactReadModel = + contactReadModels?.FirstOrDefault(p => p.SelfUserId == selfUserId && p.TargetUserId == targetUserId); + var targetUserContactReadModel = + contactReadModels?.FirstOrDefault(p => p.SelfUserId == targetUserId && p.TargetUserId == selfUserId); + + var peerNotifySettingsId = MyTelegram.Domain.Aggregates.PeerNotifySetting.PeerNotifySettingsId.Create(selfUserId, targetPeer.PeerType, targetPeer.PeerId); + var peerNotifySettingReadModel = + await queryProcessor.ProcessAsync(new MyTelegram.Queries.GetPeerNotifySettingsByIdQuery(peerNotifySettingsId.Value)); + var peerSettingReadModel = await peerSettingsAppService.GetPeerSettingsAsync(input.UserId, targetPeer.PeerId); + var contactType = contactHelper.GetContactType(myContactReadModel, targetUserContactReadModel);// await contactAppService.GetContactTypeAsync(input.UserId, targetPeer.PeerId); + + // Преобразуем доменный ContactType в ContactType схемы для peer settings + var schemaContactType = contactType switch + { + MyTelegram.ContactType.Self => MyTelegram.ContactType.Self, + MyTelegram.ContactType.TargetUserIsMyContact => MyTelegram.ContactType.TargetUserIsMyContact, + MyTelegram.ContactType.Mutual => MyTelegram.ContactType.Mutual, + MyTelegram.ContactType.TargetUserIsNotMyContact => MyTelegram.ContactType.TargetUserIsNotMyContact, + _ => MyTelegram.ContactType.None + }; + + var peerSettings = peerSettingsLayeredService.GetConverter(input.Layer).ToPeerSettings(input.UserId, + targetPeer.PeerId, + peerSettingReadModel, + schemaContactType + ); + var peerNotifySettings = peerNotifySettingsLayeredService.GetConverter(input.Layer) + .ToPeerNotifySettings(peerNotifySettingReadModel?.NotifySettings ?? PeerNotifySettings.DefaultSettings); + + var userFull = userConverterService.ToUserFull(input, userReadModel, photoReadModels, contactReadModels, + privacyReadModels, input.Layer); + + userFull.Settings = peerSettings; + userFull.NotifySettings = peerNotifySettings; + userFull.Blocked = await blockCacheAppService.IsBlockedAsync(input.UserId, targetPeer.PeerId); + userFull.TtlPeriod = userReadModel.DefaultHistoryTTL; + + // Проверяем, есть ли у пользователя сохранённые звёздные подарки, чтобы показать кнопку + var giftsCount = await GetStarGiftsCountAsync(targetPeer.PeerId); + var hasGifts = giftsCount > 0; + + + // Включаем кнопку подарков и задаём их количество + userFull.DisplayGiftsButton = hasGifts; + userFull.StargiftsCount = giftsCount > 0 ? giftsCount : null; // Задаём количество подарков + userFull.DisallowedGifts = null; // NULL заставляет клиент использовать DisplayGiftsButton для флага SendHide + + var user = userConverterService.ToUser(input, userReadModel, photoReadModels, myContactReadModel, + targetUserContactReadModel, privacyReadModels, input.Layer); + + await SetPersonalChannelAsync(input, userReadModel, userFull); + await SetCommonChatCountAsync(input, userReadModel, userFull); + await SetBotVerificationAsync(input, userReadModel, userFull); + + // Set saved music (Layer 213) + await SetSavedMusicAsync(input, userReadModel, userFull); + + // Предзагружаем документ emoji-статуса, если он есть + if (userReadModel.EmojiStatusDocumentId.HasValue) + { + // Инициируем загрузку документа запросом к нему, + // чтобы он закэшировался на стороне клиента + try + { + var emojiDoc = await queryProcessor.ProcessAsync( + new GetDocumentByIdQuery(userReadModel.EmojiStatusDocumentId.Value)); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to preload emoji document {DocumentId}", + userReadModel.EmojiStatusDocumentId.Value); + } + } + + // Загружаем и проставляем business intro, если он есть + await SetBusinessIntroAsync(input, userReadModel, userFull); + + // Флаги вычисляются автоматически + + var result = new TUserFull + { + Chats = [], + FullUser = userFull, + Users = new TVector(user) + }; + + return result; + } + + private async Task SetCommonChatCountAsync(IRequestInput input, IUserReadModel userReadModel, IUserFull userFull) + { + var count = await queryProcessor.ProcessAsync(new GetCommonChatCountQuery(input.UserId, userReadModel.UserId)); + userFull.CommonChatsCount = count; + } + + private async Task SetPersonalChannelAsync(IRequestInput input, IUserReadModel userReadModel, IUserFull userFull) + { + if (userReadModel.PersonalChannelId.HasValue) + { + var channelTopMessageId = + await queryProcessor.ProcessAsync( + new GetChannelTopMessageIdQuery(userReadModel.PersonalChannelId.Value)); + + if (channelTopMessageId.HasValue) + { + userFull.PersonalChannelId = userReadModel.PersonalChannelId; + userFull.PersonalChannelMessage = channelTopMessageId.Value; + } + } + } + + private async Task SetBotVerificationAsync(IRequestInput input, IUserReadModel userReadModel, IUserFull userFull) + { + + if (userReadModel.BotVerifierId.HasValue) + { + logger.LogWarning("Querying BotVerificationReadModel for TargetType=User, TargetId={TargetId}", userReadModel.UserId); + + var verification = await queryProcessor.ProcessAsync( + new GetBotVerificationByTargetQuery(VerificationTargetType.User, userReadModel.UserId), + default); + + logger.LogWarning("Query result: {HasVerification}, verification={Verification}", + verification != null, verification?.Description); + + if (verification != null) + { + logger.LogWarning("Setting BotVerification: BotId={BotId}, Icon={Icon}, Description={Description}", + verification.BotVerifierId, verification.IconEmojiId, verification.Description); + + // Используем CustomDescription, если он не пустой, иначе берём стандартный Description + var description = !string.IsNullOrWhiteSpace(verification.CustomDescription) + ? verification.CustomDescription + : verification.Description; + + userFull.BotVerification = new TBotVerification + { + BotId = verification.BotVerifierId, + Icon = verification.IconEmojiId, + Description = description + }; + + logger.LogWarning("BotVerification SET: BotId={BotId}, Icon={Icon}, Description=\"{Description}\" (CustomDescription=\"{Custom}\", length={Length})", + verification.BotVerifierId, verification.IconEmojiId, description, verification.CustomDescription ?? "null", description?.Length ?? 0); + + // Предзагружаем документ emoji-иконки верификации + try + { + var iconDoc = await queryProcessor.ProcessAsync( + new GetDocumentByIdQuery(verification.IconEmojiId)); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to preload verification icon document {DocumentId}", verification.IconEmojiId); + } + } + else + { + logger.LogWarning("Verification is NULL after query!"); + } + } + } + + private async Task SetSavedMusicAsync(IRequestInput input, IUserReadModel userReadModel, IUserFull userFull) + { + // Загружаем сохранённую музыку пользователя + var savedMusic = await queryProcessor.ProcessAsync( + new GetSavedMusicByUserIdQuery(userReadModel.UserId), + CancellationToken.None); + + if (savedMusic != null && savedMusic.DocumentIds.Count > 0) + { + // Берём первый документ + var firstDocumentId = savedMusic.DocumentIds[0]; + var document = await queryProcessor.ProcessAsync( + new GetDocumentByIdQuery(firstDocumentId), + CancellationToken.None); + + if (document != null) + { + userFull.SavedMusic = new TDocument + { + Id = document.DocumentId, + AccessHash = document.AccessHash, + FileReference = document.FileReference.IsEmpty ? Array.Empty() : document.FileReference.ToArray(), + Date = document.Date, + MimeType = document.MimeType ?? "audio/mpeg", + Size = document.Size, + DcId = document.DcId, + Attributes = document.Attributes2 != null + ? new TVector(document.Attributes2) + : new TVector() + }; + + logger.LogInformation( + "Set saved_music for user {UserId}: DocumentId={DocumentId}", + userReadModel.UserId, + firstDocumentId); + } + } + } + + + private async Task SetBusinessIntroAsync(IRequestInput input, IUserReadModel userReadModel, IUserFull userFull) + { + // if (userReadModel.BusinessIntro != null) + // { + // logger.LogInformation("Loading business intro for user {UserId}", userReadModel.UserId); + + IDocument? stickerDocument = null; + long stickerDocId = 0; + string title = userReadModel.BusinessIntro?.Title ?? string.Empty; + string description = userReadModel.BusinessIntro?.Description ?? string.Empty; + string stickerDocumentId = userReadModel.BusinessIntro?.StickerDocumentId ?? "0"; + + // Проверяем маркер "0" - признак динамического случайного стикера + if (stickerDocumentId == "0") + { + var randomId = await stickerAppService.GetRandomFeaturedStickerIdAsync("👋"); + if (randomId.HasValue) + { + stickerDocId = randomId.Value; + } + } + // Иначе загружаем конкретный документ стикера, если он указан + else if (!string.IsNullOrEmpty(stickerDocumentId)) + { + long.TryParse(stickerDocumentId, out stickerDocId); + } + + if (stickerDocId > 0) + { + try + { + var docReadModel = await queryProcessor.ProcessAsync( + new GetDocumentByIdQuery(stickerDocId), + CancellationToken.None); + + if (docReadModel != null) + { + stickerDocument = new TDocument + { + Id = docReadModel.DocumentId, + AccessHash = docReadModel.AccessHash, + FileReference = docReadModel.FileReference.IsEmpty ? Array.Empty() : docReadModel.FileReference.ToArray(), + Date = docReadModel.Date, + MimeType = docReadModel.MimeType ?? "application/x-tgsticker", + Size = docReadModel.Size, + DcId = docReadModel.DcId, + Attributes = docReadModel.Attributes2 != null + ? new TVector(docReadModel.Attributes2) + : new TVector() + }; + + // Если выбран случайный стикер, а заголовок пуст, подставляем в него эмодзи + if (stickerDocumentId == "0" && string.IsNullOrEmpty(title) && stickerDocument is TDocument tDoc) + { + var stickerAttribute = tDoc.Attributes.FirstOrDefault(p => p is MyTelegram.Schema.TDocumentAttributeSticker) as MyTelegram.Schema.TDocumentAttributeSticker; + if (stickerAttribute != null && !string.IsNullOrEmpty(stickerAttribute.Alt)) + { + title = stickerAttribute.Alt; + } + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to load business intro sticker document {DocumentId} for user {UserId}", + stickerDocId, userReadModel.UserId); + } + } + + if (stickerDocument != null || !string.IsNullOrEmpty(title) || !string.IsNullOrEmpty(description)) + { + userFull.BusinessIntro = new TBusinessIntro + { + Title = title, + Description = description, + Sticker = stickerDocument + }; + } + // } + } + + private async Task GetStarGiftsCountAsync(long userId) + { + try + { + var gifts = await queryProcessor.ProcessAsync( + new MyTelegram.Queries.StarGift.GetUserStarGiftsQuery(userId, "", 100)); // Берём больше подарков, чтобы точнее посчитать + + // Возвращаем количество из ответа + if (gifts is Schema.Payments.TUserStarGifts userStarGifts) + { + return userStarGifts.Count; + } + + return 0; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to get star gifts count for user {UserId}", userId); + return 0; + } + } +} diff --git a/source/src/MyTelegram.Messenger/Services/Impl/FilePartManager.cs b/source/src/MyTelegram.Messenger/Services/Impl/FilePartManager.cs index a1742d09..03a8bd82 100644 --- a/source/src/MyTelegram.Messenger/Services/Impl/FilePartManager.cs +++ b/source/src/MyTelegram.Messenger/Services/Impl/FilePartManager.cs @@ -90,13 +90,33 @@ public class FilePartManager( } var ext = Path.GetExtension(safeName); - var merged = Path.Combine(_uploadRoot, "merged", $"{userId}-{fileId}{ext}"); - using (var output = File.Create(merged)) + var mergedDir = Path.Combine(_uploadRoot, "merged"); + try { Directory.CreateDirectory(mergedDir); } catch { /* ignore */ } + var merged = Path.Combine(mergedDir, $"{userId}-{fileId}{ext}"); + try { - foreach (var part in partPaths) + using (var output = File.Create(merged)) { - using var input = File.OpenRead(part); - input.CopyTo(output); + foreach (var part in partPaths) + { + using var input = File.OpenRead(part); + input.CopyTo(output); + } + } + } + catch (UnauthorizedAccessException) + { + mergedDir = Path.Combine(Path.GetTempPath(), "niigram-merged"); + Directory.CreateDirectory(mergedDir); + merged = Path.Combine(mergedDir, $"{userId}-{fileId}{ext}"); + logger.LogWarning("uploads/merged not writable, using {Merged}", merged); + using (var output = File.Create(merged)) + { + foreach (var part in partPaths) + { + using var input = File.OpenRead(part); + input.CopyTo(output); + } } } diff --git a/source/src/MyTelegram.Messenger/Services/Impl/LocalMediaStore.cs b/source/src/MyTelegram.Messenger/Services/Impl/LocalMediaStore.cs index e8a50d8b..8cf5aea3 100644 --- a/source/src/MyTelegram.Messenger/Services/Impl/LocalMediaStore.cs +++ b/source/src/MyTelegram.Messenger/Services/Impl/LocalMediaStore.cs @@ -56,7 +56,9 @@ public class LocalMediaStore( var fileRef = RandomNumberGenerator.GetBytes(16); await PutMinioAsync(photoId.ToString(), data, mime); - var size = new TPhotoSize { Type = "x", W = w, H = h, Size = data.Length }; + var sizeX = new TPhotoSize { Type = "x", W = w, H = h, Size = data.Length }; + var sizeM = new TPhotoSize { Type = "m", W = Math.Min(w, 320), H = Math.Max(1, h * Math.Min(w, 320) / Math.Max(w, 1)), Size = data.Length }; + var sizeS = new TPhotoSize { Type = "s", W = Math.Min(w, 100), H = Math.Max(1, h * Math.Min(w, 100) / Math.Max(w, 1)), Size = data.Length }; var photo = new TPhoto { Id = photoId, @@ -64,7 +66,7 @@ public class LocalMediaStore( FileReference = fileRef, Date = date, DcId = 1, - Sizes = [size] + Sizes = [sizeS, sizeM, sizeX] }; try @@ -73,8 +75,12 @@ public class LocalMediaStore( PhotoId.Create(photoId), userId, new PhotoItem(photoId, accessHash, fileRef, date, 1, data.Length, - Sizes: [new PhotoSize(w, h, data.Length, "x")], - Sizes2: [size], + Sizes: [ + new PhotoSize(sizeS.W, sizeS.H, data.Length, "s"), + new PhotoSize(sizeM.W, sizeM.H, data.Length, "m"), + new PhotoSize(w, h, data.Length, "x") + ], + Sizes2: [sizeS, sizeM, sizeX], IsProfilePhoto: isProfilePhoto))); } catch (Exception ex) @@ -120,7 +126,7 @@ public class LocalMediaStore( var isRound = attrs.OfType().Any(v => v.RoundMessage); var isVideo = attrs.OfType().Any() || mime.StartsWith("video/", StringComparison.OrdinalIgnoreCase); - await UpsertDocumentAsync(documentId, accessHash, date, fileRef, mime, data.LongLength, name, md5, userId); + await UpsertDocumentAsync(documentId, accessHash, date, fileRef, mime, data.LongLength, name, md5, userId, attrs); var tDocument = new TDocument { @@ -212,12 +218,18 @@ public class LocalMediaStore( } private async Task UpsertDocumentAsync(long documentId, long accessHash, int date, byte[] fileRef, - string mime, long size, string? name, string? md5, long userId) + string mime, long size, string? name, string? md5, long userId, TVector? attributes = null) { try { var collection = mongo.GetCollection("ReadModel-DocumentReadModel"); var filter = Builders.Filter.Eq("DocumentId", documentId); + var attrs2 = new BsonArray(); + foreach (var attr in attributes ?? []) + { + var b = AttrToBson(attr); + if (b != null) attrs2.Add(b); + } var doc = new BsonDocument { { "_id", $"document-{documentId}" }, @@ -231,6 +243,7 @@ public class LocalMediaStore( { "Md5CheckSum", md5 ?? "" }, { "CreatorId", userId }, { "FileReference", new BsonBinaryData(fileRef) }, + { "Attributes2", attrs2 }, }; await collection.ReplaceOneAsync(filter, doc, new ReplaceOptions { IsUpsert = true }); } @@ -240,6 +253,47 @@ public class LocalMediaStore( } } + private static BsonDocument? AttrToBson(IDocumentAttribute attr) => attr switch + { + TDocumentAttributeAudio a => new BsonDocument + { + { "_t", "TDocumentAttributeAudio" }, + { "Voice", a.Voice }, + { "Duration", a.Duration }, + { "Title", a.Title ?? "" }, + { "Performer", a.Performer ?? "" }, + { "Waveform", a.Waveform.HasValue ? new BsonBinaryData(a.Waveform.Value.ToArray()) : BsonNull.Value } + }, + TDocumentAttributeFilename f => new BsonDocument + { + { "_t", "TDocumentAttributeFilename" }, + { "FileName", f.FileName ?? "file" } + }, + TDocumentAttributeVideo v => new BsonDocument + { + { "_t", "TDocumentAttributeVideo" }, + { "RoundMessage", v.RoundMessage }, + { "SupportsStreaming", v.SupportsStreaming }, + { "Duration", v.Duration }, + { "W", v.W }, + { "H", v.H } + }, + TDocumentAttributeImageSize s => new BsonDocument + { + { "_t", "TDocumentAttributeImageSize" }, + { "W", s.W }, + { "H", s.H } + }, + TDocumentAttributeSticker st => new BsonDocument + { + { "_t", "TDocumentAttributeSticker" }, + { "Mask", st.Mask }, + { "Alt", st.Alt ?? "" } + }, + TDocumentAttributeAnimated => new BsonDocument { { "_t", "TDocumentAttributeAnimated" } }, + _ => new BsonDocument { { "_t", attr.GetType().Name } } + }; + private static (long FileId, int Parts, string Name, string? Md5) GetFileInfo(IInputFile file) => file switch { diff --git a/source/src/MyTelegram.Messenger/Services/Impl/PrivacyAppService.cs b/source/src/MyTelegram.Messenger/Services/Impl/PrivacyAppService.cs index a7d8be2d..45e429d6 100644 --- a/source/src/MyTelegram.Messenger/Services/Impl/PrivacyAppService.cs +++ b/source/src/MyTelegram.Messenger/Services/Impl/PrivacyAppService.cs @@ -47,12 +47,12 @@ public class PrivacyAppService( var privacyReadModels = await queryProcessor.ProcessAsync(new GetPrivacyByUserIdQuery(userId, key)); var privacyValueDataList = privacyReadModels.Select(p => p.PrivacyValueDataList).FirstOrDefault(); - // If no privacy settings found, return default "Nobody" (most restrictive) + // Telegram default when unset: allow everyone (esp. voice messages). if (privacyValueDataList == null || privacyValueDataList.Count == 0) { privacyValueDataList = new List { - new PrivacyValueData(PrivacyValueType.DisallowAll, null) + new PrivacyValueData(PrivacyValueType.AllowAll, null) }; } diff --git a/source/src/MyTelegram.Schema/LatestLayer/Account/RequestSetMainProfileTab.cs b/source/src/MyTelegram.Schema/LatestLayer/Account/RequestSetMainProfileTab.cs index fd5ac18c..1d34bf91 100644 --- a/source/src/MyTelegram.Schema/LatestLayer/Account/RequestSetMainProfileTab.cs +++ b/source/src/MyTelegram.Schema/LatestLayer/Account/RequestSetMainProfileTab.cs @@ -6,10 +6,10 @@ namespace MyTelegram.Schema.Account; /// Changes the main profile tab of the current user /// See /// -[TlObject(0x788d7fe3)] +[TlObject(0x5dee78b0)] public class RequestSetMainProfileTab : IRequest { - public uint ConstructorId => 0x788d7fe3; + public uint ConstructorId => 0x5dee78b0; /// /// The profile tab to set as main diff --git a/source/src/MyTelegram.Schema/LatestLayer/Entities/InputPrivacyKey/TInputPrivacyKeySavedMusic.cs b/source/src/MyTelegram.Schema/LatestLayer/Entities/InputPrivacyKey/TInputPrivacyKeySavedMusic.cs new file mode 100644 index 00000000..e5d747d3 --- /dev/null +++ b/source/src/MyTelegram.Schema/LatestLayer/Entities/InputPrivacyKey/TInputPrivacyKeySavedMusic.cs @@ -0,0 +1,14 @@ +// +namespace MyTelegram.Schema; + +[TlObject(0x4dbe9226)] +public sealed class TInputPrivacyKeySavedMusic : IInputPrivacyKey +{ + public uint ConstructorId => 0x4dbe9226; + public void ComputeFlag() { } + public void Serialize(IBufferWriter writer) + { + writer.Write(ConstructorId); + } + public void Deserialize(ref ReadOnlyMemory buffer) { } +} diff --git a/source/src/MyTelegram.Schema/LatestLayer/Entities/MessagesFilter/TInputMessagesFilterPoll.cs b/source/src/MyTelegram.Schema/LatestLayer/Entities/MessagesFilter/TInputMessagesFilterPoll.cs new file mode 100644 index 00000000..c18559a1 --- /dev/null +++ b/source/src/MyTelegram.Schema/LatestLayer/Entities/MessagesFilter/TInputMessagesFilterPoll.cs @@ -0,0 +1,14 @@ +// +namespace MyTelegram.Schema; + +[TlObject(0xfa2bc90a)] +public sealed class TInputMessagesFilterPoll : IMessagesFilter +{ + public uint ConstructorId => 0xfa2bc90a; + public void ComputeFlag() { } + public void Serialize(IBufferWriter writer) + { + writer.Write(ConstructorId); + } + public void Deserialize(ref ReadOnlyMemory buffer) { } +} diff --git a/source/src/MyTelegram.Schema/LatestLayer/Entities/PhoneConnection/TPhoneConnectionWebrtc.cs b/source/src/MyTelegram.Schema/LatestLayer/Entities/PhoneConnection/TPhoneConnectionWebrtc.cs index 8d051297..003d4196 100644 --- a/source/src/MyTelegram.Schema/LatestLayer/Entities/PhoneConnection/TPhoneConnectionWebrtc.cs +++ b/source/src/MyTelegram.Schema/LatestLayer/Entities/PhoneConnection/TPhoneConnectionWebrtc.cs @@ -1,93 +1,97 @@ -// -// ReSharper disable All - -namespace MyTelegram.Schema; - -/// -/// WebRTC connection parameters -/// See -/// -[TlObject(0x635fe375)] -public sealed class TPhoneConnectionWebrtc : IPhoneConnection -{ - public uint ConstructorId => 0x635fe375; - /// - /// Flags, see TL conditional fields - /// - public int Flags { get; set; } - - /// - /// Whether this is a TURN endpoint - /// See - /// - public bool Turn { get; set; } - - /// - /// Whether this is a STUN endpoint - /// See - /// - public bool Stun { get; set; } - - /// - /// Endpoint ID - /// - public long Id { get; set; } - - /// - /// IP address - /// - public string Ip { get; set; } - - /// - /// IPv6 address - /// - public string Ipv6 { get; set; } - - /// - /// Port - /// - public int Port { get; set; } - - /// - /// Username - /// - public string Username { get; set; } - - /// - /// Password - /// - public string Password { get; set; } - - public void ComputeFlag() - { - if (Turn) { Flags = Flags.SetBit(0); } - if (Stun) { Flags = Flags.SetBit(1); } - - } - - public void Serialize(IBufferWriter writer) - { - ComputeFlag(); - writer.Write(ConstructorId); - writer.Write(Flags); - writer.Write(Id); - writer.Write(Ip); - writer.Write(Ipv6); - writer.Write(Port); - writer.Write(Username); - writer.Write(Password); - } - - public void Deserialize(ref ReadOnlyMemory buffer) - { - Flags = buffer.ReadInt32(); - if (Flags.IsBitSet(0)) { Turn = true; } - if (Flags.IsBitSet(1)) { Stun = true; } - Id = buffer.ReadInt64(); - Ip = buffer.ReadString(); - Ipv6 = buffer.ReadString(); - Port = buffer.ReadInt32(); - Username = buffer.ReadString(); - Password = buffer.ReadString(); - } +// +// ReSharper disable All + +namespace MyTelegram.Schema; + +/// +/// WebRTC connection parameters +/// See +/// +[TlObject(0x635fe375)] +public sealed class TPhoneConnectionWebrtc : IPhoneConnection +{ + public uint ConstructorId => 0x635fe375; + /// + /// Flags, see TL conditional fields + /// + public int Flags { get; set; } + + /// + /// Whether this is a TURN endpoint + /// See + /// + public bool Turn { get; set; } + + /// + /// Whether this is a STUN endpoint + /// See + /// + public bool Stun { get; set; } + + /// + /// Endpoint ID + /// + public long Id { get; set; } + + /// + /// IP address + /// + public string Ip { get; set; } + + /// + /// IPv6 address + /// + public string Ipv6 { get; set; } + + /// + /// Port + /// + public int Port { get; set; } + + /// + /// Username + /// + public string Username { get; set; } + + /// + /// Password + /// + public string Password { get; set; } + + public void ComputeFlag() + { + Flags = 0; + if (Turn) { Flags = Flags.SetBit(0); } + if (Stun) { Flags = Flags.SetBit(1); } + if (!string.IsNullOrEmpty(Username) || !string.IsNullOrEmpty(Password)) + { + Flags = Flags.SetBit(2); + } + } + + public void Serialize(IBufferWriter writer) + { + ComputeFlag(); + writer.Write(ConstructorId); + writer.Write(Flags); + writer.Write(Id); + writer.Write(Ip); + writer.Write(Ipv6); + writer.Write(Port); + writer.Write(Username ?? ""); + writer.Write(Password ?? ""); + } + + public void Deserialize(ref ReadOnlyMemory buffer) + { + Flags = buffer.ReadInt32(); + if (Flags.IsBitSet(0)) { Turn = true; } + if (Flags.IsBitSet(1)) { Stun = true; } + Id = buffer.ReadInt64(); + Ip = buffer.ReadString(); + Ipv6 = buffer.ReadString(); + Port = buffer.ReadInt32(); + Username = buffer.ReadString(); + Password = buffer.ReadString(); + } } \ No newline at end of file diff --git a/source/src/MyTelegram.Schema/LatestLayer/Entities/User/TUser.cs b/source/src/MyTelegram.Schema/LatestLayer/Entities/User/TUser.cs index f1e6a287..0b14e619 100644 --- a/source/src/MyTelegram.Schema/LatestLayer/Entities/User/TUser.cs +++ b/source/src/MyTelegram.Schema/LatestLayer/Entities/User/TUser.cs @@ -1,397 +1,399 @@ -// -// ReSharper disable All - -namespace MyTelegram.Schema; - -/// -/// Indicates info about a certain user.Unless specified otherwise, when updating the local peer database, all fields from the newly received constructor take priority over the old constructor cached locally (including by removing fields that aren't set in the new constructor).See here » for an implementation of the logic to use when updating the local user peer database. -/// See -/// -[TlObject(0x31774388)] -public sealed class TUser : IUser, ILayeredUser -{ - public uint ConstructorId => 0x31774388; - /// - /// Flags, see TL conditional fields - /// - public int Flags { get; set; } - - /// - /// Whether this user indicates the currently logged in user - /// See - /// - public bool Self { get; set; } - - /// - /// Whether this user is a contact
When updating the
local peer database, do not apply changes to this field if the min flag is set. - /// See - ///
- public bool Contact { get; set; } - - /// - /// Whether this user is a mutual contact.
When updating the
local peer database, do not apply changes to this field if the min flag is set. - /// See - ///
- public bool MutualContact { get; set; } - - /// - /// Whether the account of this user was deleted.
Changes to this flag should invalidate the local
userFull cache for this user ID, see here » for more info. - /// See - ///
- public bool Deleted { get; set; } - - /// - /// Is this user a bot?
Changes to this flag should invalidate the local
userFull cache for this user ID, see here » for more info. - /// See - ///
- public bool Bot { get; set; } - - /// - /// Can the bot see all messages in groups? - /// See - /// - public bool BotChatHistory { get; set; } - - /// - /// Can the bot be added to groups? - /// See - /// - public bool BotNochats { get; set; } - - /// - /// Whether this user is verified - /// See - /// - public bool Verified { get; set; } - - /// - /// Access to this user must be restricted for the reason specified in restriction_reason - /// See - /// - public bool Restricted { get; set; } - - /// - /// See min - /// See - /// - public bool Min { get; set; } - - /// - /// Whether the bot can request our geolocation in inline mode - /// See - /// - public bool BotInlineGeo { get; set; } - - /// - /// Whether this is an official support user - /// See - /// - public bool Support { get; set; } - - /// - /// This may be a scam user - /// See - /// - public bool Scam { get; set; } - - /// - /// If set and min is set, the value of photo can be used to update the local database, see the documentation of that flag for more info. - /// See - /// - public bool ApplyMinPhoto { get; set; } - - /// - /// If set, this user was reported by many users as a fake or scam user: be careful when interacting with them. - /// See - /// - public bool Fake { get; set; } - - /// - /// Whether this bot offers an attachment menu web app - /// See - /// - public bool BotAttachMenu { get; set; } - - /// - /// Whether this user is a Telegram Premium user
Changes to this flag should invalidate the local
userFull cache for this user ID, see here » for more info.
Changes to this flag if the self flag is set should also trigger the following calls, to refresh the respective caches:
- The help.getConfig cache
- The messages.getTopReactions cache if the bot flag is not set - /// See - ///
- public bool Premium { get; set; } - - /// - /// Whether we installed the attachment menu web app offered by this bot.
When updating the local peer database, do not apply changes to this field if the min flag is set. - /// See - ///
- public bool AttachMenuEnabled { get; set; } - - /// - /// Flags, see TL conditional fields - /// - public int Flags2 { get; set; } - - /// - /// Whether we can edit the profile picture, name, about text and description of this bot because we own it.
When updating the local peer database, do not apply changes to this field if the min flag is set.
Changes to this flag (if min is not set) should invalidate the local userFull cache for this user ID. - /// See - ///
- public bool BotCanEdit { get; set; } - - /// - /// Whether we marked this user as a close friend, see here » for more info.
When updating the local peer database, do not apply changes to this field if the min flag is set. - /// See - ///
- public bool CloseFriend { get; set; } - - /// - /// Whether we have hidden » all active stories of this user.
When updating the local peer database, do not apply changes to this field if the min flag is set. - /// See - ///
- public bool StoriesHidden { get; set; } - - /// - /// No stories from this user are visible. - /// See - /// - public bool StoriesUnavailable { get; set; } - - /// - /// If set, we can only write to this user if they have already sent some messages to us, if we are subscribed to Telegram Premium, or if they're a mutual contact (user.mutual_contact).
All the secondary conditions listed above must be checked separately to verify whether we can still write to the user, even if this flag is set (i.e. a mutual contact will have this flag set even if we can still write to them, and so on...); to avoid doing these extra checks if we haven't yet cached all the required information (for example while displaying the chat list in the sharing UI) the users.getIsPremiumRequiredToContact method may be invoked instead, passing the list of users currently visible in the UI, returning a list of booleans that directly specify whether we can or cannot write to each user; alternatively, the userFull.contact_require_premium flag contains the same (fully checked, i.e. it's not just a copy of this flag) info returned by users.getIsPremiumRequiredToContact.
To set this flag for ourselves invoke account.setGlobalPrivacySettings, setting the settings.new_noncontact_peers_require_premium flag. - /// See - ///
- public bool ContactRequirePremium { get; set; } - - /// - /// Whether this bot can be connected to a user as specified here ». - /// See - /// - public bool BotBusiness { get; set; } - - /// - /// If set, this bot has configured a Main Mini App ». - /// See - /// - public bool BotHasMainApp { get; set; } - - /// - /// ID of the user, see here » for more info. - /// - public long Id { get; set; } - - /// - /// Access hash of the user, see here » for more info.
If this flag is set, when updating the local peer database, generate a virtual flag called min_access_hash, which is:
- Set to true if min is set AND
-- The phone flag is not set OR
-- The phone flag is set and the associated phone number string is non-empty
- Set to false otherwise.

Then, apply both access_hash and min_access_hash to the local database if:
- min_access_hash is false OR
- min_access_hash is true AND
-- There is no locally cached object for this user OR
-- There is no access_hash in the local cache OR
-- The cached object's min_access_hash is also true

If the final merged object stored to the database has the min_access_hash field set to true, the related access_hash is only suitable to use in inputPeerPhotoFileLocation », to directly download the profile pictures of users, everywhere else a inputPeer*FromMessage constructor will have to be generated as specified here ».
Bots can also use min access hashes in some conditions, by passing 0 instead of the min access hash. - ///
- public long? AccessHash { get; set; } - - /// - /// First name.
When updating the local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The min flag of the locally cached user entry is set. - ///
- public string? FirstName { get; set; } - - /// - /// Last name.
When updating the local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The min flag of the locally cached user entry is set. - ///
- public string? LastName { get; set; } - - /// - /// Main active username.
When updating the local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The min flag of the locally cached user entry is set.
Changes to this flag should invalidate the local userFull cache for this user ID if the above conditions are respected and the bot_can_edit flag is also set. - ///
- public string? Username { get; set; } - - /// - /// Phone number.
When updating the local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The min flag of the locally cached user entry is set. - ///
- public string? Phone { get; set; } - - /// - /// Profile picture of user.
When updating the local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The apply_min_photo flag is set OR
-- The min flag of the locally cached user entry is set. - /// See - ///
- public MyTelegram.Schema.IUserProfilePhoto? Photo { get; set; } - - /// - /// Online status of user.
When updating the
local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The min flag of the locally cached user entry is set OR
-- The locally cached user entry is equal to userStatusEmpty. - /// See - ///
- public MyTelegram.Schema.IUserStatus? Status { get; set; } - - /// - /// Version of the bot_info field in userFull, incremented every time it changes.
Changes to this flag should invalidate the local userFull cache for this user ID, see here » for more info. - ///
- public int? BotInfoVersion { get; set; } - - /// - /// Contains the reason why access to this user must be restricted. - /// - public TVector? RestrictionReason { get; set; } - - /// - /// Inline placeholder for this inline bot - /// - public string? BotInlinePlaceholder { get; set; } - - /// - /// Language code of the user - /// - public string? LangCode { get; set; } - - /// - /// Emoji status - /// See - /// - public MyTelegram.Schema.IEmojiStatus? EmojiStatus { get; set; } - - /// - /// Additional usernames.
When updating the
local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The min flag of the locally cached user entry is set.
Changes to this flag (if the above conditions are respected) should invalidate the local userFull cache for this user ID. - ///
- public TVector? Usernames { get; set; } - - /// - /// ID of the maximum read story.
When updating the local peer database, do not apply changes to this field if the min flag of the incoming constructor is set. - ///
- public int? StoriesMaxId { get; set; } - - /// - /// The user's accent color. - /// See - /// - public MyTelegram.Schema.IPeerColor? Color { get; set; } - - /// - /// The user's profile color. - /// See - /// - public MyTelegram.Schema.IPeerColor? ProfileColor { get; set; } - - /// - /// Monthly Active Users (MAU) of this bot (may be absent for small bots). - /// - public int? BotActiveUsers { get; set; } - public long? BotVerificationIcon { get; set; } - public long? SendPaidMessagesStars { get; set; } - - public void ComputeFlag() - { - if (Self) { Flags = Flags.SetBit(10); } - if (Contact) { Flags = Flags.SetBit(11); } - if (MutualContact) { Flags = Flags.SetBit(12); } - if (Deleted) { Flags = Flags.SetBit(13); } - if (Bot) { Flags = Flags.SetBit(14); } - if (BotChatHistory) { Flags = Flags.SetBit(15); } - if (BotNochats) { Flags = Flags.SetBit(16); } - if (Verified) { Flags = Flags.SetBit(17); } - if (Restricted) { Flags = Flags.SetBit(18); } - if (Min) { Flags = Flags.SetBit(20); } - if (BotInlineGeo) { Flags = Flags.SetBit(21); } - if (Support) { Flags = Flags.SetBit(23); } - if (Scam) { Flags = Flags.SetBit(24); } - if (ApplyMinPhoto) { Flags = Flags.SetBit(25); } - if (Fake) { Flags = Flags.SetBit(26); } - if (BotAttachMenu) { Flags = Flags.SetBit(27); } - if (Premium) { Flags = Flags.SetBit(28); } - if (AttachMenuEnabled) { Flags = Flags.SetBit(29); } - if (BotCanEdit) { Flags2 = Flags2.SetBit(1); } - if (CloseFriend) { Flags2 = Flags2.SetBit(2); } - if (StoriesHidden) { Flags2 = Flags2.SetBit(3); } - if (StoriesUnavailable) { Flags2 = Flags2.SetBit(4); } - if (ContactRequirePremium) { Flags2 = Flags2.SetBit(10); } - if (BotBusiness) { Flags2 = Flags2.SetBit(11); } - if (BotHasMainApp) { Flags2 = Flags2.SetBit(13); } - if (/*AccessHash != 0 &&*/ AccessHash.HasValue) { Flags = Flags.SetBit(0); } - if (FirstName != null) { Flags = Flags.SetBit(1); } - if (LastName != null) { Flags = Flags.SetBit(2); } - if (Username != null) { Flags = Flags.SetBit(3); } - if (Phone != null) { Flags = Flags.SetBit(4); } - if (Photo != null) { Flags = Flags.SetBit(5); } - if (Status != null) { Flags = Flags.SetBit(6); } - if (/*BotInfoVersion != 0 && */BotInfoVersion.HasValue) { Flags = Flags.SetBit(14); } - if (RestrictionReason?.Count > 0) { Flags = Flags.SetBit(18); } - if (BotInlinePlaceholder != null) { Flags = Flags.SetBit(19); } - if (LangCode != null) { Flags = Flags.SetBit(22); } - if (EmojiStatus != null) { Flags = Flags.SetBit(30); } - if (Usernames?.Count > 0) { Flags2 = Flags2.SetBit(0); } - // skipped StoriesMaxId (int vs RecentStory) - if (Color != null) { Flags2 = Flags2.SetBit(8); } - if (ProfileColor != null) { Flags2 = Flags2.SetBit(9); } - if (/*BotActiveUsers != 0 && */BotActiveUsers.HasValue) { Flags2 = Flags2.SetBit(12); } - if (/*BotVerificationIcon != 0 &&*/ BotVerificationIcon.HasValue) { Flags2 = Flags2.SetBit(14); } - if (/*SendPaidMessagesStars != 0 &&*/ SendPaidMessagesStars.HasValue) { Flags2 = Flags2.SetBit(15); } - } - - public void Serialize(IBufferWriter writer) - { - ComputeFlag(); - writer.Write(ConstructorId); - writer.Write(Flags); - writer.Write(Flags2); - writer.Write(Id); - if (Flags.IsBitSet(0)) { writer.Write(AccessHash.Value); } - if (Flags.IsBitSet(1)) { writer.Write(FirstName); } - if (Flags.IsBitSet(2)) { writer.Write(LastName); } - if (Flags.IsBitSet(3)) { writer.Write(Username); } - if (Flags.IsBitSet(4)) { writer.Write(Phone); } - if (Flags.IsBitSet(5)) { writer.Write(Photo); } - if (Flags.IsBitSet(6)) { writer.Write(Status); } - if (Flags.IsBitSet(14)) { writer.Write(BotInfoVersion.Value); } - if (Flags.IsBitSet(18)) { writer.Write(RestrictionReason); } - if (Flags.IsBitSet(19)) { writer.Write(BotInlinePlaceholder); } - if (Flags.IsBitSet(22)) { writer.Write(LangCode); } - if (Flags.IsBitSet(30)) { writer.Write(EmojiStatus); } - if (Flags2.IsBitSet(0)) { writer.Write(Usernames); } - if (Flags2.IsBitSet(5)) { writer.Write(StoriesMaxId.Value); } - if (Flags2.IsBitSet(8)) { writer.Write(Color); } - if (Flags2.IsBitSet(9)) { writer.Write(ProfileColor); } - if (Flags2.IsBitSet(12)) { writer.Write(BotActiveUsers.Value); } - if (Flags2.IsBitSet(14)) { writer.Write(BotVerificationIcon.Value); } - if (Flags2.IsBitSet(15)) { writer.Write(SendPaidMessagesStars.Value); } - } - - public void Deserialize(ref ReadOnlyMemory buffer) - { - Flags = buffer.ReadInt32(); - if (Flags.IsBitSet(10)) { Self = true; } - if (Flags.IsBitSet(11)) { Contact = true; } - if (Flags.IsBitSet(12)) { MutualContact = true; } - if (Flags.IsBitSet(13)) { Deleted = true; } - if (Flags.IsBitSet(14)) { Bot = true; } - if (Flags.IsBitSet(15)) { BotChatHistory = true; } - if (Flags.IsBitSet(16)) { BotNochats = true; } - if (Flags.IsBitSet(17)) { Verified = true; } - if (Flags.IsBitSet(18)) { Restricted = true; } - if (Flags.IsBitSet(20)) { Min = true; } - if (Flags.IsBitSet(21)) { BotInlineGeo = true; } - if (Flags.IsBitSet(23)) { Support = true; } - if (Flags.IsBitSet(24)) { Scam = true; } - if (Flags.IsBitSet(25)) { ApplyMinPhoto = true; } - if (Flags.IsBitSet(26)) { Fake = true; } - if (Flags.IsBitSet(27)) { BotAttachMenu = true; } - if (Flags.IsBitSet(28)) { Premium = true; } - if (Flags.IsBitSet(29)) { AttachMenuEnabled = true; } - Flags2 = buffer.ReadInt32(); - if (Flags2.IsBitSet(1)) { BotCanEdit = true; } - if (Flags2.IsBitSet(2)) { CloseFriend = true; } - if (Flags2.IsBitSet(3)) { StoriesHidden = true; } - if (Flags2.IsBitSet(4)) { StoriesUnavailable = true; } - if (Flags2.IsBitSet(10)) { ContactRequirePremium = true; } - if (Flags2.IsBitSet(11)) { BotBusiness = true; } - if (Flags2.IsBitSet(13)) { BotHasMainApp = true; } - Id = buffer.ReadInt64(); - if (Flags.IsBitSet(0)) { AccessHash = buffer.ReadInt64(); } - if (Flags.IsBitSet(1)) { FirstName = buffer.ReadString(); } - if (Flags.IsBitSet(2)) { LastName = buffer.ReadString(); } - if (Flags.IsBitSet(3)) { Username = buffer.ReadString(); } - if (Flags.IsBitSet(4)) { Phone = buffer.ReadString(); } - if (Flags.IsBitSet(5)) { Photo = buffer.Read(); } - if (Flags.IsBitSet(6)) { Status = buffer.Read(); } - if (Flags.IsBitSet(14)) { BotInfoVersion = buffer.ReadInt32(); } - if (Flags.IsBitSet(18)) { RestrictionReason = buffer.Read>(); } - if (Flags.IsBitSet(19)) { BotInlinePlaceholder = buffer.ReadString(); } - if (Flags.IsBitSet(22)) { LangCode = buffer.ReadString(); } - if (Flags.IsBitSet(30)) { EmojiStatus = buffer.Read(); } - if (Flags2.IsBitSet(0)) { Usernames = buffer.Read>(); } - if (Flags2.IsBitSet(5)) { StoriesMaxId = buffer.ReadInt32(); } - if (Flags2.IsBitSet(8)) { Color = buffer.Read(); } - if (Flags2.IsBitSet(9)) { ProfileColor = buffer.Read(); } - if (Flags2.IsBitSet(12)) { BotActiveUsers = buffer.ReadInt32(); } - if (Flags2.IsBitSet(14)) { BotVerificationIcon = buffer.ReadInt64(); } - if (Flags2.IsBitSet(15)) { SendPaidMessagesStars = buffer.ReadInt64(); } - } +// +// ReSharper disable All + +namespace MyTelegram.Schema; + +/// +/// Indicates info about a certain user.Unless specified otherwise, when updating the local peer database, all fields from the newly received constructor take priority over the old constructor cached locally (including by removing fields that aren't set in the new constructor).See here » for an implementation of the logic to use when updating the local user peer database. +/// See +/// +[TlObject(0x31774388)] +public sealed class TUser : IUser, ILayeredUser +{ + public uint ConstructorId => 0x31774388; + /// + /// Flags, see TL conditional fields + /// + public int Flags { get; set; } + + /// + /// Whether this user indicates the currently logged in user + /// See + /// + public bool Self { get; set; } + + /// + /// Whether this user is a contact
When updating the
local peer database, do not apply changes to this field if the min flag is set. + /// See + ///
+ public bool Contact { get; set; } + + /// + /// Whether this user is a mutual contact.
When updating the
local peer database, do not apply changes to this field if the min flag is set. + /// See + ///
+ public bool MutualContact { get; set; } + + /// + /// Whether the account of this user was deleted.
Changes to this flag should invalidate the local
userFull cache for this user ID, see here » for more info. + /// See + ///
+ public bool Deleted { get; set; } + + /// + /// Is this user a bot?
Changes to this flag should invalidate the local
userFull cache for this user ID, see here » for more info. + /// See + ///
+ public bool Bot { get; set; } + + /// + /// Can the bot see all messages in groups? + /// See + /// + public bool BotChatHistory { get; set; } + + /// + /// Can the bot be added to groups? + /// See + /// + public bool BotNochats { get; set; } + + /// + /// Whether this user is verified + /// See + /// + public bool Verified { get; set; } + + /// + /// Access to this user must be restricted for the reason specified in restriction_reason + /// See + /// + public bool Restricted { get; set; } + + /// + /// See min + /// See + /// + public bool Min { get; set; } + + /// + /// Whether the bot can request our geolocation in inline mode + /// See + /// + public bool BotInlineGeo { get; set; } + + /// + /// Whether this is an official support user + /// See + /// + public bool Support { get; set; } + + /// + /// This may be a scam user + /// See + /// + public bool Scam { get; set; } + + /// + /// If set and min is set, the value of photo can be used to update the local database, see the documentation of that flag for more info. + /// See + /// + public bool ApplyMinPhoto { get; set; } + + /// + /// If set, this user was reported by many users as a fake or scam user: be careful when interacting with them. + /// See + /// + public bool Fake { get; set; } + + /// + /// Whether this bot offers an attachment menu web app + /// See + /// + public bool BotAttachMenu { get; set; } + + /// + /// Whether this user is a Telegram Premium user
Changes to this flag should invalidate the local
userFull cache for this user ID, see here » for more info.
Changes to this flag if the self flag is set should also trigger the following calls, to refresh the respective caches:
- The help.getConfig cache
- The messages.getTopReactions cache if the bot flag is not set + /// See + ///
+ public bool Premium { get; set; } + + /// + /// Whether we installed the attachment menu web app offered by this bot.
When updating the local peer database, do not apply changes to this field if the min flag is set. + /// See + ///
+ public bool AttachMenuEnabled { get; set; } + + /// + /// Flags, see TL conditional fields + /// + public int Flags2 { get; set; } + + /// + /// Whether we can edit the profile picture, name, about text and description of this bot because we own it.
When updating the local peer database, do not apply changes to this field if the min flag is set.
Changes to this flag (if min is not set) should invalidate the local userFull cache for this user ID. + /// See + ///
+ public bool BotCanEdit { get; set; } + + /// + /// Whether we marked this user as a close friend, see here » for more info.
When updating the local peer database, do not apply changes to this field if the min flag is set. + /// See + ///
+ public bool CloseFriend { get; set; } + + /// + /// Whether we have hidden » all active stories of this user.
When updating the local peer database, do not apply changes to this field if the min flag is set. + /// See + ///
+ public bool StoriesHidden { get; set; } + + /// + /// No stories from this user are visible. + /// See + /// + public bool StoriesUnavailable { get; set; } + + /// + /// If set, we can only write to this user if they have already sent some messages to us, if we are subscribed to Telegram Premium, or if they're a mutual contact (user.mutual_contact).
All the secondary conditions listed above must be checked separately to verify whether we can still write to the user, even if this flag is set (i.e. a mutual contact will have this flag set even if we can still write to them, and so on...); to avoid doing these extra checks if we haven't yet cached all the required information (for example while displaying the chat list in the sharing UI) the users.getIsPremiumRequiredToContact method may be invoked instead, passing the list of users currently visible in the UI, returning a list of booleans that directly specify whether we can or cannot write to each user; alternatively, the userFull.contact_require_premium flag contains the same (fully checked, i.e. it's not just a copy of this flag) info returned by users.getIsPremiumRequiredToContact.
To set this flag for ourselves invoke account.setGlobalPrivacySettings, setting the settings.new_noncontact_peers_require_premium flag. + /// See + ///
+ public bool ContactRequirePremium { get; set; } + + /// + /// Whether this bot can be connected to a user as specified here ». + /// See + /// + public bool BotBusiness { get; set; } + + /// + /// If set, this bot has configured a Main Mini App ». + /// See + /// + public bool BotHasMainApp { get; set; } + + /// + /// ID of the user, see here » for more info. + /// + public long Id { get; set; } + + /// + /// Access hash of the user, see here » for more info.
If this flag is set, when updating the local peer database, generate a virtual flag called min_access_hash, which is:
- Set to true if min is set AND
-- The phone flag is not set OR
-- The phone flag is set and the associated phone number string is non-empty
- Set to false otherwise.

Then, apply both access_hash and min_access_hash to the local database if:
- min_access_hash is false OR
- min_access_hash is true AND
-- There is no locally cached object for this user OR
-- There is no access_hash in the local cache OR
-- The cached object's min_access_hash is also true

If the final merged object stored to the database has the min_access_hash field set to true, the related access_hash is only suitable to use in inputPeerPhotoFileLocation », to directly download the profile pictures of users, everywhere else a inputPeer*FromMessage constructor will have to be generated as specified here ».
Bots can also use min access hashes in some conditions, by passing 0 instead of the min access hash. + ///
+ public long? AccessHash { get; set; } + + /// + /// First name.
When updating the local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The min flag of the locally cached user entry is set. + ///
+ public string? FirstName { get; set; } + + /// + /// Last name.
When updating the local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The min flag of the locally cached user entry is set. + ///
+ public string? LastName { get; set; } + + /// + /// Main active username.
When updating the local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The min flag of the locally cached user entry is set.
Changes to this flag should invalidate the local userFull cache for this user ID if the above conditions are respected and the bot_can_edit flag is also set. + ///
+ public string? Username { get; set; } + + /// + /// Phone number.
When updating the local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The min flag of the locally cached user entry is set. + ///
+ public string? Phone { get; set; } + + /// + /// Profile picture of user.
When updating the local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The apply_min_photo flag is set OR
-- The min flag of the locally cached user entry is set. + /// See + ///
+ public MyTelegram.Schema.IUserProfilePhoto? Photo { get; set; } + + /// + /// Online status of user.
When updating the
local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The min flag of the locally cached user entry is set OR
-- The locally cached user entry is equal to userStatusEmpty. + /// See + ///
+ public MyTelegram.Schema.IUserStatus? Status { get; set; } + + /// + /// Version of the bot_info field in userFull, incremented every time it changes.
Changes to this flag should invalidate the local userFull cache for this user ID, see here » for more info. + ///
+ public int? BotInfoVersion { get; set; } + + /// + /// Contains the reason why access to this user must be restricted. + /// + public TVector? RestrictionReason { get; set; } + + /// + /// Inline placeholder for this inline bot + /// + public string? BotInlinePlaceholder { get; set; } + + /// + /// Language code of the user + /// + public string? LangCode { get; set; } + + /// + /// Emoji status + /// See + /// + public MyTelegram.Schema.IEmojiStatus? EmojiStatus { get; set; } + + /// + /// Additional usernames.
When updating the
local peer database, apply changes to this field only if:
- The min flag is not set OR
- The min flag is set AND
-- The min flag of the locally cached user entry is set.
Changes to this flag (if the above conditions are respected) should invalidate the local userFull cache for this user ID. + ///
+ public TVector? Usernames { get; set; } + + /// + /// ID of the maximum read story.
When updating the local peer database, do not apply changes to this field if the min flag of the incoming constructor is set. + ///
+ public int? StoriesMaxId { get; set; } + + /// + /// The user's accent color. + /// See + /// + public MyTelegram.Schema.IPeerColor? Color { get; set; } + + /// + /// The user's profile color. + /// See + /// + public MyTelegram.Schema.IPeerColor? ProfileColor { get; set; } + + /// + /// Monthly Active Users (MAU) of this bot (may be absent for small bots). + /// + public int? BotActiveUsers { get; set; } + public long? BotVerificationIcon { get; set; } + public long? SendPaidMessagesStars { get; set; } + + public void ComputeFlag() + { + Flags = 0; + Flags2 = 0; + if (Self) { Flags = Flags.SetBit(10); } + if (Contact) { Flags = Flags.SetBit(11); } + if (MutualContact) { Flags = Flags.SetBit(12); } + if (Deleted) { Flags = Flags.SetBit(13); } + if (Bot) { Flags = Flags.SetBit(14); } + if (BotChatHistory) { Flags = Flags.SetBit(15); } + if (BotNochats) { Flags = Flags.SetBit(16); } + if (Verified) { Flags = Flags.SetBit(17); } + if (Restricted) { Flags = Flags.SetBit(18); } + if (Min) { Flags = Flags.SetBit(20); } + if (BotInlineGeo) { Flags = Flags.SetBit(21); } + if (Support) { Flags = Flags.SetBit(23); } + if (Scam) { Flags = Flags.SetBit(24); } + if (ApplyMinPhoto) { Flags = Flags.SetBit(25); } + if (Fake) { Flags = Flags.SetBit(26); } + if (BotAttachMenu) { Flags = Flags.SetBit(27); } + if (Premium) { Flags = Flags.SetBit(28); } + if (AttachMenuEnabled) { Flags = Flags.SetBit(29); } + if (BotCanEdit) { Flags2 = Flags2.SetBit(1); } + if (CloseFriend) { Flags2 = Flags2.SetBit(2); } + if (StoriesHidden) { Flags2 = Flags2.SetBit(3); } + if (StoriesUnavailable) { Flags2 = Flags2.SetBit(4); } + if (ContactRequirePremium) { Flags2 = Flags2.SetBit(10); } + if (BotBusiness) { Flags2 = Flags2.SetBit(11); } + if (BotHasMainApp) { Flags2 = Flags2.SetBit(13); } + if (/*AccessHash != 0 &&*/ AccessHash.HasValue) { Flags = Flags.SetBit(0); } + if (FirstName != null) { Flags = Flags.SetBit(1); } + if (LastName != null) { Flags = Flags.SetBit(2); } + if (Username != null) { Flags = Flags.SetBit(3); } + if (Phone != null) { Flags = Flags.SetBit(4); } + if (Photo != null) { Flags = Flags.SetBit(5); } + if (Status != null) { Flags = Flags.SetBit(6); } + if (/*BotInfoVersion != 0 && */BotInfoVersion.HasValue) { Flags = Flags.SetBit(14); } + if (RestrictionReason?.Count > 0) { Flags = Flags.SetBit(18); } + if (BotInlinePlaceholder != null) { Flags = Flags.SetBit(19); } + if (LangCode != null) { Flags = Flags.SetBit(22); } + if (EmojiStatus != null) { Flags = Flags.SetBit(30); } + if (Usernames?.Count > 0) { Flags2 = Flags2.SetBit(0); } + // skipped StoriesMaxId (int vs RecentStory) + if (Color != null) { Flags2 = Flags2.SetBit(8); } + if (ProfileColor != null) { Flags2 = Flags2.SetBit(9); } + if (/*BotActiveUsers != 0 && */BotActiveUsers.HasValue) { Flags2 = Flags2.SetBit(12); } + if (/*BotVerificationIcon != 0 &&*/ BotVerificationIcon.HasValue) { Flags2 = Flags2.SetBit(14); } + if (/*SendPaidMessagesStars != 0 &&*/ SendPaidMessagesStars.HasValue) { Flags2 = Flags2.SetBit(15); } + } + + public void Serialize(IBufferWriter writer) + { + ComputeFlag(); + writer.Write(ConstructorId); + writer.Write(Flags); + writer.Write(Flags2); + writer.Write(Id); + if (Flags.IsBitSet(0)) { writer.Write(AccessHash.Value); } + if (Flags.IsBitSet(1)) { writer.Write(FirstName); } + if (Flags.IsBitSet(2)) { writer.Write(LastName); } + if (Flags.IsBitSet(3)) { writer.Write(Username); } + if (Flags.IsBitSet(4)) { writer.Write(Phone); } + if (Flags.IsBitSet(5)) { writer.Write(Photo); } + if (Flags.IsBitSet(6)) { writer.Write(Status); } + if (Flags.IsBitSet(14)) { writer.Write(BotInfoVersion.Value); } + if (Flags.IsBitSet(18)) { writer.Write(RestrictionReason); } + if (Flags.IsBitSet(19)) { writer.Write(BotInlinePlaceholder); } + if (Flags.IsBitSet(22)) { writer.Write(LangCode); } + if (Flags.IsBitSet(30)) { writer.Write(EmojiStatus); } + if (Flags2.IsBitSet(0)) { writer.Write(Usernames); } + if (Flags2.IsBitSet(5)) { writer.Write(StoriesMaxId.Value); } + if (Flags2.IsBitSet(8)) { writer.Write(Color); } + if (Flags2.IsBitSet(9)) { writer.Write(ProfileColor); } + if (Flags2.IsBitSet(12)) { writer.Write(BotActiveUsers.Value); } + if (Flags2.IsBitSet(14)) { writer.Write(BotVerificationIcon.Value); } + if (Flags2.IsBitSet(15)) { writer.Write(SendPaidMessagesStars.Value); } + } + + public void Deserialize(ref ReadOnlyMemory buffer) + { + Flags = buffer.ReadInt32(); + if (Flags.IsBitSet(10)) { Self = true; } + if (Flags.IsBitSet(11)) { Contact = true; } + if (Flags.IsBitSet(12)) { MutualContact = true; } + if (Flags.IsBitSet(13)) { Deleted = true; } + if (Flags.IsBitSet(14)) { Bot = true; } + if (Flags.IsBitSet(15)) { BotChatHistory = true; } + if (Flags.IsBitSet(16)) { BotNochats = true; } + if (Flags.IsBitSet(17)) { Verified = true; } + if (Flags.IsBitSet(18)) { Restricted = true; } + if (Flags.IsBitSet(20)) { Min = true; } + if (Flags.IsBitSet(21)) { BotInlineGeo = true; } + if (Flags.IsBitSet(23)) { Support = true; } + if (Flags.IsBitSet(24)) { Scam = true; } + if (Flags.IsBitSet(25)) { ApplyMinPhoto = true; } + if (Flags.IsBitSet(26)) { Fake = true; } + if (Flags.IsBitSet(27)) { BotAttachMenu = true; } + if (Flags.IsBitSet(28)) { Premium = true; } + if (Flags.IsBitSet(29)) { AttachMenuEnabled = true; } + Flags2 = buffer.ReadInt32(); + if (Flags2.IsBitSet(1)) { BotCanEdit = true; } + if (Flags2.IsBitSet(2)) { CloseFriend = true; } + if (Flags2.IsBitSet(3)) { StoriesHidden = true; } + if (Flags2.IsBitSet(4)) { StoriesUnavailable = true; } + if (Flags2.IsBitSet(10)) { ContactRequirePremium = true; } + if (Flags2.IsBitSet(11)) { BotBusiness = true; } + if (Flags2.IsBitSet(13)) { BotHasMainApp = true; } + Id = buffer.ReadInt64(); + if (Flags.IsBitSet(0)) { AccessHash = buffer.ReadInt64(); } + if (Flags.IsBitSet(1)) { FirstName = buffer.ReadString(); } + if (Flags.IsBitSet(2)) { LastName = buffer.ReadString(); } + if (Flags.IsBitSet(3)) { Username = buffer.ReadString(); } + if (Flags.IsBitSet(4)) { Phone = buffer.ReadString(); } + if (Flags.IsBitSet(5)) { Photo = buffer.Read(); } + if (Flags.IsBitSet(6)) { Status = buffer.Read(); } + if (Flags.IsBitSet(14)) { BotInfoVersion = buffer.ReadInt32(); } + if (Flags.IsBitSet(18)) { RestrictionReason = buffer.Read>(); } + if (Flags.IsBitSet(19)) { BotInlinePlaceholder = buffer.ReadString(); } + if (Flags.IsBitSet(22)) { LangCode = buffer.ReadString(); } + if (Flags.IsBitSet(30)) { EmojiStatus = buffer.Read(); } + if (Flags2.IsBitSet(0)) { Usernames = buffer.Read>(); } + if (Flags2.IsBitSet(5)) { StoriesMaxId = buffer.ReadInt32(); } + if (Flags2.IsBitSet(8)) { Color = buffer.Read(); } + if (Flags2.IsBitSet(9)) { ProfileColor = buffer.Read(); } + if (Flags2.IsBitSet(12)) { BotActiveUsers = buffer.ReadInt32(); } + if (Flags2.IsBitSet(14)) { BotVerificationIcon = buffer.ReadInt64(); } + if (Flags2.IsBitSet(15)) { SendPaidMessagesStars = buffer.ReadInt64(); } + } } \ No newline at end of file diff --git a/source/src/MyTelegram.Schema/LatestLayer/Stories/RequestGetPeerMaxIDs.cs b/source/src/MyTelegram.Schema/LatestLayer/Stories/RequestGetPeerMaxIDs.cs index 40749bf7..911ef26b 100644 --- a/source/src/MyTelegram.Schema/LatestLayer/Stories/RequestGetPeerMaxIDs.cs +++ b/source/src/MyTelegram.Schema/LatestLayer/Stories/RequestGetPeerMaxIDs.cs @@ -1,35 +1,35 @@ -// -// ReSharper disable All - -namespace MyTelegram.Schema.Stories; - -/// -/// Get the IDs of the maximum read stories for a set of peers. -/// See -/// -[TlObject(0x535983c3)] -public sealed class RequestGetPeerMaxIDs : IRequest> -{ - public uint ConstructorId => 0x535983c3; - /// - /// Peers - /// - public TVector Id { get; set; } - - public void ComputeFlag() - { - - } - - public void Serialize(IBufferWriter writer) - { - ComputeFlag(); - writer.Write(ConstructorId); - writer.Write(Id); - } - - public void Deserialize(ref ReadOnlyMemory buffer) - { - Id = buffer.Read>(); - } -} +// +// ReSharper disable All + +namespace MyTelegram.Schema.Stories; + +/// +/// Get the IDs of the maximum read stories for a set of peers. +/// See +/// +[TlObject(0x78499170)] +public sealed class RequestGetPeerMaxIDs : IRequest> +{ + public uint ConstructorId => 0x78499170; + /// + /// Peers + /// + public TVector Id { get; set; } + + public void ComputeFlag() + { + + } + + public void Serialize(IBufferWriter writer) + { + ComputeFlag(); + writer.Write(ConstructorId); + writer.Write(Id); + } + + public void Deserialize(ref ReadOnlyMemory buffer) + { + Id = buffer.Read>(); + } +} diff --git a/source/src/MyTelegram.Schema/LatestLayer/Users/RequestGetSavedMusic.cs b/source/src/MyTelegram.Schema/LatestLayer/Users/RequestGetSavedMusic.cs index 3e7e58fd..942aca16 100644 --- a/source/src/MyTelegram.Schema/LatestLayer/Users/RequestGetSavedMusic.cs +++ b/source/src/MyTelegram.Schema/LatestLayer/Users/RequestGetSavedMusic.cs @@ -6,10 +6,10 @@ namespace MyTelegram.Schema.Users; /// Get songs pinned to the user's profile /// See ///
-[TlObject(0x8b6a6a2c)] +[TlObject(0x788d7fe3)] public class RequestGetSavedMusic : IRequest { - public uint ConstructorId => 0x8b6a6a2c; + public uint ConstructorId => 0x788d7fe3; /// /// The user whose profile to fetch diff --git a/source/src/MyTelegram.Schema/Serializer/ObjectSerializer.cs b/source/src/MyTelegram.Schema/Serializer/ObjectSerializer.cs index f80d9bde..111c9aed 100644 --- a/source/src/MyTelegram.Schema/Serializer/ObjectSerializer.cs +++ b/source/src/MyTelegram.Schema/Serializer/ObjectSerializer.cs @@ -78,7 +78,21 @@ public class ObjectSerializer : ISerializer var emptyPeer = new TInputPeerEmpty(); return (T)(object)emptyPeer; } - + if (typeOfT == typeof(IInputPrivacyKey)) + { + return (T)(object)new TInputPrivacyKeyStatusTimestamp(); + } + if (typeOfT == typeof(IMessagesFilter)) + { + return (T)(object)new TInputMessagesFilterEmpty(); + } + if (typeOfT == typeof(IInputMedia) && constructorId == 0x1e287d04) + { + var photo = new TInputMediaUploadedPhoto(); + photo.Deserialize(ref buffer); + return (T)(object)photo; + } + throw new NotSupportedException($"Unsupported constructorId:0x{constructorId:x2},type:{typeOfT.Namespace}.{typeOfT.Name}"); } diff --git a/source/src/MyTelegram.Schema/TContainerMessage.cs b/source/src/MyTelegram.Schema/TContainerMessage.cs index 652a039e..ed53cee7 100644 --- a/source/src/MyTelegram.Schema/TContainerMessage.cs +++ b/source/src/MyTelegram.Schema/TContainerMessage.cs @@ -1,60 +1,81 @@ -// - -using System; - -namespace MyTelegram.Schema; - -[TlObject(0x3072cfa1)] -public class TGzipPacked : IRequest -{ - public uint ConstructorId => 0x3072cfa1; - public void Serialize(IBufferWriter writer) - { - ComputeFlag(); - writer.Write((uint)ConstructorId); - writer.Write(PackedData); - } - - public void Deserialize(ref ReadOnlyMemory buffer) - { - PackedData = buffer.ReadBytes(); - } - - public ReadOnlyMemory PackedData { get; set; } = ReadOnlyMemory.Empty; - - public void ComputeFlag() - { - - } -} - -public sealed class TContainerMessage : IObject -{ - public uint ConstructorId => 0x73f1f8dc; - - public void Deserialize(ref ReadOnlyMemory buffer) - { - MsgId = buffer.ReadInt64(); - SeqNo = buffer.ReadInt32(); - Bytes = buffer.ReadInt32(); - Body = buffer.Read(); - } - - - public void Serialize(IBufferWriter writer) - { - writer.Write(MsgId); - writer.Write(SeqNo); - writer.Write(Bytes); - Body.Serialize(writer); - } - - - public long MsgId { get; set; } - - public int SeqNo { get; set; } - - public int Bytes { get; set; } - - public IObject Body { get; set; } +// + +using System; + +namespace MyTelegram.Schema; + +[TlObject(0x3072cfa1)] +public class TGzipPacked : IRequest +{ + public uint ConstructorId => 0x3072cfa1; + public void Serialize(IBufferWriter writer) + { + ComputeFlag(); + writer.Write((uint)ConstructorId); + writer.Write(PackedData); + } + + public void Deserialize(ref ReadOnlyMemory buffer) + { + PackedData = buffer.ReadBytes(); + } + + public ReadOnlyMemory PackedData { get; set; } = ReadOnlyMemory.Empty; + + public void ComputeFlag() + { + + } +} + +public sealed class TContainerMessage : IObject +{ + public uint ConstructorId => 0x73f1f8dc; + + public void Deserialize(ref ReadOnlyMemory buffer) + { + MsgId = buffer.ReadInt64(); + SeqNo = buffer.ReadInt32(); + Bytes = buffer.ReadInt32(); + if (Bytes < 0 || Bytes > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(Bytes), Bytes, "Invalid container message body length"); + } + + // Body length is known — skip unknown TL constructors instead of failing the whole msg_container + // (clients pack getDialogs together with newer methods like users.getSavedMusic). + var bodySlice = buffer[..Bytes]; + buffer = buffer[Bytes..]; + try + { + var tmp = bodySlice; + Body = tmp.Read(); + } + catch (NotSupportedException) + { + Body = null!; + } + catch (Exception) + { + Body = null!; + } + } + + + public void Serialize(IBufferWriter writer) + { + writer.Write(MsgId); + writer.Write(SeqNo); + writer.Write(Bytes); + Body.Serialize(writer); + } + + + public long MsgId { get; set; } + + public int SeqNo { get; set; } + + public int Bytes { get; set; } + + public IObject Body { get; set; } } \ No newline at end of file diff --git a/source/src/MyTelegram.Schema/TMsgContainer.cs b/source/src/MyTelegram.Schema/TMsgContainer.cs index 06a42ffa..fe1ead91 100644 --- a/source/src/MyTelegram.Schema/TMsgContainer.cs +++ b/source/src/MyTelegram.Schema/TMsgContainer.cs @@ -1,35 +1,40 @@ -// - -namespace MyTelegram.Schema; - -[TlObject(0x73f1f8dc)] -public class TMsgContainer : IRequest -{ - public uint ConstructorId => 0x73f1f8dc; - public TContainerMessage[] Messages { get; set; } - - public void Serialize(IBufferWriter writer) - { - writer.Write(ConstructorId); - writer.Write(Messages.Length); - foreach (var containerMessage in Messages) - { - writer.Write(containerMessage); - } - } - - public void Deserialize(ref ReadOnlyMemory buffer) - { - var length = buffer.ReadInt32(); - if (length > 0) - { - Messages = new TContainerMessage[length]; - for (int i = 0; i < length; i++) - { - var item = new TContainerMessage(); - item.Deserialize(ref buffer); - Messages[i] = item; - } - } - } +// + +namespace MyTelegram.Schema; + +[TlObject(0x73f1f8dc)] +public class TMsgContainer : IRequest +{ + public uint ConstructorId => 0x73f1f8dc; + public TContainerMessage[] Messages { get; set; } + + public void Serialize(IBufferWriter writer) + { + writer.Write(ConstructorId); + writer.Write(Messages.Length); + foreach (var containerMessage in Messages) + { + writer.Write(containerMessage); + } + } + + public void Deserialize(ref ReadOnlyMemory buffer) + { + var length = buffer.ReadInt32(); + if (length > 0) + { + Messages = new TContainerMessage[length]; + var list = new System.Collections.Generic.List(length); + for (int i = 0; i < length; i++) + { + var item = new TContainerMessage(); + item.Deserialize(ref buffer); + if (item.Body != null) + { + list.Add(item); + } + } + Messages = list.ToArray(); + } + } } \ No newline at end of file diff --git a/source/src/MyTelegram.Services/Services/QueuedObjectMessageSender.cs b/source/src/MyTelegram.Services/Services/QueuedObjectMessageSender.cs index b924ade2..a1f89f92 100644 --- a/source/src/MyTelegram.Services/Services/QueuedObjectMessageSender.cs +++ b/source/src/MyTelegram.Services/Services/QueuedObjectMessageSender.cs @@ -42,30 +42,31 @@ public class QueuedObjectMessageSender( { var bytes = data.ToBytes(); - // Session-server AOT is bound to LayeredPush, not PushMessageToPeerEvent. - sessionMessageQueueProcessor.Enqueue(new LayeredPushMessageCreatedIntegrationEvent( - peer.PeerType, - peer.PeerId, - bytes, - excludeAuthKeyId, - excludeUserId, - onlySendToUserId, - onlySendToThisAuthKeyId, - pts, - qts, - globalSeqNo, - pushData, - excludeUserIds - ), - peer.PeerId); - - // Also push raw bytes to each device auth key. LayeredPush re-deserializes - // in AOT and drops the update when constructor IDs do not match. + // Do NOT use LayeredPush: session AOT re-deserializes and drops IUser 0x31774388 + // (profile, calls, online status). Send raw bytes to live auth keys only. if (peer.PeerType == PeerType.User && (excludeUserIds == null || !excludeUserIds.Contains(peer.PeerId))) { FanoutToDeviceAuthKeys(peer.PeerId, bytes, excludeAuthKeyId, onlySendToThisAuthKeyId, pts, qts, globalSeqNo); } + else + { + sessionMessageQueueProcessor.Enqueue(new LayeredPushMessageCreatedIntegrationEvent( + peer.PeerType, + peer.PeerId, + bytes, + excludeAuthKeyId, + excludeUserId, + onlySendToUserId, + onlySendToThisAuthKeyId, + pts, + qts, + globalSeqNo, + pushData, + excludeUserIds + ), + peer.PeerId); + } return Task.CompletedTask; } @@ -125,12 +126,14 @@ public class QueuedObjectMessageSender( public Task SendMessageToPeerAsync(RequestInfo requestInfo, TData data) where TData : IObject { - sessionMessageQueueProcessor.Enqueue(new DataResultResponseReceivedEvent(requestInfo.ReqMsgId, Array.Empty()) - { - DataObject = data - }, + sessionMessageQueueProcessor.Enqueue( + new DataResultResponseWithUserIdReceivedEvent( + requestInfo.ReqMsgId, + GzipRpc(data, requestInfo.ReqMsgId), + requestInfo.UserId, + requestInfo.AuthKeyId, + requestInfo.PermAuthKeyId), requestInfo.PermAuthKeyId); - return Task.CompletedTask; } @@ -154,10 +157,8 @@ public class QueuedObjectMessageSender( { var rpcResult = CreateRpcResult(reqMsgId, data); - sessionMessageQueueProcessor.Enqueue(new DataResultResponseReceivedEvent(reqMsgId, Array.Empty()) - { - DataObject = rpcResult - }, + sessionMessageQueueProcessor.Enqueue( + new DataResultResponseWithUserIdReceivedEvent(reqMsgId, GzipRpc(rpcResult, reqMsgId), 0, 0, permAuthKeyId), permAuthKeyId); return Task.CompletedTask; @@ -169,13 +170,27 @@ public class QueuedObjectMessageSender( { var rpcResult = CreateRpcResult(requestInfo.ReqMsgId, data); sessionMessageQueueProcessor.Enqueue( - new DataResultResponseWithUserIdReceivedEvent(requestInfo.ReqMsgId, rpcResult.ToBytes(), userId, authKeyId, + new DataResultResponseWithUserIdReceivedEvent(requestInfo.ReqMsgId, GzipRpc(rpcResult, requestInfo.ReqMsgId), userId, authKeyId, permAuthKeyId), requestInfo.PermAuthKeyId); return Task.CompletedTask; } + private byte[] GzipRpc(IObject data, long reqMsgId) + { + TRpcResult rpc = data as TRpcResult ?? new TRpcResult { ReqMsgId = reqMsgId, Result = data }; + if (rpc.Result is not TGzipPacked && rpc.Result != null) + { + using var writer = new ArrayPoolBufferWriter(); + rpc.Result.Serialize(writer); + using var gz = new ArrayPoolBufferWriter(); + gzipHelper.Compress(writer.WrittenSpan, gz); + rpc.Result = new TGzipPacked { PackedData = gz.WrittenMemory.ToArray() }; + } + return rpc.ToBytes(); + } + private TRpcResult CreateRpcResult(long reqMsgId, TData data) where TData : IObject { var rpcResult = new TRpcResult { ReqMsgId = reqMsgId, Result = data }; diff --git a/source/src/MyTelegram.Services/Services/SessionMessageDataProcessor.cs b/source/src/MyTelegram.Services/Services/SessionMessageDataProcessor.cs index 28010917..32a9d8dd 100644 --- a/source/src/MyTelegram.Services/Services/SessionMessageDataProcessor.cs +++ b/source/src/MyTelegram.Services/Services/SessionMessageDataProcessor.cs @@ -1,91 +1,90 @@ -namespace MyTelegram.Services.Services; - -public class SessionMessageDataProcessor(IEventBus eventBus, IGZipHelper gZipHelper) : IDataProcessor -{ - //private static readonly IGZipHelper gZipHelper = new GZipHelper(); - - public async Task ProcessAsync(ISessionMessage data) - { - switch (data) - { - case DataResultResponseReceivedEvent dataResultResponseReceivedEvent: - - if (dataResultResponseReceivedEvent.DataObject is TRpcResult rpcResult) - { - using var writer = new ArrayPoolBufferWriter(); - - dataResultResponseReceivedEvent.DataObject.Serialize(writer); - if (writer.WrittenCount > 500) - { - //await eventBus.PublishAsync(newData); - using var writer2 = new ArrayPoolBufferWriter(); - rpcResult.Result.Serialize(writer2); - using var writer3 = new ArrayPoolBufferWriter(); - gZipHelper.Compress(writer2.WrittenSpan, writer3); - rpcResult.Result = new TGzipPacked - { - PackedData = writer3.WrittenMemory - }; - using var writer4 = new ArrayPoolBufferWriter(); - rpcResult.Serialize(writer4); - await eventBus.PublishAsync(dataResultResponseReceivedEvent with - { - DataObject = null, - Data = writer4.WrittenMemory - }); - - } - else - { - await eventBus.PublishAsync(dataResultResponseReceivedEvent with - { - DataObject = null, - Data = writer.WrittenMemory - }); - } - } - else - { - if (dataResultResponseReceivedEvent.DataObject != null) - { - using var writer = new ArrayPoolBufferWriter(); - dataResultResponseReceivedEvent.DataObject.Serialize(writer); - await eventBus.PublishAsync(dataResultResponseReceivedEvent with - { - DataObject = null, - Data = writer.WrittenMemory - }); - } - } - - break; - case DataResultResponseWithUserIdReceivedEvent dataResultResponseWithUserIdReceivedEvent: - await eventBus.PublishAsync(dataResultResponseWithUserIdReceivedEvent); - break; - case FileDataResultResponseReceivedEvent fileDataResultResponseReceivedEvent: - await eventBus.PublishAsync(fileDataResultResponseReceivedEvent); - break; - case LayeredAuthKeyIdMessageCreatedIntegrationEvent layeredAuthKeyIdMessageCreatedIntegrationEvent: - await eventBus.PublishAsync(layeredAuthKeyIdMessageCreatedIntegrationEvent); - break; - case LayeredPushMessageCreatedIntegrationEvent layeredPushMessageCreatedIntegrationEvent: - await eventBus.PublishAsync(layeredPushMessageCreatedIntegrationEvent); - break; - case PushMessageToPeerEvent pushMessageToPeerEvent: - await eventBus.PublishAsync(pushMessageToPeerEvent); - break; - //case PushSessionMessageToAuthKeyIdEvent pushSessionMessageToAuthKeyIdEvent: - // await _eventBus.PublishAsync(pushSessionMessageToAuthKeyIdEvent); - // break; - //case PushSessionMessageToPeerEvent pushSessionMessageToPeerEvent: - // await _eventBus.PublishAsync(pushSessionMessageToPeerEvent); - // break; - - - default: - throw new ArgumentOutOfRangeException(nameof(data)); - } - - //return Task.CompletedTask; - } +namespace MyTelegram.Services.Services; + +public class SessionMessageDataProcessor(IEventBus eventBus, IGZipHelper gZipHelper) : IDataProcessor +{ + //private static readonly IGZipHelper gZipHelper = new GZipHelper(); + + public async Task ProcessAsync(ISessionMessage data) + { + switch (data) + { + case DataResultResponseReceivedEvent dataResultResponseReceivedEvent: + + if (dataResultResponseReceivedEvent.DataObject is TRpcResult rpcResult) + { + using var writer = new ArrayPoolBufferWriter(); + + dataResultResponseReceivedEvent.DataObject.Serialize(writer); + if (writer.WrittenCount > 0) + { + using var writer2 = new ArrayPoolBufferWriter(); + rpcResult.Result.Serialize(writer2); + using var writer3 = new ArrayPoolBufferWriter(); + gZipHelper.Compress(writer2.WrittenSpan, writer3); + rpcResult.Result = new TGzipPacked + { + PackedData = writer3.WrittenMemory.ToArray() + }; + using var writer4 = new ArrayPoolBufferWriter(); + rpcResult.Serialize(writer4); + await eventBus.PublishAsync(dataResultResponseReceivedEvent with + { + DataObject = null, + Data = writer4.WrittenMemory.ToArray() + }); + + } + else + { + await eventBus.PublishAsync(dataResultResponseReceivedEvent with + { + DataObject = null, + Data = writer.WrittenMemory.ToArray() + }); + } + } + else + { + if (dataResultResponseReceivedEvent.DataObject != null) + { + using var writer = new ArrayPoolBufferWriter(); + dataResultResponseReceivedEvent.DataObject.Serialize(writer); + await eventBus.PublishAsync(dataResultResponseReceivedEvent with + { + DataObject = null, + Data = writer.WrittenMemory.ToArray() + }); + } + } + + break; + case DataResultResponseWithUserIdReceivedEvent dataResultResponseWithUserIdReceivedEvent: + await eventBus.PublishAsync(dataResultResponseWithUserIdReceivedEvent); + break; + case FileDataResultResponseReceivedEvent fileDataResultResponseReceivedEvent: + await eventBus.PublishAsync(fileDataResultResponseReceivedEvent); + break; + case LayeredAuthKeyIdMessageCreatedIntegrationEvent layeredAuthKeyIdMessageCreatedIntegrationEvent: + await eventBus.PublishAsync(layeredAuthKeyIdMessageCreatedIntegrationEvent); + break; + case LayeredPushMessageCreatedIntegrationEvent layeredPushMessageCreatedIntegrationEvent: + await eventBus.PublishAsync(layeredPushMessageCreatedIntegrationEvent); + break; + case PushMessageToPeerEvent pushMessageToPeerEvent: + await eventBus.PublishAsync(pushMessageToPeerEvent); + break; + //case PushSessionMessageToAuthKeyIdEvent pushSessionMessageToAuthKeyIdEvent: + // await _eventBus.PublishAsync(pushSessionMessageToAuthKeyIdEvent); + // break; + //case PushSessionMessageToPeerEvent pushSessionMessageToPeerEvent: + // await _eventBus.PublishAsync(pushSessionMessageToPeerEvent); + // break; + + + default: + throw new ArgumentOutOfRangeException(nameof(data)); + } + + //return Task.CompletedTask; + } } \ No newline at end of file diff --git a/source/src/MyTelegram.SessionServer/Dockerfile b/source/src/MyTelegram.SessionServer/Dockerfile index 3f373aa6..87797c0d 100644 --- a/source/src/MyTelegram.SessionServer/Dockerfile +++ b/source/src/MyTelegram.SessionServer/Dockerfile @@ -1,5 +1,5 @@ FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base -USER app +USER root WORKDIR /app FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build