Deal.Api/Http -> Services/Models/Extensions; Contracts/Integrations и SharedKernel/Tenants -> Abstractions/Models; extension-классы telegram/ml -> Extensions. namespace/using/FQN мигрированы, using дедуплицированы.
113 lines
5.5 KiB
C#
113 lines
5.5 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// Сборка статуса вкладки Telegram — GET /api/tg/status (Ruling 8, api-map §4.9 L357–359).
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Форма 1:1 с status() python L103–119 в терминах этапа 6:
|
||
/// <list type="bullet">
|
||
/// <item>live-поля (phase/connected/listener/error/qrUrl) — из гейта <see cref="ITelegramGateway.StatusAsync"/>
|
||
/// (живой telegram-service); сервис недоступен/сессии нет (RPC-отказ) → idle-форма (Ruling 8);</item>
|
||
/// <item>account — из KV tgAccount (источник истины — ReportStatus ингресса, Ruling 7);</item>
|
||
/// <item>monitored — count(Dialogs WHERE Monitor) ядра (DialogsService.ListMonitoredIds);</item>
|
||
/// <item>keysSet — оба глобальных ключа приложения заданы оператором (TelegramKeysService, Ruling 3, ТЗ §4.1/§8.1).</item>
|
||
/// </list>
|
||
/// Scoped: зависимости живут на контекстах запроса (ISettingsStore — схема тенанта, IGlobalSettingsStore — public).
|
||
/// </remarks>
|
||
/// <param name="gateway">Порт-гейт telegram-service (живой статус аккаунта).</param>
|
||
/// <param name="dialogs">Сервис каталога диалогов ядра (счётчик мониторящихся).</param>
|
||
/// <param name="settings">KV-хранилище настроек тенанта (tgAccount).</param>
|
||
/// <param name="keys">Глобальные ключи приложения Telegram (keysSet).</param>
|
||
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,
|
||
};
|
||
|
||
/// <summary>
|
||
/// Форма GET /api/tg/status текущего тенанта (поля §4.9).
|
||
/// </summary>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Полный статус вкладки Telegram.</returns>
|
||
public async Task<TgStatusDto> 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<TelegramAccountStatusDto> 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<string> 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;
|
||
}
|
||
}
|
||
}
|