Merge branch 'main' into t16_null_exceptions_resources
ci / build-test (pull_request) Successful in 3m1s

This commit is contained in:
2026-09-13 14:51:18 +03:00
29 changed files with 35 additions and 212 deletions
+2 -1
View File
@@ -271,7 +271,8 @@
- **Явная реализация интерфейсов — по умолчанию** (`Task ICardStore.GetAsync(...)`). **[изм. 2026-09-11, - **Явная реализация интерфейсов — по умолчанию** (`Task ICardStore.GetAsync(...)`). **[изм. 2026-09-11,
решение владельца]** Классы напрямую не вызываются — только через интерфейсы; исключения: DTO/модели решение владельца]** Классы напрямую не вызываются — только через интерфейсы; исключения: DTO/модели
(напр. `Card` и семейство `I*Card`), хелперы, extension-классы. Весь прод-код уже переведён на явные (напр. `Card` и семейство `I*Card`), хелперы, extension-классы. Весь прод-код уже переведён на явные
реализации (codemod `scripts/make_explicit.py`, идемпотентный). реализации (codemod `scripts/make_explicit.py` и `scripts/strip_implementation_docs.py` — идемпотентны,
`--apply` применяет правки, без флага — dry-run-отчёт).
- Один публичный тип интерфейса = один файл (как и для классов); имя файла = имя типа. - Один публичный тип интерфейса = один файл (как и для классов); имя файла = имя типа.
- **Маркерные классы не используются** — если нужен маркер, это маркерный интерфейс - **Маркерные классы не используются** — если нужен маркер, это маркерный интерфейс
(`IKanbanModule`, `ISharedKernel` и т.п.). **[изм. 2026-09-11]** (`IKanbanModule`, `ISharedKernel` и т.п.). **[изм. 2026-09-11]**
@@ -4,9 +4,6 @@ using System.Text.Json.Nodes;
namespace Deal.Ai.Llm; namespace Deal.Ai.Llm;
/// <summary>
/// HTTP-реализация <see cref="IProviderClient"/>
/// </summary>
public sealed class LlmHttpClient : IProviderClient public sealed class LlmHttpClient : IProviderClient
{ {
// Относительный путь OpenAI-совместимого эндпоинта (база уже без хвостового «/»). // Относительный путь OpenAI-совместимого эндпоинта (база уже без хвостового «/»).
@@ -58,13 +55,6 @@ public sealed class LlmHttpClient : IProviderClient
_anthropicCallTimeout = anthropicCallTimeout; _anthropicCallTimeout = anthropicCallTimeout;
} }
/// <summary>
/// Выполняет один вызов модели по выбранной схеме API.
/// </summary>
/// <param name="config">Конфиг провайдера (стиль — <c>ApiStyle</c>).</param>
/// <param name="systemPrompt">Системный промпт.</param>
/// <param name="userText">Пользовательское сообщение/контекст.</param>
/// <returns>Текст ответа и usage API-ответа (null при его отсутствии).</returns>
async Task<ProviderChatResult> IProviderClient.ChatAsync( async Task<ProviderChatResult> IProviderClient.ChatAsync(
LlmConfig config, LlmConfig config,
string systemPrompt, string systemPrompt,
@@ -3,22 +3,17 @@ using Deal.SharedKernel.Tenants.Models;
namespace Deal.Infrastructure.Data; namespace Deal.Infrastructure.Data;
/// <summary>
/// Контекст тенанта на AsyncLocal
/// </summary>
public sealed class TenantContext : ITenantContext public sealed class TenantContext : ITenantContext
{ {
private static readonly AsyncLocal<TenantId?> Current = new(); private static readonly AsyncLocal<TenantId?> Current = new();
public TenantId? TenantId => Current.Value; TenantId? ITenantContext.TenantId => Current.Value;
public bool HasTenant => Current.Value is not null; bool ITenantContext.HasTenant => Current.Value is not null;
public string? SchemaName => Current.Value?.SchemaName; string? ITenantContext.SchemaName => Current.Value?.SchemaName;
/// <inheritdoc />
void ITenantContext.SetTenant(TenantId tenantId) => Current.Value = tenantId; void ITenantContext.SetTenant(TenantId tenantId) => Current.Value = tenantId;
/// <inheritdoc />
void ITenantContext.Reset() => Current.Value = null; void ITenantContext.Reset() => Current.Value = null;
} }
@@ -6,9 +6,6 @@ using Deal.Modules.Settings.Application.Models;
namespace Deal.Infrastructure.Integrations.Services; namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// HTTP-реализация проверки подключения к AI-провайдеру.
/// </summary>
public sealed class AiConnectionChecker : IAiConnectionChecker public sealed class AiConnectionChecker : IAiConnectionChecker
{ {
/// <summary> /// <summary>
@@ -78,7 +75,6 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
_httpClient = httpClient; _httpClient = httpClient;
} }
/// <inheritdoc />
async Task<AiCheckResultDto> IAiConnectionChecker.CheckAsync(AiCheckRequest request, CancellationToken ct) async Task<AiCheckResultDto> IAiConnectionChecker.CheckAsync(AiCheckRequest request, CancellationToken ct)
{ {
ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(request);
@@ -9,9 +9,6 @@ using Microsoft.Extensions.Logging;
namespace Deal.Infrastructure.Integrations.Services; namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// Декоратор бюджетного гейта порта <see cref="IAiClassifier"/>
/// </summary>
public sealed class BudgetedAiClassifier : IAiClassifier public sealed class BudgetedAiClassifier : IAiClassifier
{ {
// Текст ошибки вызова вне tenant-контекста (гейт читает лимиты по тенанту). // Текст ошибки вызова вне tenant-контекста (гейт читает лимиты по тенанту).
@@ -54,7 +51,6 @@ public sealed class BudgetedAiClassifier : IAiClassifier
_logger = logger; _logger = logger;
} }
/// <inheritdoc />
async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct) async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct)
{ {
if (await IsPaidAllowedAsync(ct)) if (await IsPaidAllowedAsync(ct))
@@ -67,7 +63,6 @@ public sealed class BudgetedAiClassifier : IAiClassifier
return await _localClassifier.FilterAsync(text, ct); return await _localClassifier.FilterAsync(text, ct);
} }
/// <inheritdoc />
async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct) async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct)
{ {
if (await IsPaidAllowedAsync(ct)) if (await IsPaidAllowedAsync(ct))
@@ -9,9 +9,6 @@ using Microsoft.Extensions.Logging;
namespace Deal.Infrastructure.Integrations.Services; namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// Декоратор бюджетного гейта порта <see cref="IAiTools"/>
/// </summary>
public sealed class BudgetedAiTools : IAiTools public sealed class BudgetedAiTools : IAiTools
{ {
private const string ExhaustedKeywordsError = "ИИ-бюджет исчерпан — генерация ключевых слов недоступна"; private const string ExhaustedKeywordsError = "ИИ-бюджет исчерпан — генерация ключевых слов недоступна";
@@ -50,7 +47,6 @@ public sealed class BudgetedAiTools : IAiTools
_logger = logger; _logger = logger;
} }
/// <inheritdoc />
async Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct) async Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
{ {
BudgetStateDto state = await GateStateAsync(ct); BudgetStateDto state = await GateStateAsync(ct);
@@ -69,7 +65,6 @@ public sealed class BudgetedAiTools : IAiTools
Error: state.Status == TenantStatuses.Suspended ? SuspendedKeywordsError : ExhaustedKeywordsError); Error: state.Status == TenantStatuses.Suspended ? SuspendedKeywordsError : ExhaustedKeywordsError);
} }
/// <inheritdoc />
async Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync( async Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
string text, string text,
string description, string description,
@@ -5,9 +5,6 @@ using Microsoft.Extensions.Logging;
namespace Deal.Infrastructure.Integrations.Services; namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// HTTP-источник курсов ЦБ РФ
/// </summary>
public sealed class CbrRateSource : IRatesSource public sealed class CbrRateSource : IRatesSource
{ {
/// <summary> /// <summary>
@@ -47,7 +44,6 @@ public sealed class CbrRateSource : IRatesSource
_logger = logger; _logger = logger;
} }
/// <inheritdoc />
async Task<Dictionary<string, double>?> IRatesSource.FetchAsync(CancellationToken ct) async Task<Dictionary<string, double>?> IRatesSource.FetchAsync(CancellationToken ct)
{ {
try try
@@ -11,9 +11,6 @@ using Microsoft.Extensions.Logging;
namespace Deal.Infrastructure.Integrations.Services; namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// gRPC-адаптер порта <see cref="IAiClassifier"/> к автономному ai-service.
/// </summary>
public sealed class GrpcAiClassifier : IAiClassifier public sealed class GrpcAiClassifier : IAiClassifier
{ {
/// <summary> /// <summary>
@@ -70,7 +67,6 @@ public sealed class GrpcAiClassifier : IAiClassifier
_logger = logger; _logger = logger;
} }
/// <inheritdoc />
async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct) async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -106,7 +102,6 @@ public sealed class GrpcAiClassifier : IAiClassifier
} }
} }
/// <inheritdoc />
async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct) async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -10,9 +10,6 @@ using Microsoft.Extensions.Logging;
namespace Deal.Infrastructure.Integrations.Services; namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// gRPC-адаптер порта <see cref="IAiTools"/> к автономному ai-service.
/// </summary>
public sealed class GrpcAiTools : IAiTools public sealed class GrpcAiTools : IAiTools
{ {
/// <summary> /// <summary>
@@ -70,7 +67,6 @@ public sealed class GrpcAiTools : IAiTools
_logger = logger; _logger = logger;
} }
/// <inheritdoc />
async Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct) async Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -104,7 +100,6 @@ public sealed class GrpcAiTools : IAiTools
} }
} }
/// <inheritdoc />
async Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync( async Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
string text, string text,
string description, string description,
@@ -16,9 +16,6 @@ using Microsoft.Extensions.Logging;
namespace Deal.Infrastructure.Integrations.Services; namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// gRPC-адаптер порта IMlClient к автономному ml-service.
/// </summary>
public sealed class GrpcMlClient : IMlClient, IMlTrainClient public sealed class GrpcMlClient : IMlClient, IMlTrainClient
{ {
/// <summary> /// <summary>
@@ -97,7 +94,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
_logger = logger; _logger = logger;
} }
/// <inheritdoc />
async Task<MlStatusResponseDto> IMlClient.StatusAsync(CancellationToken ct) async Task<MlStatusResponseDto> IMlClient.StatusAsync(CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -122,7 +118,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
return new MlStatusResponseDto(Enabled: enabled, Service: snapshot.Service, Reachable: snapshot.Reachable, Stats: stats); return new MlStatusResponseDto(Enabled: enabled, Service: snapshot.Service, Reachable: snapshot.Reachable, Stats: stats);
} }
/// <inheritdoc />
async Task<MlPredictResultDto> IMlClient.PredictAsync(string text, CancellationToken ct) async Task<MlPredictResultDto> IMlClient.PredictAsync(string text, CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -143,7 +138,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
} }
} }
/// <inheritdoc />
async Task<MlResetResultDto> IMlClient.ResetAsync(CancellationToken ct) async Task<MlResetResultDto> IMlClient.ResetAsync(CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -171,7 +165,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
return new MlResetResultDto(Ok: true, Error: null); return new MlResetResultDto(Ok: true, Error: null);
} }
/// <inheritdoc />
async Task IMlClient.PushAsync( async Task IMlClient.PushAsync(
string text, string text,
string label, string label,
@@ -181,7 +174,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
await MlOutboxQueue.PushAsync(_learningStore, text, label, delta, ct); await MlOutboxQueue.PushAsync(_learningStore, text, label, delta, ct);
} }
/// <inheritdoc />
async Task<int> IMlTrainClient.TrainBatchAsync(IReadOnlyList<MlOutboxEntryDto> items, CancellationToken ct) async Task<int> IMlTrainClient.TrainBatchAsync(IReadOnlyList<MlOutboxEntryDto> items, CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -9,9 +9,6 @@ using Microsoft.Extensions.Logging;
namespace Deal.Infrastructure.Integrations.Services; namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// gRPC-адаптер порта <see cref="ITelegramGateway"/> к автономному telegram-service.
/// </summary>
public sealed class GrpcTelegramClient : ITelegramGateway public sealed class GrpcTelegramClient : ITelegramGateway
{ {
/// <summary> /// <summary>
@@ -58,7 +55,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
_logger = logger; _logger = logger;
} }
/// <inheritdoc />
async Task<TelegramAccountStatusDto> ITelegramGateway.StatusAsync(CancellationToken ct) async Task<TelegramAccountStatusDto> ITelegramGateway.StatusAsync(CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -81,7 +77,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task<TelegramAuthResultDto> ITelegramGateway.StartPhoneAsync( async Task<TelegramAuthResultDto> ITelegramGateway.StartPhoneAsync(
string phone, string phone,
int apiId, int apiId,
@@ -104,7 +99,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task<TelegramAuthResultDto> ITelegramGateway.StartQrAsync( async Task<TelegramAuthResultDto> ITelegramGateway.StartQrAsync(
int apiId, int apiId,
string apiHash, string apiHash,
@@ -128,7 +122,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task<string> ITelegramGateway.SendCodeAsync(string code, CancellationToken ct) async Task<string> ITelegramGateway.SendCodeAsync(string code, CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -146,7 +139,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task<string> ITelegramGateway.SendPasswordAsync(string password, CancellationToken ct) async Task<string> ITelegramGateway.SendPasswordAsync(string password, CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -164,7 +156,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task ITelegramGateway.LogoutAsync(CancellationToken ct) async Task ITelegramGateway.LogoutAsync(CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -180,7 +171,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct) async Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -197,7 +187,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task ITelegramGateway.SetMonitorAsync( async Task ITelegramGateway.SetMonitorAsync(
string dialogId, string dialogId,
bool enabled, bool enabled,
@@ -217,7 +206,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct) async Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -234,7 +222,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task<int> ITelegramGateway.BackfillAsync( async Task<int> ITelegramGateway.BackfillAsync(
string dialogId, string dialogId,
bool force, bool force,
@@ -255,7 +242,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task<IReadOnlyList<TelegramRecentMessageDto>> ITelegramGateway.ReadRecentAsync( async Task<IReadOnlyList<TelegramRecentMessageDto>> ITelegramGateway.ReadRecentAsync(
string dialogId, string dialogId,
int limit, int limit,
@@ -278,7 +264,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task<TelegramSourceContentDto> ITelegramGateway.ReadSourceAsync( async Task<TelegramSourceContentDto> ITelegramGateway.ReadSourceAsync(
string dialogId, string dialogId,
long msgId, long msgId,
@@ -302,7 +287,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.SearchAsync( async Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.SearchAsync(
string query, string query,
int limit, int limit,
@@ -323,7 +307,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task<TelegramChannelInfoDto> ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct) async Task<TelegramChannelInfoDto> ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -349,7 +332,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task<TelegramEvalReadDto> ITelegramGateway.ReadForEvalAsync( async Task<TelegramEvalReadDto> ITelegramGateway.ReadForEvalAsync(
string dialogId, string dialogId,
int limit, int limit,
@@ -380,7 +362,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task ITelegramGateway.JoinAsync(string username, CancellationToken ct) async Task ITelegramGateway.JoinAsync(string username, CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -397,7 +378,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
} }
} }
/// <inheritdoc />
async Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct) async Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct)
{ {
TenantId tenantId = RequireTenant(); TenantId tenantId = RequireTenant();
@@ -3,20 +3,15 @@ using Deal.Contracts.Integrations.Models;
namespace Deal.Infrastructure.Integrations.Services; namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// Локальная реализация <see cref="IAiTools"/> без внешнего ИИ-сервиса.
/// </summary>
public sealed class LocalAiTools : IAiTools public sealed class LocalAiTools : IAiTools
{ {
// Сообщение исключения методов (локальный режим = ai-service не подключён). // Сообщение исключения методов (локальный режим = ai-service не подключён).
private const string NotSupportedMessage = private const string NotSupportedMessage =
"ИИ-инструменты доступны только при подключённом ai-service (Services:Ai:UseLocal=false)."; "ИИ-инструменты доступны только при подключённом ai-service (Services:Ai:UseLocal=false).";
/// <inheritdoc />
Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct) Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
=> throw new NotSupportedException(NotSupportedMessage); => throw new NotSupportedException(NotSupportedMessage);
/// <inheritdoc />
Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync( Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
string text, string text,
string description, string description,
@@ -3,21 +3,16 @@ using Deal.Contracts.Integrations.Models;
namespace Deal.Infrastructure.Integrations.Services; namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// Локальная заглушка <see cref="ITelegramGateway"/> без telegram-service.
/// </summary>
public sealed class LocalTelegramGateway : ITelegramGateway public sealed class LocalTelegramGateway : ITelegramGateway
{ {
// Фаза idle-формы (аккаунт не подключён — сервиса нет). // Фаза idle-формы (аккаунт не подключён — сервиса нет).
private const string IdlePhase = "idle"; private const string IdlePhase = "idle";
/// <inheritdoc />
Task<TelegramAccountStatusDto> ITelegramGateway.StatusAsync(CancellationToken ct) Task<TelegramAccountStatusDto> ITelegramGateway.StatusAsync(CancellationToken ct)
{ {
return Task.FromResult(new TelegramAccountStatusDto(IdlePhase, false, false, string.Empty, null, null)); return Task.FromResult(new TelegramAccountStatusDto(IdlePhase, false, false, string.Empty, null, null));
} }
/// <inheritdoc />
Task<TelegramAuthResultDto> ITelegramGateway.StartPhoneAsync( Task<TelegramAuthResultDto> ITelegramGateway.StartPhoneAsync(
string phone, string phone,
int apiId, int apiId,
@@ -25,76 +20,61 @@ public sealed class LocalTelegramGateway : ITelegramGateway
CancellationToken ct) CancellationToken ct)
=> Task.FromResult(new TelegramAuthResultDto(IdlePhase, null)); => Task.FromResult(new TelegramAuthResultDto(IdlePhase, null));
/// <inheritdoc />
Task<TelegramAuthResultDto> ITelegramGateway.StartQrAsync( Task<TelegramAuthResultDto> ITelegramGateway.StartQrAsync(
int apiId, int apiId,
string apiHash, string apiHash,
CancellationToken ct) CancellationToken ct)
=> Task.FromResult(new TelegramAuthResultDto(IdlePhase, null)); => Task.FromResult(new TelegramAuthResultDto(IdlePhase, null));
/// <inheritdoc /> Task<string> ITelegramGateway.SendCodeAsync(string code, CancellationToken ct) => Task.FromResult(IdlePhase);
public Task<string> SendCodeAsync(string code, CancellationToken ct) => Task.FromResult(IdlePhase);
/// <inheritdoc /> Task<string> ITelegramGateway.SendPasswordAsync(string password, CancellationToken ct) => Task.FromResult(IdlePhase);
public Task<string> SendPasswordAsync(string password, CancellationToken ct) => Task.FromResult(IdlePhase);
/// <inheritdoc />
Task ITelegramGateway.LogoutAsync(CancellationToken ct) => Task.CompletedTask; Task ITelegramGateway.LogoutAsync(CancellationToken ct) => Task.CompletedTask;
/// <inheritdoc />
Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct) Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct)
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]); => Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
/// <inheritdoc />
Task ITelegramGateway.SetMonitorAsync( Task ITelegramGateway.SetMonitorAsync(
string dialogId, string dialogId,
bool enabled, bool enabled,
CancellationToken ct) => Task.CompletedTask; CancellationToken ct) => Task.CompletedTask;
/// <inheritdoc />
Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct) => Task.CompletedTask; Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct) => Task.CompletedTask;
/// <inheritdoc /> Task<int> ITelegramGateway.BackfillAsync(
public Task<int> BackfillAsync(
string dialogId, string dialogId,
bool force, bool force,
CancellationToken ct) => Task.FromResult(0); CancellationToken ct) => Task.FromResult(0);
/// <inheritdoc />
Task<IReadOnlyList<TelegramRecentMessageDto>> ITelegramGateway.ReadRecentAsync( Task<IReadOnlyList<TelegramRecentMessageDto>> ITelegramGateway.ReadRecentAsync(
string dialogId, string dialogId,
int limit, int limit,
CancellationToken ct) CancellationToken ct)
=> Task.FromResult<IReadOnlyList<TelegramRecentMessageDto>>([]); => Task.FromResult<IReadOnlyList<TelegramRecentMessageDto>>([]);
/// <inheritdoc />
Task<TelegramSourceContentDto> ITelegramGateway.ReadSourceAsync( Task<TelegramSourceContentDto> ITelegramGateway.ReadSourceAsync(
string dialogId, string dialogId,
long msgId, long msgId,
CancellationToken ct) CancellationToken ct)
=> Task.FromResult(new TelegramSourceContentDto(false, null, null)); => Task.FromResult(new TelegramSourceContentDto(false, null, null));
/// <inheritdoc />
Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.SearchAsync( Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.SearchAsync(
string query, string query,
int limit, int limit,
CancellationToken ct) CancellationToken ct)
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]); => Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
/// <inheritdoc />
Task<TelegramChannelInfoDto> ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct) Task<TelegramChannelInfoDto> ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct)
=> Task.FromResult(new TelegramChannelInfoDto(dialogId, string.Empty, string.Empty, string.Empty, SourceDefaults.DefaultHue, null, false)); => Task.FromResult(new TelegramChannelInfoDto(dialogId, string.Empty, string.Empty, string.Empty, SourceDefaults.DefaultHue, null, false));
/// <inheritdoc />
Task<TelegramEvalReadDto> ITelegramGateway.ReadForEvalAsync( Task<TelegramEvalReadDto> ITelegramGateway.ReadForEvalAsync(
string dialogId, string dialogId,
int limit, int limit,
CancellationToken ct) CancellationToken ct)
=> Task.FromResult(new TelegramEvalReadDto(false, "no_history", [])); => Task.FromResult(new TelegramEvalReadDto(false, "no_history", []));
/// <inheritdoc />
Task ITelegramGateway.JoinAsync(string username, CancellationToken ct) => Task.CompletedTask; Task ITelegramGateway.JoinAsync(string username, CancellationToken ct) => Task.CompletedTask;
/// <inheritdoc />
Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct) => Task.CompletedTask; Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct) => Task.CompletedTask;
} }
@@ -3,9 +3,6 @@ using Deal.Contracts.Integrations.Models;
namespace Deal.Infrastructure.Integrations.Storage.Services; namespace Deal.Infrastructure.Integrations.Storage.Services;
/// <summary>
/// Локальное файловое хранилище вложений — каталог на диске.
/// </summary>
public sealed class LocalFileStorage : IFileStorage public sealed class LocalFileStorage : IFileStorage
{ {
// Размер буфера чтения при скачивании (async FileStream). // Размер буфера чтения при скачивании (async FileStream).
@@ -31,7 +28,6 @@ public sealed class LocalFileStorage : IFileStorage
/// <returns>Строка вида <c>LocalFileStorage (root: …)</c>.</returns> /// <returns>Строка вида <c>LocalFileStorage (root: …)</c>.</returns>
public override string ToString() => $"LocalFileStorage (root: {_rootPath})"; public override string ToString() => $"LocalFileStorage (root: {_rootPath})";
/// <inheritdoc />
async Task<string> IFileStorage.PutAsync( async Task<string> IFileStorage.PutAsync(
string objectKey, string objectKey,
Stream content, Stream content,
@@ -55,7 +51,6 @@ public sealed class LocalFileStorage : IFileStorage
return objectKey; return objectKey;
} }
/// <inheritdoc />
Task<Stream?> IFileStorage.GetAsync(string objectKey, CancellationToken ct) Task<Stream?> IFileStorage.GetAsync(string objectKey, CancellationToken ct)
{ {
string path = ResolvePath(objectKey); string path = ResolvePath(objectKey);
@@ -68,7 +63,6 @@ public sealed class LocalFileStorage : IFileStorage
return Task.FromResult<Stream?>(stream); return Task.FromResult<Stream?>(stream);
} }
/// <inheritdoc />
Task<FileMeta?> IFileStorage.StatAsync(string objectKey, CancellationToken ct) Task<FileMeta?> IFileStorage.StatAsync(string objectKey, CancellationToken ct)
{ {
string path = ResolvePath(objectKey); string path = ResolvePath(objectKey);
@@ -81,7 +75,6 @@ public sealed class LocalFileStorage : IFileStorage
return Task.FromResult<FileMeta?>(new FileMeta(objectKey, info.Length, string.Empty)); return Task.FromResult<FileMeta?>(new FileMeta(objectKey, info.Length, string.Empty));
} }
/// <inheritdoc />
Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct) Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct)
{ {
string path = ResolvePath(objectKey); string path = ResolvePath(objectKey);
@@ -9,9 +9,6 @@ using Minio.Exceptions;
namespace Deal.Infrastructure.Integrations.Storage.Services; namespace Deal.Infrastructure.Integrations.Storage.Services;
/// <summary>
/// Хранилище вложений на MinIO
/// </summary>
public sealed class MinioFileStorage : IFileStorage public sealed class MinioFileStorage : IFileStorage
{ {
private const string DefaultContentType = "application/octet-stream"; private const string DefaultContentType = "application/octet-stream";
@@ -66,7 +63,6 @@ public sealed class MinioFileStorage : IFileStorage
/// <returns>Строка вида <c>MinioFileStorage (endpoint: …; bucket: …)</c>.</returns> /// <returns>Строка вида <c>MinioFileStorage (endpoint: …; bucket: …)</c>.</returns>
public override string ToString() => $"MinioFileStorage (endpoint: {_endpoint}; bucket: {_bucket})"; public override string ToString() => $"MinioFileStorage (endpoint: {_endpoint}; bucket: {_bucket})";
/// <inheritdoc />
async Task<string> IFileStorage.PutAsync( async Task<string> IFileStorage.PutAsync(
string objectKey, string objectKey,
Stream content, Stream content,
@@ -98,7 +94,6 @@ public sealed class MinioFileStorage : IFileStorage
return objectKey; return objectKey;
} }
/// <inheritdoc />
async Task<Stream?> IFileStorage.GetAsync(string objectKey, CancellationToken ct) async Task<Stream?> IFileStorage.GetAsync(string objectKey, CancellationToken ct)
{ {
MemoryStream buffer = new(); MemoryStream buffer = new();
@@ -128,7 +123,6 @@ public sealed class MinioFileStorage : IFileStorage
return buffer; return buffer;
} }
/// <inheritdoc />
async Task<FileMeta?> IFileStorage.StatAsync(string objectKey, CancellationToken ct) async Task<FileMeta?> IFileStorage.StatAsync(string objectKey, CancellationToken ct)
{ {
try try
@@ -144,7 +138,6 @@ public sealed class MinioFileStorage : IFileStorage
} }
} }
/// <inheritdoc />
async Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct) async Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct)
{ {
try try
@@ -5,9 +5,6 @@ using Deal.Modules.Discovery.Application.Models;
namespace Deal.Infrastructure.Persistence.Repositories; namespace Deal.Infrastructure.Persistence.Repositories;
/// <summary>
/// EF-адаптер хранилища Discovery
/// </summary>
public sealed partial class DiscoveryStore : IDiscoveryStore public sealed partial class DiscoveryStore : IDiscoveryStore
{ {
private readonly TenantDbContext _dbContext; private readonly TenantDbContext _dbContext;
@@ -9,9 +9,6 @@ using Microsoft.EntityFrameworkCore;
namespace Deal.Infrastructure.Persistence.Repositories; namespace Deal.Infrastructure.Persistence.Repositories;
/// <summary>
/// EF-адаптер хранилища карточек и контейнеров
/// </summary>
public sealed partial class KanbanStore : ICardStore public sealed partial class KanbanStore : ICardStore
{ {
private readonly TenantDbContext _dbContext; private readonly TenantDbContext _dbContext;
@@ -6,9 +6,6 @@ using Microsoft.EntityFrameworkCore;
namespace Deal.Infrastructure.Persistence.Repositories; namespace Deal.Infrastructure.Persistence.Repositories;
/// <summary>
/// EF-адаптер хранилища лимитов ИИ-бюджета
/// </summary>
public sealed class TenantLimitStore : ITenantLimitStore public sealed class TenantLimitStore : ITenantLimitStore
{ {
private readonly DealDbContext _dbContext; private readonly DealDbContext _dbContext;
@@ -58,7 +55,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
_utcNow = utcNow; _utcNow = utcNow;
} }
/// <inheritdoc />
async Task<TenantLimitDto> ITenantLimitStore.GetOrCreateAsync( async Task<TenantLimitDto> ITenantLimitStore.GetOrCreateAsync(
Guid tenantId, Guid tenantId,
CancellationToken ct, CancellationToken ct,
@@ -68,7 +64,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
return ToLimitDto(entity); return ToLimitDto(entity);
} }
/// <inheritdoc />
async Task<BudgetStateDto> ITenantLimitStore.GetStateAsync(Guid tenantId, CancellationToken ct) async Task<BudgetStateDto> ITenantLimitStore.GetStateAsync(Guid tenantId, CancellationToken ct)
{ {
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct); TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
@@ -76,7 +71,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
return await ToStateDtoAsync(entity, ct); return await ToStateDtoAsync(entity, ct);
} }
/// <inheritdoc />
async Task<BudgetStateDto> ITenantLimitStore.AddUsageAsync( async Task<BudgetStateDto> ITenantLimitStore.AddUsageAsync(
Guid tenantId, Guid tenantId,
long tokens, long tokens,
@@ -109,7 +103,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
return await ToStateDtoAsync(entity, ct); return await ToStateDtoAsync(entity, ct);
} }
/// <inheritdoc />
async Task<BudgetStateDto> ITenantLimitStore.UpdateBudgetAsync( async Task<BudgetStateDto> ITenantLimitStore.UpdateBudgetAsync(
Guid tenantId, Guid tenantId,
long budgetTokens, long budgetTokens,
@@ -132,7 +125,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
return await ToStateDtoAsync(entity, ct); return await ToStateDtoAsync(entity, ct);
} }
/// <inheritdoc />
async Task<bool> ITenantLimitStore.TryMarkWarnedAsync(Guid tenantId, CancellationToken ct) async Task<bool> ITenantLimitStore.TryMarkWarnedAsync(Guid tenantId, CancellationToken ct)
{ {
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct); TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
@@ -148,7 +140,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
return true; return true;
} }
/// <inheritdoc />
async Task<bool> ITenantLimitStore.TryMarkNotifiedExhaustedAsync(Guid tenantId, CancellationToken ct) async Task<bool> ITenantLimitStore.TryMarkNotifiedExhaustedAsync(Guid tenantId, CancellationToken ct)
{ {
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct); TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
@@ -219,7 +210,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
return true; return true;
} }
/// <inheritdoc />
async Task<int> ITenantLimitStore.ResetExpiredPeriodsAsync(DateTimeOffset now, CancellationToken ct) async Task<int> ITenantLimitStore.ResetExpiredPeriodsAsync(DateTimeOffset now, CancellationToken ct)
{ {
// Трогаем только строки с накоплениями (расход/флаги): строки без накоплений чистить нечего. // Трогаем только строки с накоплениями (расход/флаги): строки без накоплений чистить нечего.
@@ -4,9 +4,6 @@ using Deal.Modules.Settings.Application.Abstractions;
namespace Deal.Infrastructure.Security; namespace Deal.Infrastructure.Security;
/// <summary>
/// AES-256-GCM-шифр секретов
/// </summary>
public sealed class AesGcmSecretCipher : ISecretCipher public sealed class AesGcmSecretCipher : ISecretCipher
{ {
// Префикс зашифрованного значения (маркер формата в хранилище). // Префикс зашифрованного значения (маркер формата в хранилище).
@@ -39,7 +36,6 @@ public sealed class AesGcmSecretCipher : ISecretCipher
_key = key; _key = key;
} }
/// <inheritdoc />
string ISecretCipher.Encrypt(string plainText) string ISecretCipher.Encrypt(string plainText)
{ {
if (string.IsNullOrEmpty(plainText)) if (string.IsNullOrEmpty(plainText))
@@ -65,7 +61,6 @@ public sealed class AesGcmSecretCipher : ISecretCipher
return EncryptedPrefix + Convert.ToBase64String(payload); return EncryptedPrefix + Convert.ToBase64String(payload);
} }
/// <inheritdoc />
string ISecretCipher.Decrypt(string cipherText) string ISecretCipher.Decrypt(string cipherText)
{ {
if (string.IsNullOrEmpty(cipherText) || !cipherText.StartsWith(EncryptedPrefix, StringComparison.Ordinal)) if (string.IsNullOrEmpty(cipherText) || !cipherText.StartsWith(EncryptedPrefix, StringComparison.Ordinal))
@@ -3,9 +3,6 @@ using Deal.Modules.Discovery.Application.Abstractions;
namespace Deal.Modules.Discovery.Application.Services; namespace Deal.Modules.Discovery.Application.Services;
/// <summary>
/// Потокобезопасная реализация <see cref="IDiscoverySearchErrorCounter"/>
/// </summary>
public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter
{ {
/// <summary> /// <summary>
@@ -43,7 +40,6 @@ public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter
_utcNow = utcNow; _utcNow = utcNow;
} }
/// <inheritdoc />
int IDiscoverySearchErrorCounter.Next(string taskId) int IDiscoverySearchErrorCounter.Next(string taskId)
{ {
EvictExpired(); EvictExpired();
@@ -55,7 +51,6 @@ public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter
return fresh.Count; return fresh.Count;
} }
/// <inheritdoc />
void IDiscoverySearchErrorCounter.Reset(string taskId) void IDiscoverySearchErrorCounter.Reset(string taskId)
{ {
EvictExpired(); EvictExpired();
@@ -3,14 +3,9 @@ using Isopoh.Cryptography.Argon2;
namespace Deal.Modules.Tenants.Application.Services; namespace Deal.Modules.Tenants.Application.Services;
/// <summary>
/// Реализация <see cref="IPasswordHasher"/> на Argon2id.
/// </summary>
public sealed class DefaultPasswordHasher : IPasswordHasher public sealed class DefaultPasswordHasher : IPasswordHasher
{ {
/// <inheritdoc /> string IPasswordHasher.Hash(string password) => Argon2.Hash(password);
public string Hash(string password) => Argon2.Hash(password);
/// <inheritdoc /> bool IPasswordHasher.Verify(string password, string encodedHash) => Argon2.Verify(encodedHash, password);
public bool Verify(string password, string encodedHash) => Argon2.Verify(encodedHash, password);
} }
@@ -39,7 +39,7 @@ public sealed class DiscoveryWorkerSchedulerTests
TestDiscoveryStore StoreB, TestDiscoveryStore StoreB,
TestDiscoveryGateway GatewayA, TestDiscoveryGateway GatewayA,
TestDiscoveryGateway GatewayB, TestDiscoveryGateway GatewayB,
TenantContext TenantContext, ITenantContext TenantContext,
ListLogger Logs); ListLogger Logs);
[Fact] [Fact]
@@ -91,7 +91,7 @@ public sealed class DiscoveryWorkerSchedulerTests
private static Context CreateContext() private static Context CreateContext()
{ {
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)); var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
var storeA = new TestDiscoveryStore(); var storeA = new TestDiscoveryStore();
var storeB = new TestDiscoveryStore(); var storeB = new TestDiscoveryStore();
var settingsA = new TestSettingsStore(); var settingsA = new TestSettingsStore();
@@ -41,7 +41,7 @@ public sealed class MlOutboxFlushSchedulerTests
{ {
var store = new TestMlLearningStore(); var store = new TestMlLearningStore();
SeedRows(store, count: 25, prefix: "a"); SeedRows(store, count: 25, prefix: "a");
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
port, port,
new TestTenantRepository(Tenant(TenantA)), new TestTenantRepository(Tenant(TenantA)),
@@ -67,7 +67,7 @@ public sealed class MlOutboxFlushSchedulerTests
service.TrainUnavailable = true; service.TrainUnavailable = true;
var store = new TestMlLearningStore(); var store = new TestMlLearningStore();
SeedRows(store, count: 5, prefix: "a"); SeedRows(store, count: 5, prefix: "a");
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
port, port,
new TestTenantRepository(Tenant(TenantA)), new TestTenantRepository(Tenant(TenantA)),
@@ -95,7 +95,7 @@ public sealed class MlOutboxFlushSchedulerTests
SeedRows(storeA, count: 12, prefix: "a"); SeedRows(storeA, count: 12, prefix: "a");
var storeB = new TestMlLearningStore(); var storeB = new TestMlLearningStore();
SeedRows(storeB, count: 3, prefix: "b"); SeedRows(storeB, count: 3, prefix: "b");
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
port, port,
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)), new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)),
@@ -121,7 +121,7 @@ public sealed class MlOutboxFlushSchedulerTests
{ {
var store = new TestMlLearningStore(); var store = new TestMlLearningStore();
SeedRows(store, count: 105, prefix: "a"); SeedRows(store, count: 105, prefix: "a");
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
port, port,
new TestTenantRepository(Tenant(TenantA)), new TestTenantRepository(Tenant(TenantA)),
@@ -148,7 +148,7 @@ public sealed class MlOutboxFlushSchedulerTests
private static ServiceProvider BuildProvider( private static ServiceProvider BuildProvider(
int port, int port,
TestTenantRepository tenants, TestTenantRepository tenants,
TenantContext tenantContext, ITenantContext tenantContext,
Dictionary<Guid, TestMlLearningStore> storesByTenant) Dictionary<Guid, TestMlLearningStore> storesByTenant)
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
@@ -47,7 +47,7 @@ public sealed class PipelineWorkerSchedulerTests
TestKanjStore KanjB, TestKanjStore KanjB,
SseSubscription SubscriptionB, SseSubscription SubscriptionB,
PipelinePumpGate PumpGate, PipelinePumpGate PumpGate,
TenantContext TenantContext, ITenantContext TenantContext,
ListLogger Logs); ListLogger Logs);
// ─── Цикл: pump каждого тенанта в собственном scope + new_card ───────── // ─── Цикл: pump каждого тенанта в собственном scope + new_card ─────────
@@ -149,7 +149,7 @@ public sealed class PipelineWorkerSchedulerTests
private static Context CreateContext(bool withThrowingQueueReadA = false) private static Context CreateContext(bool withThrowingQueueReadA = false)
{ {
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)); var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
var pipelineA = new TestPipelineStore(throwOnList: withThrowingQueueReadA); var pipelineA = new TestPipelineStore(throwOnList: withThrowingQueueReadA);
var pipelineB = new TestPipelineStore(); var pipelineB = new TestPipelineStore();
var kanjA = new TestKanjStore(); var kanjA = new TestKanjStore();
@@ -46,7 +46,7 @@ public sealed class StorageTickSchedulerTests
TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old"); TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
var settings = new TestSettingsStore(); var settings = new TestSettingsStore();
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)); var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider(tenants, tenantContext, storeA, storeB, settings); await using ServiceProvider provider = BuildProvider(tenants, tenantContext, storeA, storeB, settings);
SseBroker broker = provider.GetRequiredService<SseBroker>(); SseBroker broker = provider.GetRequiredService<SseBroker>();
@@ -74,7 +74,7 @@ public sealed class StorageTickSchedulerTests
var storeB = new TestKanjStore(); var storeB = new TestKanjStore();
storeB.SeedCard(Card("l_b_fresh", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromHours(1)))); storeB.SeedCard(Card("l_b_fresh", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromHours(1))));
var settings = new TestSettingsStore(); var settings = new TestSettingsStore();
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)), new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)),
tenantContext, tenantContext,
@@ -100,7 +100,7 @@ public sealed class StorageTickSchedulerTests
{ {
// У тенанта A настройки падают (имитация сбоя схемы/БД) — тик A логирует ошибку, B обрабатывается. // У тенанта A настройки падают (имитация сбоя схемы/БД) — тик A логирует ошибку, B обрабатывается.
TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old"); TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
var settingsByTenant = new Dictionary<Guid, ISettingsStore> var settingsByTenant = new Dictionary<Guid, ISettingsStore>
{ {
[TenantA] = new ThrowingSettingsStore(), [TenantA] = new ThrowingSettingsStore(),
@@ -133,7 +133,7 @@ public sealed class StorageTickSchedulerTests
[Fact] [Fact]
public async Task RunCycle_TenantListFailure_DoesNotThrow() public async Task RunCycle_TenantListFailure_DoesNotThrow()
{ {
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
new ThrowingTenantRepository(), new ThrowingTenantRepository(),
tenantContext, tenantContext,
@@ -152,7 +152,7 @@ public sealed class StorageTickSchedulerTests
{ {
var kanjStore = new TestKanjStore(); var kanjStore = new TestKanjStore();
var settings = new TestSettingsStore(); var settings = new TestSettingsStore();
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
var pipelineStoreA = new TestPipelineStore(); var pipelineStoreA = new TestPipelineStore();
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
pipelineStoreA.SeedRejected(Rejected("r_old_a", (long)(nowMs - TimeSpan.FromDays(4).TotalMilliseconds))); pipelineStoreA.SeedRejected(Rejected("r_old_a", (long)(nowMs - TimeSpan.FromDays(4).TotalMilliseconds)));
@@ -186,7 +186,7 @@ public sealed class StorageTickSchedulerTests
cardStoreA.SeedCard(HoldCard("c_a_future", title: "Будущий", reminderAtMs: NowMs() + 60_000)); cardStoreA.SeedCard(HoldCard("c_a_future", title: "Будущий", reminderAtMs: NowMs() + 60_000));
cardStoreA.SeedCard(HoldCard("c_a_work", stage: "work", title: "В работе", reminderAtMs: NowMs() - 60_000)); cardStoreA.SeedCard(HoldCard("c_a_work", stage: "work", title: "В работе", reminderAtMs: NowMs() - 60_000));
var cardStoreB = new TestKanjStore(); var cardStoreB = new TestKanjStore();
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository, new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
tenantContext, tenantContext,
@@ -221,7 +221,7 @@ public sealed class StorageTickSchedulerTests
cardStoreA.SeedCard(HoldCard("c_past", title: "Старое", reminderAtMs: NowMs() - 60_000)); cardStoreA.SeedCard(HoldCard("c_past", title: "Старое", reminderAtMs: NowMs() - 60_000));
var settingsA = new TestSettingsStore(); var settingsA = new TestSettingsStore();
settingsA.Preload(SettingsKeys.RemindersEnabled, "false"); settingsA.Preload(SettingsKeys.RemindersEnabled, "false");
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
new TestTenantRepository(Tenant(TenantA)).Repository, new TestTenantRepository(Tenant(TenantA)).Repository,
tenantContext, tenantContext,
@@ -247,7 +247,7 @@ public sealed class StorageTickSchedulerTests
// и тик A завершается, тенант B обрабатывается (автоархив + тост) — проход жив. // и тик A завершается, тенант B обрабатывается (автоархив + тост) — проход жив.
var cardStoreA = new TestKanjStore(throwOnDueReminders: true); var cardStoreA = new TestKanjStore(throwOnDueReminders: true);
TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old"); TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
var tenantContext = new TenantContext(); ITenantContext tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository, new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
tenantContext, tenantContext,
@@ -280,7 +280,7 @@ public sealed class StorageTickSchedulerTests
// Возвращает: Провайдер с зарегистрированными сервисами теста. // Возвращает: Провайдер с зарегистрированными сервисами теста.
private static ServiceProvider BuildProvider( private static ServiceProvider BuildProvider(
TestTenantRepository tenants, TestTenantRepository tenants,
TenantContext tenantContext, ITenantContext tenantContext,
TestKanjStore storeA, TestKanjStore storeA,
TestKanjStore storeB, TestKanjStore storeB,
TestSettingsStore settings) TestSettingsStore settings)
@@ -301,7 +301,7 @@ public sealed class StorageTickSchedulerTests
// Возвращает: Провайдер с зарегистрированными сервисами теста. // Возвращает: Провайдер с зарегистрированными сервисами теста.
private static ServiceProvider BuildProvider( private static ServiceProvider BuildProvider(
ITenantRepository tenants, ITenantRepository tenants,
TenantContext tenantContext, ITenantContext tenantContext,
Dictionary<Guid, TestKanjStore> storesByTenant, Dictionary<Guid, TestKanjStore> storesByTenant,
Dictionary<Guid, ISettingsStore> settingsByTenant, Dictionary<Guid, ISettingsStore> settingsByTenant,
Dictionary<Guid, TestPipelineStore>? pipelineStoresByTenant = null) Dictionary<Guid, TestPipelineStore>? pipelineStoresByTenant = null)
@@ -11,9 +11,6 @@ using Deal.Telegram.Core;
namespace Deal.Telegram.Core; namespace Deal.Telegram.Core;
/// <summary>
/// Исходящий gRPC-канал в ядро
/// </summary>
public sealed class CoreIngressClient : ICoreIngressClient public sealed class CoreIngressClient : ICoreIngressClient
{ {
public const string TenantIdMetadataKey = "tenant-id"; public const string TenantIdMetadataKey = "tenant-id";
@@ -44,7 +41,6 @@ public sealed class CoreIngressClient : ICoreIngressClient
_mtlsCertificates = mtlsCertificates; _mtlsCertificates = mtlsCertificates;
} }
/// <inheritdoc />
async Task<PushSourceReply> ICoreIngressClient.PushSourceAsync( async Task<PushSourceReply> ICoreIngressClient.PushSourceAsync(
string tenantId, string tenantId,
PushSourceRequest request, PushSourceRequest request,
@@ -61,7 +57,6 @@ public sealed class CoreIngressClient : ICoreIngressClient
} }
} }
/// <inheritdoc />
async Task<IReadOnlyList<string>> ICoreIngressClient.SyncDialogsAsync( async Task<IReadOnlyList<string>> ICoreIngressClient.SyncDialogsAsync(
string tenantId, string tenantId,
IReadOnlyList<DialogEntry> entries, IReadOnlyList<DialogEntry> entries,
@@ -1,12 +1,8 @@
using Deal.Telegram.Dialogs; using Deal.Telegram.Dialogs;
namespace Deal.Telegram.Dialogs; namespace Deal.Telegram.Dialogs;
/// <summary>
/// Реальная реализация <see cref="IBackfillPacer"/>
/// </summary>
public sealed class RandomBackfillPacer : IBackfillPacer public sealed class RandomBackfillPacer : IBackfillPacer
{ {
/// <inheritdoc />
async Task IBackfillPacer.WaitAsync( async Task IBackfillPacer.WaitAsync(
double minSeconds, double minSeconds,
double maxSeconds, double maxSeconds,
@@ -1,12 +1,8 @@
using Deal.Telegram.Telegram; using Deal.Telegram.Telegram;
namespace Deal.Telegram.Telegram; namespace Deal.Telegram.Telegram;
/// <summary>
/// Фабрика реальных клиентов WTelegramClient.
/// </summary>
public sealed class ClientFactory : ITelegramClientFactory public sealed class ClientFactory : ITelegramClientFactory
{ {
/// <inheritdoc />
ISessionClient ITelegramClientFactory.Create( ISessionClient ITelegramClientFactory.Create(
int apiId, int apiId,
string apiHash, string apiHash,
@@ -11,9 +11,6 @@ namespace Deal.Telegram.Telegram;
#pragma warning disable CS0618 // Auth_SendCode/Auth_SignIn используются осознанно: ручной веб-вход 1:1 с прототипом #pragma warning disable CS0618 // Auth_SendCode/Auth_SignIn используются осознанно: ручной веб-вход 1:1 с прототипом
/// <summary>
/// Реальная реализация <see cref="ISessionClient"/> поверх WTelegramClient.
/// </summary>
public sealed class WTelegramSessionClient : ISessionClient public sealed class WTelegramSessionClient : ISessionClient
{ {
private readonly Client _client; private readonly Client _client;
@@ -67,25 +64,19 @@ public sealed class WTelegramSessionClient : ISessionClient
_updateManager = new UpdateManager(_client, OnSingleUpdateAsync); _updateManager = new UpdateManager(_client, OnSingleUpdateAsync);
} }
/// <inheritdoc /> bool ISessionClient.IsAuthorized => _client.UserId != 0;
public bool IsAuthorized => _client.UserId != 0;
/// <inheritdoc /> bool ISessionClient.IsConnected => _connected && !_client.Disconnected;
public bool IsConnected => _connected && !_client.Disconnected;
/// <inheritdoc /> int ISessionClient.ApiId => _apiId;
public int ApiId => _apiId;
/// <inheritdoc /> string ISessionClient.ApiHash => _apiHash;
public string ApiHash => _apiHash;
/// <inheritdoc /> byte[]? ISessionClient.SessionBytes => Volatile.Read(ref _latestSessionBytes);
public byte[]? SessionBytes => Volatile.Read(ref _latestSessionBytes);
/// <inheritdoc />
async Task ISessionClient.ConnectAsync(CancellationToken cancellationToken) async Task ISessionClient.ConnectAsync(CancellationToken cancellationToken)
{ {
if (IsConnected) if (((ISessionClient)this).IsConnected)
{ {
return; return;
} }
@@ -94,7 +85,6 @@ public sealed class WTelegramSessionClient : ISessionClient
_connected = true; _connected = true;
} }
/// <inheritdoc />
async Task ISessionClient.RequestCodeAsync(string phone, CancellationToken cancellationToken) async Task ISessionClient.RequestCodeAsync(string phone, CancellationToken cancellationToken)
{ {
_phone = phone; _phone = phone;
@@ -118,7 +108,6 @@ public sealed class WTelegramSessionClient : ISessionClient
} }
} }
/// <inheritdoc />
async Task<string?> ISessionClient.SubmitCodeAsync(string code, CancellationToken cancellationToken) async Task<string?> ISessionClient.SubmitCodeAsync(string code, CancellationToken cancellationToken)
{ {
if (_phoneAlreadyAuthorized) if (_phoneAlreadyAuthorized)
@@ -158,7 +147,6 @@ public sealed class WTelegramSessionClient : ISessionClient
return null; return null;
} }
/// <inheritdoc />
async Task ISessionClient.SubmitPasswordAsync(string password, CancellationToken cancellationToken) async Task ISessionClient.SubmitPasswordAsync(string password, CancellationToken cancellationToken)
{ {
try try
@@ -178,7 +166,6 @@ public sealed class WTelegramSessionClient : ISessionClient
} }
} }
/// <inheritdoc />
async Task ISessionClient.StartQrAsync(Action<string> onQrUrl, CancellationToken cancellationToken) async Task ISessionClient.StartQrAsync(Action<string> onQrUrl, CancellationToken cancellationToken)
{ {
try try
@@ -196,13 +183,11 @@ public sealed class WTelegramSessionClient : ISessionClient
} }
} }
/// <inheritdoc />
async Task ISessionClient.LogOutAsync(CancellationToken cancellationToken) async Task ISessionClient.LogOutAsync(CancellationToken cancellationToken)
{ {
await _client.Auth_LogOut().WaitAsync(cancellationToken).ConfigureAwait(false); await _client.Auth_LogOut().WaitAsync(cancellationToken).ConfigureAwait(false);
} }
/// <inheritdoc />
async Task<string> ISessionClient.GetAccountAsync(CancellationToken cancellationToken) async Task<string> ISessionClient.GetAccountAsync(CancellationToken cancellationToken)
{ {
UserBase[] users = await _client.Users_GetUsers(InputUser.Self).WaitAsync(cancellationToken).ConfigureAwait(false); UserBase[] users = await _client.Users_GetUsers(InputUser.Self).WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -221,7 +206,6 @@ public sealed class WTelegramSessionClient : ISessionClient
/// <inheritdoc /> /// <inheritdoc />
public event Func<TelegramMessage, Task>? MessageReceived; public event Func<TelegramMessage, Task>? MessageReceived;
/// <inheritdoc />
async Task<IReadOnlyList<TelegramDialog>> ISessionClient.GetDialogsAsync(int limit, CancellationToken cancellationToken) async Task<IReadOnlyList<TelegramDialog>> ISessionClient.GetDialogsAsync(int limit, CancellationToken cancellationToken)
{ {
Messages_DialogsBase result = await RunTlCallAsync(() => _client.Messages_GetDialogs(limit: limit), cancellationToken).ConfigureAwait(false); Messages_DialogsBase result = await RunTlCallAsync(() => _client.Messages_GetDialogs(limit: limit), cancellationToken).ConfigureAwait(false);
@@ -241,7 +225,6 @@ public sealed class WTelegramSessionClient : ISessionClient
return items; return items;
} }
/// <inheritdoc />
async Task<IReadOnlyList<TelegramMessage>> ISessionClient.GetMessagesAsync( async Task<IReadOnlyList<TelegramMessage>> ISessionClient.GetMessagesAsync(
string dialogId, string dialogId,
int limit, int limit,
@@ -265,7 +248,6 @@ public sealed class WTelegramSessionClient : ISessionClient
return items; return items;
} }
/// <inheritdoc />
async Task<TelegramMessage?> ISessionClient.GetMessageAsync( async Task<TelegramMessage?> ISessionClient.GetMessageAsync(
string dialogId, string dialogId,
long msgId, long msgId,
@@ -295,7 +277,6 @@ public sealed class WTelegramSessionClient : ISessionClient
return null; return null;
} }
/// <inheritdoc />
async Task ISessionClient.MarkReadAsync(string dialogId, CancellationToken cancellationToken) async Task ISessionClient.MarkReadAsync(string dialogId, CancellationToken cancellationToken)
{ {
InputPeer peer = await ResolvePeerAsync(dialogId, cancellationToken).ConfigureAwait(false); InputPeer peer = await ResolvePeerAsync(dialogId, cancellationToken).ConfigureAwait(false);
@@ -303,7 +284,6 @@ public sealed class WTelegramSessionClient : ISessionClient
} }
/// <inheritdoc />
async Task<IReadOnlyList<TelegramDialog>> ISessionClient.SearchAsync( async Task<IReadOnlyList<TelegramDialog>> ISessionClient.SearchAsync(
string query, string query,
int limit, int limit,
@@ -327,7 +307,6 @@ public sealed class WTelegramSessionClient : ISessionClient
return items; return items;
} }
/// <inheritdoc />
async Task<TelegramSourceInfo> ISessionClient.GetInfoAsync(string dialogId, CancellationToken cancellationToken) async Task<TelegramSourceInfo> ISessionClient.GetInfoAsync(string dialogId, CancellationToken cancellationToken)
{ {
TelegramSourceInfo unknown = DefaultSourceInfo(dialogId); TelegramSourceInfo unknown = DefaultSourceInfo(dialogId);
@@ -361,7 +340,6 @@ public sealed class WTelegramSessionClient : ISessionClient
return unknown; return unknown;
} }
/// <inheritdoc />
async Task<DiscoveryReadResult> ISessionClient.ReadForEvalAsync( async Task<DiscoveryReadResult> ISessionClient.ReadForEvalAsync(
string dialogId, string dialogId,
int limit, int limit,
@@ -410,7 +388,6 @@ public sealed class WTelegramSessionClient : ISessionClient
} }
} }
/// <inheritdoc />
async Task ISessionClient.JoinAsync(string username, CancellationToken cancellationToken) async Task ISessionClient.JoinAsync(string username, CancellationToken cancellationToken)
{ {
Contacts_ResolvedPeer resolved = await RunTlCallAsync(() => _client.Contacts_ResolveUsername(username), cancellationToken).ConfigureAwait(false); Contacts_ResolvedPeer resolved = await RunTlCallAsync(() => _client.Contacts_ResolveUsername(username), cancellationToken).ConfigureAwait(false);
@@ -425,7 +402,6 @@ public sealed class WTelegramSessionClient : ISessionClient
await RunTlCallAsync(() => _client.Channels_JoinChannel(new InputChannel(channel.id, channel.access_hash)), cancellationToken).ConfigureAwait(false); await RunTlCallAsync(() => _client.Channels_JoinChannel(new InputChannel(channel.id, channel.access_hash)), cancellationToken).ConfigureAwait(false);
} }
/// <inheritdoc />
async Task ISessionClient.LeaveAsync(string dialogId, CancellationToken cancellationToken) async Task ISessionClient.LeaveAsync(string dialogId, CancellationToken cancellationToken)
{ {
if (!TryParseSignedId(dialogId, out bool isChannel, out _, out _, out long rawId) || !isChannel) if (!TryParseSignedId(dialogId, out bool isChannel, out _, out _, out long rawId) || !isChannel)