using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Models; using Deal.Grpc.Telegram; using Deal.Infrastructure.Integrations.Models; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Grpc.Core; using Microsoft.Extensions.Logging; namespace Deal.Infrastructure.Integrations.Services; public sealed class GrpcTelegramClient : ITelegramGateway { /// /// Deadline локальных команд статуса/зеркала — 10 с. /// public const int ShortDeadlineSeconds = 10; /// /// Deadline сетевых команд Telegram — 60 с. /// public const int CommandDeadlineSeconds = 60; /// /// Deadline тяжёлых команд каталога/backfill — 120 с. /// public const int LongDeadlineSeconds = 120; private const string NotConnectedDetail = "Telegram не подключён"; private readonly ITenantContext _tenantContext; // Транспорт gRPC telegram-service (канал + metadata). private readonly TelegramGrpcConnection _connection; // Логгер сбоев вызовов telegram-service. private readonly ILogger _logger; /// /// Создаёт gRPC-адаптер гейта telegram-service. /// /// Контекст текущего тенанта (AsyncLocal, заполняется middleware/циклами). /// Транспорт telegram-service (singleton-канал + service-token). /// Логгер сбоев. public GrpcTelegramClient( ITenantContext tenantContext, TelegramGrpcConnection connection, ILogger logger) { ArgumentNullException.ThrowIfNull(tenantContext); ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(logger); _tenantContext = tenantContext; _connection = connection; _logger = logger; } async Task ITelegramGateway.StatusAsync(CancellationToken ct) { TenantId tenantId = RequireTenant(); try { GetStatusReply reply = await CallAsync( tenantId, TimeSpan.FromSeconds(ShortDeadlineSeconds), ct, (client, options) => client.GetStatusAsync(new GetStatusRequest(), options)); return new TelegramAccountStatusDto( Phase: reply.Phase, Connected: reply.Connected, Listener: reply.Listener, Account: reply.Account, Error: reply.HasError ? reply.Error : null, QrUrl: reply.HasQrUrl ? reply.QrUrl : null); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "status"); } } async Task ITelegramGateway.StartPhoneAsync( string phone, int apiId, string apiHash, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { StartPhoneReply reply = await CallAsync( tenantId, TimeSpan.FromSeconds(CommandDeadlineSeconds), ct, (client, options) => client.StartPhoneAsync( new StartPhoneRequest { Phone = phone ?? string.Empty, ApiId = apiId, ApiHash = apiHash ?? string.Empty }, options)); return new TelegramAuthResultDto(Phase: reply.Phase, QrUrl: null); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "start_phone"); } } async Task ITelegramGateway.StartQrAsync( int apiId, string apiHash, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { StartQrReply reply = await CallAsync( tenantId, TimeSpan.FromSeconds(CommandDeadlineSeconds), ct, (client, options) => client.StartQrAsync( new StartQrRequest { ApiId = apiId, ApiHash = apiHash ?? string.Empty }, options)); return new TelegramAuthResultDto( Phase: reply.Phase, QrUrl: string.IsNullOrEmpty(reply.QrUrl) ? null : reply.QrUrl); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "start_qr"); } } async Task ITelegramGateway.SendCodeAsync(string code, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { SendCodeReply reply = await CallAsync( tenantId, TimeSpan.FromSeconds(CommandDeadlineSeconds), ct, (client, options) => client.SendCodeAsync( new SendCodeRequest { Code = code ?? string.Empty }, options)); return reply.Phase; } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "send_code"); } } async Task ITelegramGateway.SendPasswordAsync(string password, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { SendPasswordReply reply = await CallAsync( tenantId, TimeSpan.FromSeconds(CommandDeadlineSeconds), ct, (client, options) => client.SendPasswordAsync( new SendPasswordRequest { Password = password ?? string.Empty }, options)); return reply.Phase; } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "send_password"); } } async Task ITelegramGateway.LogoutAsync(CancellationToken ct) { TenantId tenantId = RequireTenant(); try { await CallAsync( tenantId, TimeSpan.FromSeconds(ShortDeadlineSeconds), ct, (client, options) => client.LogoutAsync(new LogoutRequest(), options)); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "logout"); } } async Task> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct) { TenantId tenantId = RequireTenant(); try { RefreshDialogsReply reply = await CallAsync( tenantId, TimeSpan.FromSeconds(LongDeadlineSeconds), ct, (client, options) => client.RefreshDialogsAsync(new RefreshDialogsRequest(), options)); return MapEntries(reply.Entries); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "refresh_dialogs"); } } async Task ITelegramGateway.SetMonitorAsync( string dialogId, bool enabled, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { await CallAsync( tenantId, TimeSpan.FromSeconds(ShortDeadlineSeconds), ct, (client, options) => client.SetMonitorAsync( new SetMonitorRequest { DialogId = dialogId ?? string.Empty, Enabled = enabled }, options)); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "set_monitor"); } } async Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { await CallAsync( tenantId, TimeSpan.FromSeconds(ShortDeadlineSeconds), ct, (client, options) => client.SetMonitorAllAsync( new SetMonitorAllRequest { Enabled = enabled }, options)); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "set_monitor_all"); } } async Task ITelegramGateway.BackfillAsync( string dialogId, bool force, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { BackfillReply reply = await CallAsync( tenantId, TimeSpan.FromSeconds(LongDeadlineSeconds), ct, (client, options) => client.BackfillAsync( new BackfillRequest { DialogId = dialogId ?? string.Empty, Force = force }, options)); return reply.Processed; } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "backfill"); } } async Task> ITelegramGateway.ReadRecentAsync( string dialogId, int limit, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { ReadRecentReply reply = await CallAsync( tenantId, TimeSpan.FromSeconds(CommandDeadlineSeconds), ct, (client, options) => client.ReadRecentAsync( new ReadRecentRequest { DialogId = dialogId ?? string.Empty, Limit = limit }, options)); return reply.Messages .Select(message => new TelegramRecentMessageDto(message.Id, message.Text, message.Time)) .ToList(); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "read_recent"); } } async Task ITelegramGateway.ReadSourceAsync( string dialogId, long msgId, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { ReadSourceReply reply = await CallAsync( tenantId, TimeSpan.FromSeconds(CommandDeadlineSeconds), ct, (client, options) => client.ReadSourceAsync( new ReadSourceRequest { DialogId = dialogId ?? string.Empty, MsgId = msgId }, options)); return new TelegramSourceContentDto( reply.Found, reply.HasText ? reply.Text : null, reply.HasTime ? reply.Time : null); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "read_source"); } } async Task> ITelegramGateway.SearchAsync( string query, int limit, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { SearchReply reply = await CallAsync( tenantId, TimeSpan.FromSeconds(CommandDeadlineSeconds), ct, (client, options) => client.SearchAsync( new SearchRequest { Query = query ?? string.Empty, Limit = limit }, options)); return MapEntries(reply.Results); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "search"); } } async Task ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { GetInfoReply reply = await CallAsync( tenantId, TimeSpan.FromSeconds(CommandDeadlineSeconds), ct, (client, options) => client.GetInfoAsync( new GetInfoRequest { DialogId = dialogId ?? string.Empty }, options)); ChannelInfo info = reply.Info; return new TelegramChannelInfoDto( Id: info.Id, Name: info.Name, Username: info.Username, Kind: info.Kind, Hue: info.Hue, Participants: info.HasParticipants ? info.Participants : null, IsForum: info.IsForum); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "get_info"); } } async Task ITelegramGateway.ReadForEvalAsync( string dialogId, int limit, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { ReadForEvalReply reply = await CallAsync( tenantId, TimeSpan.FromSeconds(CommandDeadlineSeconds), ct, (client, options) => client.ReadForEvalAsync( new ReadForEvalRequest { DialogId = dialogId ?? string.Empty, Limit = limit }, options)); return new TelegramEvalReadDto( Ok: reply.Ok, Error: reply.HasError ? reply.Error : null, Messages: reply.Messages .Select(message => new TelegramEvalMessageDto( Id: message.Id, Text: message.Text, DateMs: message.DateMs, TopicId: message.HasTopicId ? message.TopicId : null, TopicTitle: message.HasTopicTitle ? message.TopicTitle : null)) .ToList()); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "read_for_eval"); } } async Task ITelegramGateway.JoinAsync(string username, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { await CallAsync( tenantId, TimeSpan.FromSeconds(CommandDeadlineSeconds), ct, (client, options) => client.JoinAsync( new JoinRequest { Username = username ?? string.Empty }, options)); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "join"); } } async Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct) { TenantId tenantId = RequireTenant(); try { await CallAsync( tenantId, TimeSpan.FromSeconds(CommandDeadlineSeconds), ct, (client, options) => client.LeaveAsync( new LeaveRequest { DialogId = dialogId ?? string.Empty }, options)); } catch (Exception exception) { throw TranslateTransportFailure(exception, tenantId, "leave"); } } // Текущий тенант scope (без него metadata вызовов не имеет смысла). // Возвращает: Идентификатор тенанта. // Исключение InvalidOperationException: Вызов вне tenant-контекста (конфигурация). private TenantId RequireTenant() => _tenantContext.TenantId ?? throw new InvalidOperationException( "GrpcTelegramClient запрошен вне tenant-контекста (ITenantContext.TenantId == null)."); private async Task CallAsync( TenantId tenantId, TimeSpan deadline, CancellationToken ct, Func> call) where TReply : class { TelegramService.TelegramServiceClient client = _connection.CreateClient(); var options = new CallOptions( headers: _connection.CreateMetadata(tenantId.Value), deadline: DateTime.UtcNow.Add(deadline), cancellationToken: ct); return await call(client, options); } private Exception TranslateTransportFailure( Exception exception, TenantId tenantId, string operation) { // Отмена по токену вызывающего — не ошибка сервиса (пробрасываем как обычно). if (exception is OperationCanceledException) { return exception; } if (exception is RpcException rpc && (rpc.StatusCode != StatusCode.Unavailable || !string.IsNullOrEmpty(rpc.Status.Detail))) { return rpc; } _logger.LogWarning(exception, "Telegram {Operation} недоступен (тенант {TenantId})", operation, tenantId.Value); return new RpcException(new Status(StatusCode.Unavailable, NotConnectedDetail)); } private static IReadOnlyList MapEntries(Google.Protobuf.Collections.RepeatedField entries) { return entries .Select(entry => new TelegramDialogEntryDto( Id: entry.Id, Name: entry.Name, Handle: entry.Username, Kind: entry.Kind, Hue: entry.Hue)) .ToList(); } }