using System.Text.Json; using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Models; using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; using Deal.Modules.Telegram.Application; using Deal.Modules.Telegram.Application.Models; using Deal.Api.Services; using Deal.Api.Dtos; namespace Deal.Api.Telegram; /// /// Сборка статуса вкладки Telegram — GET /api/tg/status (Ruling 8, api-map §4.9 L357–359). /// /// /// Форма 1:1 с status() python L103–119 в терминах этапа 6: /// /// live-поля (phase/connected/listener/error/qrUrl) — из гейта /// (живой telegram-service); сервис недоступен/сессии нет (RPC-отказ) → idle-форма (Ruling 8); /// account — из KV tgAccount (источник истины — ReportStatus ингресса, Ruling 7); /// monitored — count(Dialogs WHERE Monitor) ядра (DialogsService.ListMonitoredIds); /// keysSet — оба глобальных ключа приложения заданы оператором (TelegramKeysService, Ruling 3, ТЗ §4.1/§8.1). /// /// Scoped: зависимости живут на контекстах запроса (ISettingsStore — схема тенанта, IGlobalSettingsStore — public). /// /// Порт-гейт telegram-service (живой статус аккаунта). /// Сервис каталога диалогов ядра (счётчик мониторящихся). /// KV-хранилище настроек тенанта (tgAccount). /// Глобальные ключи приложения Telegram (keysSet). public sealed class TgStatusService( ITelegramGateway gateway, DialogsService dialogs, ISettingsStore settings, TelegramKeysService keys) { // Фаза idle-формы (аккаунт не подключён/сервис недоступен — Ruling 8). private const string IdlePhase = "idle"; // Опции JSON KV-значений статуса: camelCase (как пишет ингресс) + терпимость регистра. private static readonly JsonSerializerOptions KvJsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, }; /// /// Форма GET /api/tg/status текущего тенанта (поля §4.9). /// /// Токен отмены. /// Полный статус вкладки Telegram. public async Task GetAsync(CancellationToken ct) { TelegramAccountStatusDto live = await ReadLiveAsync(ct).ConfigureAwait(false); string account = await ReadAccountAsync(ct).ConfigureAwait(false); int monitored = (await dialogs.ListMonitoredIdsAsync(ct).ConfigureAwait(false)).Count; TgKeysSnapshot snapshot = await keys.GetAsync(ct).ConfigureAwait(false); return new TgStatusDto( Phase: live.Phase, Connected: live.Connected, Listener: live.Listener, Account: account, Monitored: monitored, KeysSet: snapshot.KeysSet, Error: live.Error, QrUrl: live.QrUrl); } // Живой статус из гейта; сбой (сервис недоступен/нет сессии) → idle-форма (Ruling 8). // ct: Токен отмены. // Возвращает: Статус гейта либо idle-поля. private async Task ReadLiveAsync(CancellationToken ct) { try { return await gateway.StatusAsync(ct).ConfigureAwait(false); } catch (Exception exception) when (exception is not OperationCanceledException || !ct.IsCancellationRequested) { // «Сервис недоступен → idle-форма» (Ruling 8): connected=false, live-поля пусты. Аккаунт/счётчики // ядро всё равно докладывает из своего KV/БД (ниже) — как python при отключённом клиенте. return new TelegramAccountStatusDto(IdlePhase, Connected: false, Listener: false, string.Empty, null, null); } } // Аккаунт «@username» из KV tgAccount (JSON-строка, пишет ReportStatus ингресса, Ruling 7). // ct: Токен отмены. // Возвращает: Аккаунт или пустая строка. private async Task ReadAccountAsync(CancellationToken ct) { SettingValue? row = await settings.GetAsync(SettingsKeys.TgAccount, ct).ConfigureAwait(false); if (row is null) { return string.Empty; } try { using JsonDocument document = JsonDocument.Parse(row.ValueJson); return document.RootElement.ValueKind == JsonValueKind.String ? document.RootElement.GetString() ?? string.Empty : string.Empty; } catch (JsonException) { return string.Empty; } } }