using System.Globalization; using Deal.Telegram.Caching; using Deal.Telegram.Sessions; using Grpc.Core; using TL; using WTelegram; using RpcException = TL.RpcException; namespace Deal.Telegram.Telegram; #pragma warning disable CS0618 // Auth_SendCode/Auth_SignIn используются осознанно: ручной веб-вход 1:1 с прототипом /// /// Реальная реализация поверх WTelegramClient. /// public sealed class WTelegramSessionClient : ISessionClient { private readonly Client _client; private readonly int _apiId; private readonly string _apiHash; private byte[]? _latestSessionBytes; private bool _connected; private string? _phone; private string? _phoneCodeHash; private bool _phoneAlreadyAuthorized; // Ёмкость LRU-кэша access_hash сущностей (ограничивает память на длинной истории диалогов). private const int AccessHashCacheCapacity = 2000; // Ёмкость LRU-кэша сущностей чатов/каналов (последние использованные имена/forum). private const int ChatEntityCacheCapacity = 1000; // Ёмкость LRU-кэша сущностей пользователей (последние использованные имена). private const int UserEntityCacheCapacity = 2000; // LRU-кэш access_hash сущностей (ключ — подписанный id диалога; заполняется из ответов). private readonly LruCache _entityAccessHashes = new(AccessHashCacheCapacity); private readonly LruCache _chatsById = new(ChatEntityCacheCapacity); private readonly LruCache _usersById = new(UserEntityCacheCapacity); // Защита кэшей сущностей (обновляются из потоков reactor/вызовов). private readonly object _entityCacheGate = new(); // Штатный UpdateManager библиотеки: единственный нормализованный колбэк на каждое обновление // (порядок/pts, восстановление пропусков через getDifference, дедупликация устаревших). // Новые сообщения всех типов приходят как TL.UpdateNewMessage (каналы/супергруппы — // подкласс UpdateNewChannelMessage; короткие UpdateShort* — синтез в UpdateList). private readonly UpdateManager _updateManager; /// /// Создаёт клиент WTelegramClient с сессией в памяти. /// /// api_id приложения Telegram. /// api_hash приложения Telegram (же ключ внутреннего шифрования сессии). /// Сохранённые байты сессии (null — новая сессия). public WTelegramSessionClient( int apiId, string apiHash, byte[]? storedSession) { _apiId = apiId; _apiHash = apiHash; _client = new Client(ConfigProvider, storedSession ?? [], OnSessionSaved); _updateManager = new UpdateManager(_client, OnSingleUpdateAsync); } /// public bool IsAuthorized => _client.UserId != 0; /// public bool IsConnected => _connected && !_client.Disconnected; /// public int ApiId => _apiId; /// public string ApiHash => _apiHash; /// public byte[]? SessionBytes => Volatile.Read(ref _latestSessionBytes); /// public async Task ConnectAsync(CancellationToken cancellationToken) { if (IsConnected) { return; } await _client.ConnectAsync().WaitAsync(cancellationToken).ConfigureAwait(false); _connected = true; } /// public async Task RequestCodeAsync(string phone, CancellationToken cancellationToken) { _phone = phone; _phoneCodeHash = null; _phoneAlreadyAuthorized = false; Auth_SentCodeBase sentCode = await SendCodeOnceAsync(phone, cancellationToken).ConfigureAwait(false); switch (sentCode) { case Auth_SentCode { phone_code_hash: not null } code: _phoneCodeHash = code.phone_code_hash; return; case Auth_SentCodeSuccess: // Номер уже авторизован в этой сессии: код не нужен (SubmitCodeAsync вернёт готово). _phoneAlreadyAuthorized = true; return; default: throw new SessionException( StatusCode.InvalidArgument, "Telegram не запросил код для этого номера — повторите вход по номеру телефона"); } } /// public async Task SubmitCodeAsync(string code, CancellationToken cancellationToken) { if (_phoneAlreadyAuthorized) { return null; } if (_phone is null || _phoneCodeHash is null) { throw new SessionException(StatusCode.FailedPrecondition, SessionErrorMessages.CodeNotRequested); } Auth_AuthorizationBase authorization; try { authorization = await _client.Auth_SignIn(_phone, _phoneCodeHash, code).WaitAsync(cancellationToken).ConfigureAwait(false); } catch (RpcException exception) when (exception.Code == 401 && exception.Message == "SESSION_PASSWORD_NEEDED") { // Включён 2FA — следующий шаг: облачный пароль (submit_password). return "password"; } catch (RpcException exception) when (exception.Code == 400 && exception.Message == "PHONE_CODE_INVALID") { throw new SessionException(StatusCode.InvalidArgument, SessionErrorMessages.WrongCode, exception); } catch (RpcException exception) when (exception.Code == 400 && exception.Message == "PHONE_CODE_EXPIRED") { throw new SessionException(StatusCode.InvalidArgument, SessionErrorMessages.CodeExpired, exception); } catch (RpcException exception) { throw MapRpcException(exception); } CompleteAuthorization(authorization); return null; } /// public async Task SubmitPasswordAsync(string password, CancellationToken cancellationToken) { try { Account_Password accountPassword = await _client.Account_GetPassword().WaitAsync(cancellationToken).ConfigureAwait(false); InputCheckPasswordSRP checkPassword = await Client.InputCheckPassword(accountPassword, password).WaitAsync(cancellationToken).ConfigureAwait(false); Auth_AuthorizationBase authorization = await _client.Auth_CheckPassword(checkPassword).WaitAsync(cancellationToken).ConfigureAwait(false); CompleteAuthorization(authorization); } catch (RpcException exception) when (exception.Code == 400 && exception.Message == "PASSWORD_HASH_INVALID") { throw new SessionException(StatusCode.InvalidArgument, SessionErrorMessages.WrongPassword, exception); } catch (RpcException exception) { throw MapRpcException(exception); } } /// public async Task StartQrAsync(Action onQrUrl, CancellationToken cancellationToken) { try { // logoutFirst=false: сессия без пользователя (проверено TenantSession до запуска QR); // завершившись, библиотека сама сохраняет авторизованную сессию в память (LoginAlreadyDone). await _client .LoginWithQRCode(url => onQrUrl(url), except_ids: null, logoutFirst: false, ct: cancellationToken) .WaitAsync(cancellationToken) .ConfigureAwait(false); } catch (RpcException exception) { throw MapRpcException(exception); } } /// public async Task LogOutAsync(CancellationToken cancellationToken) { await _client.Auth_LogOut().WaitAsync(cancellationToken).ConfigureAwait(false); } /// public async Task GetAccountAsync(CancellationToken cancellationToken) { UserBase[] users = await _client.Users_GetUsers(InputUser.Self).WaitAsync(cancellationToken).ConfigureAwait(false); string username = users.OfType().FirstOrDefault()?.username ?? string.Empty; return "@" + (string.IsNullOrEmpty(username) ? "user" : username); } /// public ValueTask DisposeAsync() { _connected = false; return _client.DisposeAsync(); } /// public event Func? MessageReceived; /// public async Task> GetDialogsAsync(int limit, CancellationToken cancellationToken) { Messages_DialogsBase result = await RunTlCallAsync(() => _client.Messages_GetDialogs(limit: limit), cancellationToken).ConfigureAwait(false); (DialogBase[] dialogs, Dictionary chats, Dictionary users) = UnpackDialogs(result); CacheEntities(chats.Values, users.Values); var items = new List(dialogs.Length); foreach (DialogBase dialog in dialogs) { TelegramDialog? item = TlMessageMapper.ToDialog(dialog, chats, users); if (item is not null) { items.Add(item); } } return items; } /// public async Task> GetMessagesAsync( string dialogId, int limit, CancellationToken cancellationToken) { InputPeer peer = await ResolvePeerAsync(dialogId, cancellationToken).ConfigureAwait(false); Messages_MessagesBase result = await RunTlCallAsync(() => _client.Messages_GetHistory(peer, limit: limit), cancellationToken).ConfigureAwait(false); (MessageBase[] messages, Dictionary chats, Dictionary users) = UnpackMessages(result); CacheEntities(chats.Values, users.Values); var items = new List(messages.Length); foreach (MessageBase message in messages) { TelegramMessage? item = TlMessageMapper.ToMessage(message, chats, users); if (item is not null) { items.Add(item); } } return items; } /// public async Task GetMessageAsync( string dialogId, long msgId, CancellationToken cancellationToken) { // Диалог обязан быть доступен аккаунту (иначе INVALID_ARGUMENT, как в истории). await ResolvePeerAsync(dialogId, cancellationToken).ConfigureAwait(false); // messages.getMessages по голому id (InputMessageID) — без привязки к peer. InputMessage[] ids = [new InputMessageID { id = (int)msgId }]; Messages_MessagesBase result = await RunTlCallAsync( () => _client.Messages_GetMessages(ids), cancellationToken).ConfigureAwait(false); (MessageBase[] messages, Dictionary chats, Dictionary users) = UnpackMessages(result); CacheEntities(chats.Values, users.Values); foreach (MessageBase message in messages) { TelegramMessage? mapped = TlMessageMapper.ToMessage(message, chats, users); if (mapped is not null && mapped.Id == msgId && mapped.DialogId == dialogId) { return mapped; } } // Медиа без текста или удалённое сообщение — не найдено (found=false). return null; } /// public async Task MarkReadAsync(string dialogId, CancellationToken cancellationToken) { InputPeer peer = await ResolvePeerAsync(dialogId, cancellationToken).ConfigureAwait(false); await RunTlCallAsync(() => _client.ReadHistory(peer), cancellationToken).ConfigureAwait(false); } /// public async Task> SearchAsync( string query, int limit, CancellationToken cancellationToken) { // contacts.SearchRequest(q, limit): результат — сущности chats/users (поиск каналов/групп/людей). Contacts_Found found = await RunTlCallAsync(() => _client.Contacts_Search(query, limit), cancellationToken).ConfigureAwait(false); CacheEntities(found.chats.Values, found.users.Values); var items = new List(found.chats.Count + found.users.Count); foreach (ChatBase chat in found.chats.Values) { items.Add(TlMessageMapper.ToFoundChat(chat)); } foreach (User user in found.users.Values) { items.Add(TlMessageMapper.ToFoundUser(user)); } return items; } /// public async Task GetInfoAsync(string dialogId, CancellationToken cancellationToken) { TelegramSourceInfo unknown = DefaultSourceInfo(dialogId); if (!TryParseSignedId(dialogId, out bool isChannel, out bool isChat, out bool isUser, out long rawId)) { return unknown; } try { if (isChannel) { return await GetChannelInfoAsync(dialogId, rawId, unknown, cancellationToken).ConfigureAwait(false); } if (isChat) { return await GetChatInfoAsync(dialogId, rawId, unknown, cancellationToken).ConfigureAwait(false); } if (isUser) { return GetUserInfo(dialogId, rawId, unknown); } } catch (SessionException) { return unknown; } return unknown; } /// public async Task ReadForEvalAsync( string dialogId, int limit, CancellationToken cancellationToken) { if (limit <= 0) { return DiscoveryReadResult.Empty; } if (!TryParseSignedId(dialogId, out bool isChannel, out bool isChat, out bool isUser, out long rawId)) { return DiscoveryReadResult.NoHistory(); } InputPeer peer; try { peer = await ResolvePeerAsync(dialogId, cancellationToken).ConfigureAwait(false); } catch (SessionException) { return DiscoveryReadResult.NoHistory(); } if (isChannel && IsForumChannel(rawId)) { IReadOnlyList forumMessages = await ReadForumTopicsAsync(peer, limit, cancellationToken).ConfigureAwait(false); if (forumMessages.Count > 0) { return new DiscoveryReadResult(true, null, forumMessages); } } // Обычная лента (каналы/группы; для форума — General-тема; форум без читаемых тем → тоже лента). try { Messages_MessagesBase result = await RunTlCallAsync(() => _client.Messages_GetHistory(peer, limit: limit), cancellationToken).ConfigureAwait(false); (MessageBase[] messages, Dictionary chats, Dictionary users) = UnpackMessages(result); CacheEntities(chats.Values, users.Values); return new DiscoveryReadResult(true, null, MapEvalMessages(messages)); } catch (SessionException) { return DiscoveryReadResult.NoHistory(); } } /// public async Task JoinAsync(string username, CancellationToken cancellationToken) { Contacts_ResolvedPeer resolved = await RunTlCallAsync(() => _client.Contacts_ResolveUsername(username), cancellationToken).ConfigureAwait(false); CacheEntities(resolved.chats.Values, resolved.users.Values); Channel? channel = resolved.chats.Values.OfType().FirstOrDefault(); if (channel is null) { throw new SessionException(StatusCode.InvalidArgument, SessionErrorMessages.JoinTargetNotChannel); } await RunTlCallAsync(() => _client.Channels_JoinChannel(new InputChannel(channel.id, channel.access_hash)), cancellationToken).ConfigureAwait(false); } /// public async Task LeaveAsync(string dialogId, CancellationToken cancellationToken) { if (!TryParseSignedId(dialogId, out bool isChannel, out _, out _, out long rawId) || !isChannel) { throw new SessionException(StatusCode.InvalidArgument, SessionErrorMessages.InvalidDialogId); } InputChannel channel = await ResolveInputChannelAsync(dialogId, rawId, cancellationToken).ConfigureAwait(false); await RunTlCallAsync(() => _client.Channels_LeaveChannel(channel), cancellationToken).ConfigureAwait(false); } private async Task GetChannelInfoAsync( string dialogId, long rawId, TelegramSourceInfo unknown, CancellationToken cancellationToken) { Channel? cached; lock (_entityCacheGate) { cached = _chatsById.TryGetValue(rawId, out ChatBase? chat) ? chat as Channel : null; } if (cached is not null) { int? participants = await TryFetchChannelParticipantsAsync(dialogId, rawId, cancellationToken).ConfigureAwait(false); return DescribeChannel(dialogId, cached, participants); } InputChannel input = await ResolveInputChannelAsync(dialogId, rawId, cancellationToken).ConfigureAwait(false); Messages_ChatFull full = await RunTlCallAsync(() => _client.Channels_GetFullChannel(input), cancellationToken).ConfigureAwait(false); CacheEntities(full.chats.Values, full.users.Values); Channel? channel = full.chats.TryGetValue(rawId, out ChatBase? found) ? found as Channel : null; if (channel is null) { return unknown; } int? fullParticipants = (full.full_chat as ChannelFull)?.participants_count is int count && count > 0 ? count : null; return DescribeChannel(dialogId, channel, fullParticipants); } private async Task TryFetchChannelParticipantsAsync( string dialogId, long rawId, CancellationToken cancellationToken) { try { InputChannel input = await ResolveInputChannelAsync(dialogId, rawId, cancellationToken).ConfigureAwait(false); Messages_ChatFull full = await RunTlCallAsync(() => _client.Channels_GetFullChannel(input), cancellationToken).ConfigureAwait(false); CacheEntities(full.chats.Values, full.users.Values); return (full.full_chat as ChannelFull)?.participants_count is int count && count > 0 ? count : null; } catch (SessionException) { return null; } } // Инфо о базовой группе: entity из кэша/полного чата, участники — GetFullChat (только при членстве). // dialogId: Подписанный id. // rawId: Raw id группы (положительный). // unknown: Инфо по умолчанию. // cancellationToken: Отмена операции. private async Task GetChatInfoAsync( string dialogId, long rawId, TelegramSourceInfo unknown, CancellationToken cancellationToken) { Chat? cached; lock (_entityCacheGate) { cached = _chatsById.TryGetValue(rawId, out ChatBase? chat) ? chat as Chat : null; } if (cached is not null) { int? participants = await TryFetchBasicParticipantsAsync(rawId, cancellationToken).ConfigureAwait(false); return DescribeGroup(dialogId, cached, participants); } Messages_ChatFull full = await RunTlCallAsync(() => _client.Messages_GetFullChat(rawId), cancellationToken).ConfigureAwait(false); CacheEntities(full.chats.Values, full.users.Values); Chat? group = full.chats.TryGetValue(rawId, out ChatBase? found) ? found as Chat : null; if (group is null) { return unknown; } int? fullParticipants = full.full_chat is ChatFull fullChat && fullChat.participants is ChatParticipants members ? members.participants.Length : null; return DescribeGroup(dialogId, group, fullParticipants); } private async Task TryFetchBasicParticipantsAsync(long rawId, CancellationToken cancellationToken) { try { Messages_ChatFull full = await RunTlCallAsync(() => _client.Messages_GetFullChat(rawId), cancellationToken).ConfigureAwait(false); CacheEntities(full.chats.Values, full.users.Values); return full.full_chat is ChatFull fullChat && fullChat.participants is ChatParticipants members ? members.participants.Length : null; } catch (SessionException) { return null; } } // Собирает инфо базовой группы из entity (имя/username/kind group, без форума). // dialogId: Подписанный id. // group: Entity группы. // participants: Число участников (null — определить не удалось). private static TelegramSourceInfo DescribeGroup( string dialogId, Chat group, int? participants) => new( dialogId, DisplayName(dialogId, group), group.MainUsername ?? string.Empty, DialogKinds.Group, participants, isForum: false); private TelegramSourceInfo GetUserInfo( string dialogId, long rawId, TelegramSourceInfo unknown) { User? user; lock (_entityCacheGate) { _usersById.TryGetValue(rawId, out user); } if (user is null) { return unknown; } string name = (user.first_name + " " + user.last_name).Trim(); string username = user.username ?? string.Empty; if (name.Length == 0) { name = username.Length > 0 ? username : dialogId; } return new TelegramSourceInfo(dialogId, name, username, DialogKinds.Chat, participants: null, isForum: false); } private async Task> ReadForumTopicsAsync( InputPeer peer, int limit, CancellationToken cancellationToken) { var outMessages = new List(); try { Messages_ForumTopics forum = await RunTlCallAsync( () => _client.Messages_GetForumTopics(peer, offset_date: default, offset_id: 0, offset_topic: 0, limit: ForumTopicsLimit), cancellationToken).ConfigureAwait(false); CacheEntities(forum.chats.Values, forum.users.Values); ForumTopic[] topics = forum.topics.OfType().ToArray(); if (topics.Length == 0) { return outMessages; } int perTopic = Math.Min(Math.Max(3, (int)Math.Ceiling(limit / (double)topics.Length)), ForumMessagesPerTopicCap); DateTimeOffset now = DateTimeOffset.UtcNow; foreach (ForumTopic topic in topics) { try { Messages_MessagesBase result = await RunTlCallAsync( () => _client.Messages_GetReplies(peer, topic.id, limit: perTopic), cancellationToken).ConfigureAwait(false); (MessageBase[] messages, Dictionary chats, Dictionary users) = UnpackMessages(result); CacheEntities(chats.Values, users.Values); foreach (MessageBase message in messages) { DiscoveryMessage? item = TlMessageMapper.ToEvalMessage(message, topic.id, topic.title, now); if (item is not null) { outMessages.Add(item); } } } catch (SessionException) { } } } catch (SessionException) { outMessages.Clear(); } return outMessages; } // Маппит сообщения ленты/тем в нейтральные (только непустые тексты, темы пусты). // messages: Сообщения выборки (MessageBase). private static IReadOnlyList MapEvalMessages(MessageBase[] messages) { var items = new List(messages.Length); DateTimeOffset now = DateTimeOffset.UtcNow; foreach (MessageBase message in messages) { DiscoveryMessage? item = TlMessageMapper.ToEvalMessage(message, null, null, now); if (item is not null) { items.Add(item); } } return items; } // Инфо по умолчанию (сущность недоступна): name=id, username/kind пусты, без участников. // dialogId: Подписанный id источника. private static TelegramSourceInfo DefaultSourceInfo(string dialogId) => new(dialogId, dialogId, string.Empty, string.Empty, participants: null, isForum: false); // Собирает инфо канала из entity полного чата (имя/username/kind/forum + участники). // dialogId: Подписанный id. // channel: Entity канала из ответа полного чата. // participants: Число участников (full_chat) либо null. private static TelegramSourceInfo DescribeChannel( string dialogId, Channel channel, int? participants) => new( dialogId, DisplayName(dialogId, channel), channel.MainUsername ?? string.Empty, TlMessageMapper.KindOf(channel), participants, isForum: (channel.flags & Channel.Flags.forum) != 0); private static string DisplayName(string dialogId, ChatBase chat) { string title = chat.Title ?? string.Empty; return title.Length > 0 ? title : dialogId; } private bool IsForumChannel(long rawId) { lock (_entityCacheGate) { return _chatsById.TryGetValue(rawId, out ChatBase? chat) && chat is Channel channel && (channel.flags & Channel.Flags.forum) != 0; } } private const int ForumTopicsLimit = 5; private const int ForumMessagesPerTopicCap = 10; private async Task OnSingleUpdateAsync(Update update) { try { // TlMessageMapper.NewMessageFrom: UpdateNewMessage/UpdateNewChannelMessage (каналы — // подкласс) и синтезированные из коротких вариантов; прочие обновления игнорируются. MessageBase? message = TlMessageMapper.NewMessageFrom(update); if (message is null) { return; } // UpdateManager собрал сущности сообщения в коллектор (Users/Chats) до вызова колбэка — // имя/username диалога и access_hash (для read-ack) доступны без сетевых запросов. Dictionary chats = _updateManager.Chats; Dictionary users = _updateManager.Users; TelegramMessage? item = TlMessageMapper.ToMessage(message, chats, users); if (item is not null) { await DispatchMessageAsync(item).ConfigureAwait(false); } } catch (Exception exception) when (exception is not OperationCanceledException) { // Сбой обработки обновления не должен останавливать realtime; ошибки подписчиков // обрабатывает TenantSession (логгер есть на уровне сессии). } } // Последовательно вызывает подписчиков события нового сообщения. // message: Входящее сообщение диалога. private async Task DispatchMessageAsync(TelegramMessage message) { Func? handler = MessageReceived; if (handler is null) { return; } foreach (Delegate subscriber in handler.GetInvocationList()) { try { await ((Func)subscriber)(message).ConfigureAwait(false); } catch (Exception exception) when (exception is not OperationCanceledException) { // Исключение подписчика (например, ядро недоступно) логирует сам подписчик // (TenantSession) и не влияет на остальных. } } } // Возвращает значения конфигурации WTelegramClient (api_id/api_hash; остальное — по умолчанию). // what: Запрашиваемый библиотекой ключ конфигурации. private string? ConfigProvider(string what) => what switch { "api_id" => _apiId.ToString(System.Globalization.CultureInfo.InvariantCulture), "api_hash" => _apiHash, _ => null, }; // Колбэк сохранения сессии библиотекой: держим последние байты в памяти процесса. // sessionBytes: Новые байты сессии (готовый самодостаточный блок). private void OnSessionSaved(byte[] sessionBytes) => Volatile.Write(ref _latestSessionBytes, sessionBytes); private async Task SendCodeOnceAsync(string phone, CancellationToken cancellationToken) { try { return await _client.Auth_SendCode(phone, _apiId, _apiHash, new CodeSettings()).WaitAsync(cancellationToken).ConfigureAwait(false); } catch (RpcException exception) when (exception.Code == 500 && exception.Message == "AUTH_RESTART") { return await _client.Auth_SendCode(phone, _apiId, _apiHash, new CodeSettings()).WaitAsync(cancellationToken).ConfigureAwait(false); } catch (RpcException exception) { throw MapRpcException(exception); } } // Завершает авторизацию по объекту Auth_AuthorizationBase: регистрирует пользователя в сессии // (LoginAlreadyDone сохраняет сессию — колбэк OnSessionSaved обновит байты для файла). // authorization: Ответ Auth_SignIn/Auth_CheckPassword. private void CompleteAuthorization(Auth_AuthorizationBase authorization) { if (authorization is Auth_AuthorizationSignUpRequired) { throw new SessionException(StatusCode.InvalidArgument, SessionErrorMessages.SignUpRequired); } try { _client.LoginAlreadyDone(authorization); } catch (WTException exception) { throw new SessionException(StatusCode.Internal, exception.Message, exception); } } private static SessionException MapRpcException(RpcException exception) { if (exception.Message.StartsWith("FLOOD_WAIT_", StringComparison.Ordinal)) { return new SessionException(StatusCode.ResourceExhausted, "flood: " + exception.Message, exception); } if (exception.Code == 400) { return new SessionException(StatusCode.InvalidArgument, exception.Message, exception); } return new SessionException(StatusCode.Unavailable, SessionErrorMessages.TelegramUnavailable, exception); } // Исполняет TL-вызов с единым переводом ошибок (RpcException Telegram → SessionException; // прочие сбои — UNAVAILABLE «Telegram недоступен…»). // TResult: Тип результата TL-вызова. // call: Фабрика задачи TL-вызова (методы библиотеки токенов не принимают). // cancellationToken: Отмена операции. private async Task RunTlCallAsync(Func> call, CancellationToken cancellationToken) { try { return await call().WaitAsync(cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (RpcException exception) { throw MapRpcException(exception); } catch (Exception exception) { throw new SessionException(StatusCode.Unavailable, SessionErrorMessages.TelegramUnavailable, exception); } } // Разворачивает результат messages.getDialogs в диалоги и словари сущностей. // result: Результат TL-вызова списка диалогов. private static (DialogBase[] Dialogs, Dictionary Chats, Dictionary Users) UnpackDialogs(Messages_DialogsBase result) => result switch { // Slice наследует Messages_Dialogs — сначала конкретный тип (иначе ветка недостижима). Messages_DialogsSlice slice => (slice.dialogs, slice.chats, slice.users), Messages_Dialogs dialogs => (dialogs.dialogs, dialogs.chats, dialogs.users), _ => throw new SessionException(StatusCode.Internal, "Telegram вернул неподдерживаемый список диалогов"), }; // Разворачивает результат messages.getHistory в сообщения и словари сущностей. // result: Результат TL-вызова истории. private static (MessageBase[] Messages, Dictionary Chats, Dictionary Users) UnpackMessages(Messages_MessagesBase result) => result switch { Messages_Messages messages => (messages.messages, messages.chats, messages.users), Messages_ChannelMessages channelMessages => (channelMessages.messages, channelMessages.chats, channelMessages.users), // Прочие варианты (rare/новые слои) — сообщения отдаёт базовый доступор, сущностей нет: // имена диалогов в таких сообщениях будут подменены id (кадр редкий, реальный поток не страдает). _ => (result.Messages, new Dictionary(), new Dictionary()), }; // Кэширует access_hash и сущности из ответа (для InputPeer по id и имён/forum discovery). // chats: Каналы/группы ответа (по raw id). // users: Пользователи ответа (по raw id). private void CacheEntities(IEnumerable chats, IEnumerable users) { lock (_entityCacheGate) { foreach (ChatBase chat in chats) { switch (chat) { case Channel channel: _entityAccessHashes.Set("-100" + channel.id.ToString(CultureInfo.InvariantCulture), channel.access_hash); _chatsById.Set(channel.id, channel); break; case Chat group: _chatsById.Set(group.id, group); break; } } foreach (UserBase user in users) { if (user is User regularUser) { _entityAccessHashes.Set("+" + regularUser.id.ToString(CultureInfo.InvariantCulture), regularUser.access_hash); _usersById.Set(regularUser.id, regularUser); } } } } // Строит InputPeer диалога; при пустом кэше — доливка списком диалогов (первая страница). // dialogId: Подписанный id диалога. // cancellationToken: Отмена операции. private async Task ResolvePeerAsync(string dialogId, CancellationToken cancellationToken) { if (!TryParseSignedId(dialogId, out bool isChannel, out bool isChat, out bool isUser, out long rawId)) { throw new SessionException(StatusCode.InvalidArgument, SessionErrorMessages.InvalidDialogId); } InputPeer? peer = BuildPeer(isChannel, isChat, isUser, rawId); if (peer is not null) { return peer; } // Сущность может быть уже собрана коллектором UpdateManager (realtime-сообщение пришло до // списка диалогов) — access_hash берётся оттуда без сетевых запросов. if (TryImportPeerFromCollector(isChannel, isUser, rawId)) { InputPeer? imported = BuildPeer(isChannel, isChat, isUser, rawId); if (imported is not null) { return imported; } } // Канал/пользователь без access_hash (рестарт/новый источник): доливаем сущности // первой страницей списка диалогов — каталог ядра строится из неё же (лимит 500, как refresh). await GetDialogsAsync(DialogResolvePageSize, cancellationToken).ConfigureAwait(false); return BuildPeer(isChannel, isChat, isUser, rawId) ?? throw new SessionException(StatusCode.InvalidArgument, SessionErrorMessages.UnknownDialog); } // InputChannel по подписанному id канала (join/leave/полный чат); как ResolvePeerAsync. // dialogId: Подписанный id канала («-100…»). // rawId: Raw id канала. // cancellationToken: Отмена операции. private async Task ResolveInputChannelAsync( string dialogId, long rawId, CancellationToken cancellationToken) { lock (_entityCacheGate) { if (_entityAccessHashes.TryGetValue(dialogId, out long cached)) { return new InputChannel(rawId, cached); } } await GetDialogsAsync(DialogResolvePageSize, cancellationToken).ConfigureAwait(false); lock (_entityCacheGate) { return _entityAccessHashes.TryGetValue(dialogId, out long accessHash) ? new InputChannel(rawId, accessHash) : throw new SessionException(StatusCode.InvalidArgument, SessionErrorMessages.UnknownDialog); } } // Импортирует access_hash сущности из коллектора UpdateManager в кэш (базовая группа хеша не // требует — возвращает true сразу). False — сущности в коллекторе нет или хеш нулевой (min-entity). // isChannel: Признак канала («-100…»). // isUser: Признак личного чата («+…»). // rawId: Raw id (канала/пользователя). private bool TryImportPeerFromCollector( bool isChannel, bool isUser, long rawId) { if (!isChannel && !isUser) { return true; } string signedId = (isChannel ? "-100" : "+") + rawId.ToString(CultureInfo.InvariantCulture); lock (_entityCacheGate) { if (_entityAccessHashes.ContainsKey(signedId)) { return true; } } long accessHash = 0; if (isChannel && _updateManager.Chats.TryGetValue(rawId, out ChatBase? chat) && chat is Channel channel) { accessHash = channel.access_hash; } else if (isUser && _updateManager.Users.TryGetValue(rawId, out User? user)) { accessHash = user.access_hash; } if (accessHash == 0) { return false; } lock (_entityCacheGate) { _entityAccessHashes.Set(signedId, accessHash); } return true; } // Строит InputPeer по разобранному id и access_hash из кэша (null — сущность неизвестна). // isChannel: Признак канала («-100…»). // isChat: Признак базовой группы («-…»). // isUser: Признак личного чата («+…»). // rawId: Raw id (канала/группы/пользователя). private InputPeer? BuildPeer( bool isChannel, bool isChat, bool isUser, long rawId) { if (isChat) { return new InputPeerChat(rawId); } string signedId = (isChannel ? "-100" : "+") + rawId.ToString(CultureInfo.InvariantCulture); long accessHash; lock (_entityCacheGate) { if (!_entityAccessHashes.TryGetValue(signedId, out accessHash)) { return null; } } return isChannel ? new InputPeerChannel(rawId, accessHash) : new InputPeerUser(rawId, accessHash); } // Разбирает подписанный id канона контракта («-100…» канал, «-…» группа, «+…» личный). // dialogId: Подписанный id. // isChannel: True — канал. // isChat: True — базовая группа. // isUser: True — личный чат. // rawId: Raw id без префикса. private static bool TryParseSignedId( string dialogId, out bool isChannel, out bool isChat, out bool isUser, out long rawId) { isChannel = false; isChat = false; isUser = false; rawId = 0; string digits; if (dialogId.StartsWith("-100", StringComparison.Ordinal)) { digits = dialogId[4..]; isChannel = true; } else if (dialogId.StartsWith('-')) { digits = dialogId[1..]; isChat = true; } else if (dialogId.StartsWith('+')) { digits = dialogId[1..]; isUser = true; } else { return false; } return digits.Length > 0 && long.TryParse(digits, NumberStyles.None, CultureInfo.InvariantCulture, out rawId); } // Размер страницы списка диалогов для доливки кэша сущностей (как refresh_dialogs limit). private const int DialogResolvePageSize = 500; } #pragma warning restore CS0618