Отформатировать списки параметров по код-стайлу

Больше двух параметров — каждый на отдельной строке (закрывающая
скобка в конце последнего); два и меньше — в одну строку. Правило
добавлено в docs/spec/Код-стайл-Дейл.md; применено к 628 сигнатурам
в 253 файлах.
This commit is contained in:
Rustam Khalimov
2026-09-11 13:22:56 +03:00
parent 410194b0cb
commit 492950bdd0
255 changed files with 2672 additions and 668 deletions
@@ -39,7 +39,10 @@ public sealed class CoreIngressClient : ICoreIngressClient
/// <param name="options">Конфигурация (адрес из env, service-token).</param>
/// <param name="logger">Логгер.</param>
/// <param name="mtlsCertificates">Сертификаты mTLS (Ruling 6, Task 13): null — plaintext-канал (dev).</param>
public CoreIngressClient(CoreIngressOptions options, ILogger<CoreIngressClient> logger, MtlsCertificates? mtlsCertificates = null)
public CoreIngressClient(
CoreIngressOptions options,
ILogger<CoreIngressClient> logger,
MtlsCertificates? mtlsCertificates = null)
{
_options = options;
_logger = logger;
@@ -47,7 +50,10 @@ public sealed class CoreIngressClient : ICoreIngressClient
}
/// <inheritdoc />
public async Task<PushMessageReply> PushMessageAsync(string tenantId, PushMessageRequest message, CancellationToken cancellationToken)
public async Task<PushMessageReply> PushMessageAsync(
string tenantId,
PushMessageRequest message,
CancellationToken cancellationToken)
{
IngressService.IngressServiceClient client = GetClient();
try
@@ -61,7 +67,10 @@ public sealed class CoreIngressClient : ICoreIngressClient
}
/// <inheritdoc />
public async Task<IReadOnlyList<string>> SyncDialogsAsync(string tenantId, IReadOnlyList<DialogEntry> entries, CancellationToken cancellationToken)
public async Task<IReadOnlyList<string>> SyncDialogsAsync(
string tenantId,
IReadOnlyList<DialogEntry> entries,
CancellationToken cancellationToken)
{
IngressService.IngressServiceClient client = GetClient();
var request = new SyncDialogsRequest();
@@ -21,7 +21,10 @@ public interface ICoreIngressClient
/// <param name="message">Сообщение в контракте PushMessageRequest.</param>
/// <param name="cancellationToken">Отмена вызова.</param>
/// <returns>Ответ ядра (accepted/duplicate — дубль dialog+msgId в очереди не растёт).</returns>
public Task<PushMessageReply> PushMessageAsync(string tenantId, PushMessageRequest message, CancellationToken cancellationToken);
public Task<PushMessageReply> PushMessageAsync(
string tenantId,
PushMessageRequest message,
CancellationToken cancellationToken);
/// <summary>
/// Синхронизирует каталог с ядром (Ingress.SyncDialogs → SyncFromTelegram, Ruling 7). Ядро применяет
@@ -32,5 +35,8 @@ public interface ICoreIngressClient
/// <param name="entries">Актуальный каталог диалогов (как refresh_dialogs).</param>
/// <param name="cancellationToken">Отмена вызова.</param>
/// <returns>Список id диалогов с включённым мониторингом (по версии ядра).</returns>
public Task<IReadOnlyList<string>> SyncDialogsAsync(string tenantId, IReadOnlyList<DialogEntry> entries, CancellationToken cancellationToken);
public Task<IReadOnlyList<string>> SyncDialogsAsync(
string tenantId,
IReadOnlyList<DialogEntry> entries,
CancellationToken cancellationToken);
}
@@ -89,7 +89,11 @@ public sealed class BackfillService
/// <param name="force">True — «Перечитать» по кнопке (семантика флага — в ядре, см. класс).</param>
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Сколько сообщений отправлено в ядро (0 — диалог уже перечитывается/нет текстов).</returns>
public async Task<int> ExecuteAsync(string tenantId, string dialogId, bool force, CancellationToken cancellationToken)
public async Task<int> ExecuteAsync(
string tenantId,
string dialogId,
bool force,
CancellationToken cancellationToken)
{
var key = (tenantId, dialogId);
if (!_running.TryAdd(key, 0))
@@ -168,7 +172,10 @@ public sealed class BackfillService
// dialogId: Подписанный id диалога.
// cancellationToken: Отмена операции.
// Возвращает: Сколько сообщений отправлено в ядро.
private async Task<int> BackfillDialogAsync(string tenantId, string dialogId, CancellationToken cancellationToken)
private async Task<int> BackfillDialogAsync(
string tenantId,
string dialogId,
CancellationToken cancellationToken)
{
IReadOnlyList<TelegramMessage> messages = await _sessionFarm
.GetMessagesAsync(tenantId, dialogId, MessagesLimit, cancellationToken)
@@ -43,7 +43,10 @@ public sealed class DialogCatalog
/// <param name="tenantId">Id тенанта.</param>
/// <param name="dialogId">Id диалога.</param>
/// <param name="enabled">Мониторить (true) или выключить (false).</param>
public void SetMonitored(string tenantId, string dialogId, bool enabled)
public void SetMonitored(
string tenantId,
string dialogId,
bool enabled)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(dialogId);
@@ -17,5 +17,8 @@ public interface IBackfillPacer
/// <param name="maxSeconds">Верхняя граница паузы, секунды (≥ minSeconds).</param>
/// <param name="cancellationToken">Отмена ожидания.</param>
/// <returns>Задача ожидания.</returns>
public Task WaitAsync(double minSeconds, double maxSeconds, CancellationToken cancellationToken);
public Task WaitAsync(
double minSeconds,
double maxSeconds,
CancellationToken cancellationToken);
}
@@ -7,7 +7,10 @@ namespace Deal.Telegram.Dialogs;
public sealed class RandomBackfillPacer : IBackfillPacer
{
/// <inheritdoc />
public async Task WaitAsync(double minSeconds, double maxSeconds, CancellationToken cancellationToken)
public async Task WaitAsync(
double minSeconds,
double maxSeconds,
CancellationToken cancellationToken)
{
if (maxSeconds < 0 || minSeconds < 0 || minSeconds > maxSeconds)
{
@@ -47,7 +47,11 @@ public sealed class RealtimeSweep
/// <param name="catalog">Зеркало каталога/мониторинга (актуализируется ответом SyncDialogs).</param>
/// <param name="ingress">Канал в ядро (PushMessage/SyncDialogs).</param>
/// <param name="logger">Логгер.</param>
public RealtimeSweep(SessionFarm sessionFarm, DialogCatalog catalog, ICoreIngressClient ingress, ILogger<RealtimeSweep> logger)
public RealtimeSweep(
SessionFarm sessionFarm,
DialogCatalog catalog,
ICoreIngressClient ingress,
ILogger<RealtimeSweep> logger)
{
_sessionFarm = sessionFarm;
_catalog = catalog;
@@ -136,7 +140,10 @@ public sealed class RealtimeSweep
// tenantId: Id тенанта.
// dialog: Диалог с unread_count &gt; 0.
// cancellationToken: Отмена операции.
private async Task SweepDialogAsync(string tenantId, TelegramDialog dialog, CancellationToken cancellationToken)
private async Task SweepDialogAsync(
string tenantId,
TelegramDialog dialog,
CancellationToken cancellationToken)
{
try
{
@@ -48,7 +48,10 @@ public sealed class DiscoveryOps
/// <param name="sessionFarm">Пул сессий тенантов (операции только на сессии своего тенанта).</param>
/// <param name="pacer">Анти-бан-паузы (реальный — случайные 2–4 с, тесты — фейк).</param>
/// <param name="logger">Логгер.</param>
public DiscoveryOps(SessionFarm sessionFarm, IBackfillPacer pacer, ILogger<DiscoveryOps> logger)
public DiscoveryOps(
SessionFarm sessionFarm,
IBackfillPacer pacer,
ILogger<DiscoveryOps> logger)
{
_sessionFarm = sessionFarm;
_pacer = pacer;
@@ -65,7 +68,11 @@ public sealed class DiscoveryOps
/// <param name="limit">Верхняя граница результата (≤ 0 — прототип-дефолт 30).</param>
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Найденные источники (каналы/группы/личные) без дублей, не длиннее limit.</returns>
public async Task<IReadOnlyList<TelegramDialog>> SearchAsync(string tenantId, string query, int limit, CancellationToken cancellationToken)
public async Task<IReadOnlyList<TelegramDialog>> SearchAsync(
string tenantId,
string query,
int limit,
CancellationToken cancellationToken)
{
int effectiveLimit = limit > 0 ? limit : SearchDefaultLimit;
IReadOnlyList<TelegramDialog> found = await _sessionFarm
@@ -84,7 +91,10 @@ public sealed class DiscoveryOps
/// <param name="dialogId">Подписанный id источника («-100…»/«-…»/«+…»).</param>
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Инфо об источнике (сбои определения не бросаются — см. ISessionClient.GetInfoAsync).</returns>
public Task<TelegramSourceInfo> GetInfoAsync(string tenantId, string dialogId, CancellationToken cancellationToken)
public Task<TelegramSourceInfo> GetInfoAsync(
string tenantId,
string dialogId,
CancellationToken cancellationToken)
=> _sessionFarm.GetInfoAsync(tenantId, dialogId, cancellationToken);
/// <summary>
@@ -95,7 +105,11 @@ public sealed class DiscoveryOps
/// <param name="limit">Размер выборки (≤ 0 — дефолт; форумы читаются по активным темам).</param>
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>ok/сообщения либо ok=false/error="no_history" (не ошибка RPC).</returns>
public Task<DiscoveryReadResult> ReadForEvalAsync(string tenantId, string dialogId, int limit, CancellationToken cancellationToken)
public Task<DiscoveryReadResult> ReadForEvalAsync(
string tenantId,
string dialogId,
int limit,
CancellationToken cancellationToken)
=> _sessionFarm.ReadForEvalAsync(tenantId, dialogId, limit > 0 ? limit : ReadForEvalDefaultLimit, cancellationToken);
/// <summary>
@@ -106,7 +120,10 @@ public sealed class DiscoveryOps
/// <param name="tenantId">Id тенанта.</param>
/// <param name="username">Username источника («@»/пробелы нормализуются).</param>
/// <param name="cancellationToken">Отмена операции.</param>
public async Task JoinAsync(string tenantId, string username, CancellationToken cancellationToken)
public async Task JoinAsync(
string tenantId,
string username,
CancellationToken cancellationToken)
{
string normalized = NormalizeUsername(username);
if (normalized.Length == 0)
@@ -124,7 +141,10 @@ public sealed class DiscoveryOps
/// <param name="tenantId">Id тенанта.</param>
/// <param name="dialogId">Подписанный id диалога.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task LeaveAsync(string tenantId, string dialogId, CancellationToken cancellationToken)
public Task LeaveAsync(
string tenantId,
string dialogId,
CancellationToken cancellationToken)
=> _sessionFarm.LeaveAsync(tenantId, dialogId, cancellationToken);
/// <summary>
@@ -18,7 +18,10 @@ public sealed class SessionException : Exception
/// <param name="code">gRPC-статус ошибки (контракт telegram.proto, шапка файла).</param>
/// <param name="message">Текст причины — detail RPC (1:1 с текстами прототипа).</param>
/// <param name="innerException">Внутренняя причина (исключение Telegram/адаптера), если есть.</param>
public SessionException(StatusCode code, string message, Exception? innerException = null)
public SessionException(
StatusCode code,
string message,
Exception? innerException = null)
: base(message, innerException)
{
Code = code;
@@ -56,7 +56,10 @@ public sealed class SessionFarm
/// <param name="tenantId">Id тенанта.</param>
/// <param name="limit">Верхняя граница числа диалогов.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task<IReadOnlyList<TelegramDialog>> ListDialogsAsync(string tenantId, int limit, CancellationToken cancellationToken)
public Task<IReadOnlyList<TelegramDialog>> ListDialogsAsync(
string tenantId,
int limit,
CancellationToken cancellationToken)
=> RequireSession(tenantId).ListDialogsAsync(limit, cancellationToken);
/// <summary>
@@ -66,7 +69,11 @@ public sealed class SessionFarm
/// <param name="dialogId">Подписанный id диалога.</param>
/// <param name="limit">Сколько последних сообщений запросить.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task<IReadOnlyList<TelegramMessage>> GetMessagesAsync(string tenantId, string dialogId, int limit, CancellationToken cancellationToken)
public Task<IReadOnlyList<TelegramMessage>> GetMessagesAsync(
string tenantId,
string dialogId,
int limit,
CancellationToken cancellationToken)
=> RequireSession(tenantId).GetMessagesAsync(dialogId, limit, cancellationToken);
/// <summary>
@@ -75,7 +82,10 @@ public sealed class SessionFarm
/// <param name="tenantId">Id тенанта.</param>
/// <param name="dialogId">Подписанный id диалога.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task MarkReadAsync(string tenantId, string dialogId, CancellationToken cancellationToken)
public Task MarkReadAsync(
string tenantId,
string dialogId,
CancellationToken cancellationToken)
=> RequireSession(tenantId).MarkReadAsync(dialogId, cancellationToken);
/// <summary>
@@ -85,7 +95,11 @@ public sealed class SessionFarm
/// <param name="query">Поисковый запрос (ключ задачи discovery).</param>
/// <param name="limit">Верхняя граница результата.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task<IReadOnlyList<TelegramDialog>> SearchAsync(string tenantId, string query, int limit, CancellationToken cancellationToken)
public Task<IReadOnlyList<TelegramDialog>> SearchAsync(
string tenantId,
string query,
int limit,
CancellationToken cancellationToken)
=> RequireSession(tenantId).SearchAsync(query, limit, cancellationToken);
/// <summary>
@@ -94,7 +108,10 @@ public sealed class SessionFarm
/// <param name="tenantId">Id тенанта.</param>
/// <param name="dialogId">Подписанный id источника.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task<TelegramSourceInfo> GetInfoAsync(string tenantId, string dialogId, CancellationToken cancellationToken)
public Task<TelegramSourceInfo> GetInfoAsync(
string tenantId,
string dialogId,
CancellationToken cancellationToken)
=> RequireSession(tenantId).GetInfoAsync(dialogId, cancellationToken);
/// <summary>
@@ -104,7 +121,11 @@ public sealed class SessionFarm
/// <param name="dialogId">Подписанный id источника.</param>
/// <param name="limit">Размер выборки.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task<DiscoveryReadResult> ReadForEvalAsync(string tenantId, string dialogId, int limit, CancellationToken cancellationToken)
public Task<DiscoveryReadResult> ReadForEvalAsync(
string tenantId,
string dialogId,
int limit,
CancellationToken cancellationToken)
=> RequireSession(tenantId).ReadForEvalAsync(dialogId, limit, cancellationToken);
/// <summary>
@@ -113,7 +134,10 @@ public sealed class SessionFarm
/// <param name="tenantId">Id тенанта.</param>
/// <param name="username">Username (без «@»; нормализует DiscoveryOps).</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task JoinAsync(string tenantId, string username, CancellationToken cancellationToken)
public Task JoinAsync(
string tenantId,
string username,
CancellationToken cancellationToken)
=> RequireSession(tenantId).JoinAsync(username, cancellationToken);
/// <summary>
@@ -122,7 +146,10 @@ public sealed class SessionFarm
/// <param name="tenantId">Id тенанта.</param>
/// <param name="dialogId">Подписанный id диалога.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task LeaveAsync(string tenantId, string dialogId, CancellationToken cancellationToken)
public Task LeaveAsync(
string tenantId,
string dialogId,
CancellationToken cancellationToken)
=> RequireSession(tenantId).LeaveAsync(dialogId, cancellationToken);
/// <summary>
@@ -133,7 +160,12 @@ public sealed class SessionFarm
/// <param name="apiHash">api_hash приложения.</param>
/// <param name="phone">Номер телефона.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task<TenantSessionSnapshot> StartPhoneAsync(string tenantId, int apiId, string apiHash, string phone, CancellationToken cancellationToken)
public Task<TenantSessionSnapshot> StartPhoneAsync(
string tenantId,
int apiId,
string apiHash,
string phone,
CancellationToken cancellationToken)
=> GetOrCreate(tenantId).StartPhoneAsync(apiId, apiHash, phone, cancellationToken);
/// <summary>
@@ -143,7 +175,11 @@ public sealed class SessionFarm
/// <param name="apiId">api_id приложения.</param>
/// <param name="apiHash">api_hash приложения.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task<TenantSessionSnapshot> StartQrAsync(string tenantId, int apiId, string apiHash, CancellationToken cancellationToken)
public Task<TenantSessionSnapshot> StartQrAsync(
string tenantId,
int apiId,
string apiHash,
CancellationToken cancellationToken)
=> GetOrCreate(tenantId).StartQrAsync(apiId, apiHash, cancellationToken);
/// <summary>
@@ -152,7 +188,10 @@ public sealed class SessionFarm
/// <param name="tenantId">Id тенанта.</param>
/// <param name="code">Код.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task<TenantSessionSnapshot> SendCodeAsync(string tenantId, string code, CancellationToken cancellationToken)
public Task<TenantSessionSnapshot> SendCodeAsync(
string tenantId,
string code,
CancellationToken cancellationToken)
=> RequireSession(tenantId).SendCodeAsync(code, cancellationToken);
/// <summary>
@@ -161,7 +200,10 @@ public sealed class SessionFarm
/// <param name="tenantId">Id тенанта.</param>
/// <param name="password">Пароль.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public Task<TenantSessionSnapshot> SendPasswordAsync(string tenantId, string password, CancellationToken cancellationToken)
public Task<TenantSessionSnapshot> SendPasswordAsync(
string tenantId,
string password,
CancellationToken cancellationToken)
=> RequireSession(tenantId).SendPasswordAsync(password, cancellationToken);
/// <summary>
@@ -32,7 +32,10 @@ public sealed class SessionStore
/// <param name="options">Опции хранения сессий (каталог, ключ шифрования).</param>
/// <param name="cipher">AES-GCM-обёртка файлов сессий.</param>
/// <param name="logger">Логгер.</param>
public SessionStore(TgOptions options, SessionFileCipher cipher, ILogger<SessionStore> logger)
public SessionStore(
TgOptions options,
SessionFileCipher cipher,
ILogger<SessionStore> logger)
{
_sessionsDirectory = options.SessionsDirectory;
_cipher = cipher;
@@ -101,7 +104,10 @@ public sealed class SessionStore
/// <param name="tenantId">Id тенанта.</param>
/// <param name="session">Открытое содержимое сессии для шифрования at-rest.</param>
/// <param name="cancellationToken">Отмена операции.</param>
public async Task SaveAsync(string tenantId, StoredSession session, CancellationToken cancellationToken = default)
public async Task SaveAsync(
string tenantId,
StoredSession session,
CancellationToken cancellationToken = default)
{
string filePath = PathFor(tenantId);
byte[] plainBytes = JsonSerializer.SerializeToUtf8Bytes(session);
@@ -90,7 +90,11 @@ public sealed class TenantSession : IAsyncDisposable
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Снимок состояния после операции (фаза "code").</returns>
/// <exception cref="SessionException">Нет ключей (INVALID_ARGUMENT) / ошибки Telegram.</exception>
public async Task<TenantSessionSnapshot> StartPhoneAsync(int apiId, string apiHash, string phone, CancellationToken cancellationToken)
public async Task<TenantSessionSnapshot> StartPhoneAsync(
int apiId,
string apiHash,
string phone,
CancellationToken cancellationToken)
{
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
@@ -148,7 +152,10 @@ public sealed class TenantSession : IAsyncDisposable
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Снимок состояния: Qr с qrUrl или Ready (авторизация уже была).</returns>
/// <exception cref="SessionException">Нет ключей (INVALID_ARGUMENT) / ошибки Telegram до первого URL.</exception>
public async Task<TenantSessionSnapshot> StartQrAsync(int apiId, string apiHash, CancellationToken cancellationToken)
public async Task<TenantSessionSnapshot> StartQrAsync(
int apiId,
string apiHash,
CancellationToken cancellationToken)
{
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
@@ -410,7 +417,10 @@ public sealed class TenantSession : IAsyncDisposable
/// <param name="limit">Сколько последних сообщений запросить.</param>
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Сообщения диалога (от новых к старым, непустые тексты).</returns>
public async Task<IReadOnlyList<TelegramMessage>> GetMessagesAsync(string dialogId, int limit, CancellationToken cancellationToken)
public async Task<IReadOnlyList<TelegramMessage>> GetMessagesAsync(
string dialogId,
int limit,
CancellationToken cancellationToken)
{
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
@@ -452,7 +462,10 @@ public sealed class TenantSession : IAsyncDisposable
/// <param name="limit">Верхняя граница результата.</param>
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Найденные источники (нейтральный вид; личные чаты отсеивает ядро, Ruling 10).</returns>
public async Task<IReadOnlyList<TelegramDialog>> SearchAsync(string query, int limit, CancellationToken cancellationToken)
public async Task<IReadOnlyList<TelegramDialog>> SearchAsync(
string query,
int limit,
CancellationToken cancellationToken)
{
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
@@ -493,7 +506,10 @@ public sealed class TenantSession : IAsyncDisposable
/// <param name="limit">Размер выборки (limit ≤ 0 — пусто без сети).</param>
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Результат чтения (ok/messages либо no_history).</returns>
public async Task<DiscoveryReadResult> ReadForEvalAsync(string dialogId, int limit, CancellationToken cancellationToken)
public async Task<DiscoveryReadResult> ReadForEvalAsync(
string dialogId,
int limit,
CancellationToken cancellationToken)
{
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
@@ -812,7 +828,10 @@ public sealed class TenantSession : IAsyncDisposable
// apiId: api_id приложения.
// apiHash: api_hash приложения.
// cancellationToken: Отмена операции.
private async Task EnsureClientAsync(int apiId, string apiHash, CancellationToken cancellationToken)
private async Task EnsureClientAsync(
int apiId,
string apiHash,
CancellationToken cancellationToken)
{
if (_client is not null && _clientApiId == apiId && _clientApiHash == apiHash)
{
@@ -988,7 +1007,10 @@ public sealed class TenantSession : IAsyncDisposable
// firstUrlTcs: TCS первого URL.
// cancellationToken: Отмена операции.
// Возвращает: Первый URL либо null (задача завершилась без URL).
private static async Task<string?> WaitForFirstQrUrlAsync(Task qrTask, TaskCompletionSource<string?> firstUrlTcs, CancellationToken cancellationToken)
private static async Task<string?> WaitForFirstQrUrlAsync(
Task qrTask,
TaskCompletionSource<string?> firstUrlTcs,
CancellationToken cancellationToken)
{
Task<string?> urlTask = firstUrlTcs.Task;
Task completed = await Task.WhenAny(urlTask, qrTask).WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -12,6 +12,9 @@ namespace Deal.Telegram.Telegram;
public sealed class ClientFactory : ITelegramClientFactory
{
/// <inheritdoc />
public ISessionClient Create(int apiId, string apiHash, byte[]? storedSession)
public ISessionClient Create(
int apiId,
string apiHash,
byte[]? storedSession)
=> new WTelegramSessionClient(apiId, apiHash, storedSession);
}
@@ -18,7 +18,12 @@ public sealed record DiscoveryMessage
/// <param name="dateMs">Время сообщения, epoch-ms.</param>
/// <param name="topicId">Id темы форума (для обычных источников пуст).</param>
/// <param name="topicTitle">Название темы форума (для обычных источников пусто).</param>
public DiscoveryMessage(long id, string text, long dateMs, long? topicId, string? topicTitle)
public DiscoveryMessage(
long id,
string text,
long dateMs,
long? topicId,
string? topicTitle)
{
Id = id;
Text = text;
@@ -25,7 +25,10 @@ public sealed record DiscoveryReadResult
/// <param name="ok">True — выборка получена; false — история недоступна.</param>
/// <param name="error">Код причины при ok=false ("no_history"); иначе null.</param>
/// <param name="messages">Сообщения выборки (форумы — по темам, topic_id/topic_title заполнены).</param>
public DiscoveryReadResult(bool ok, string? error, IReadOnlyList<DiscoveryMessage> messages)
public DiscoveryReadResult(
bool ok,
string? error,
IReadOnlyList<DiscoveryMessage> messages)
{
Ok = ok;
Error = error;
@@ -114,7 +114,10 @@ public interface ISessionClient : IAsyncDisposable
/// <param name="limit">Сколько последних сообщений запросить.</param>
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Сообщения диалога (с канальными полями для PushMessage).</returns>
public Task<IReadOnlyList<TelegramMessage>> GetMessagesAsync(string dialogId, int limit, CancellationToken cancellationToken);
public Task<IReadOnlyList<TelegramMessage>> GetMessagesAsync(
string dialogId,
int limit,
CancellationToken cancellationToken);
/// <summary>
/// Помечает весь диалог прочитанным (send_read_acknowledge прототипа L277/L383/L454/L609).
@@ -145,7 +148,10 @@ public interface ISessionClient : IAsyncDisposable
/// <param name="limit">Верхняя граница результата (прототип: default 30).</param>
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Найденные источники в нейтральном виде (каналы/группы/личные).</returns>
public Task<IReadOnlyList<TelegramDialog>> SearchAsync(string query, int limit, CancellationToken cancellationToken);
public Task<IReadOnlyList<TelegramDialog>> SearchAsync(
string query,
int limit,
CancellationToken cancellationToken);
/// <summary>
/// Инфо об источнике для оценки кандидата (discovery_info L666716): имя/username/kind + участники
@@ -169,7 +175,10 @@ public interface ISessionClient : IAsyncDisposable
/// <param name="limit">Размер выборки (прототип: limit сообщений/тем; limit ≤ 0 — пусто без сети).</param>
/// <param name="cancellationToken">Отмена операции.</param>
/// <returns>Результат чтения выборки (ok + сообщения либо no_history).</returns>
public Task<DiscoveryReadResult> ReadForEvalAsync(string dialogId, int limit, CancellationToken cancellationToken);
public Task<DiscoveryReadResult> ReadForEvalAsync(
string dialogId,
int limit,
CancellationToken cancellationToken);
/// <summary>
/// Вступить в канал/группу по username (discovery_join L818839; ручной join вне квот — паузу перед
@@ -19,5 +19,8 @@ public interface ITelegramClientFactory
/// либо null — новая сессия (нет файла или ключи приложения сменились).
/// </param>
/// <returns>Клиент, готовый к ConnectAsync/логину.</returns>
public ISessionClient Create(int apiId, string apiHash, byte[]? storedSession);
public ISessionClient Create(
int apiId,
string apiHash,
byte[]? storedSession);
}
@@ -20,7 +20,13 @@ public sealed record TelegramDialog
/// <param name="kind">Тип канона: channel|group|forum|chat (шапка telegram.proto).</param>
/// <param name="unreadCount">Число непрочитанных сообщений (для realtime_sweep).</param>
/// <param name="topMessageId">Id самого свежего сообщения диалога (для read-ack).</param>
public TelegramDialog(string id, string name, string username, string kind, int unreadCount, int topMessageId)
public TelegramDialog(
string id,
string name,
string username,
string kind,
int unreadCount,
int topMessageId)
{
Id = id;
Name = name;
@@ -19,7 +19,13 @@ public sealed record TelegramMessage
/// <param name="dateMs">Время сообщения, epoch-ms.</param>
/// <param name="dialogName">Имя диалога (title/first_name) для PushMessage.channel_name.</param>
/// <param name="dialogHandle">Username диалога для PushMessage.channel_handle.</param>
public TelegramMessage(string dialogId, int id, string text, long dateMs, string dialogName, string dialogHandle)
public TelegramMessage(
string dialogId,
int id,
string text,
long dateMs,
string dialogName,
string dialogHandle)
{
DialogId = dialogId;
Id = id;
@@ -19,7 +19,13 @@ public sealed record TelegramSourceInfo
/// <param name="kind">Тип канона контракта: channel|group|forum|chat (пусто — не определён).</param>
/// <param name="participants">Число участников (full_chat); null — определить не удалось.</param>
/// <param name="isForum">True — мегагруппа с темами (форум; core трактует kind как forum).</param>
public TelegramSourceInfo(string id, string name, string username, string kind, int? participants, bool isForum)
public TelegramSourceInfo(
string id,
string name,
string username,
string kind,
int? participants,
bool isForum)
{
Id = id;
Name = name;
@@ -32,7 +32,10 @@ public static class TlMessageMapper
/// <param name="dialog">Диалог из ответа getDialogs.</param>
/// <param name="chats">Сущности чатов контейнера (по raw id).</param>
/// <param name="users">Сущности пользователей контейнера (по raw id).</param>
public static TelegramDialog? ToDialog(DialogBase dialog, IReadOnlyDictionary<long, ChatBase> chats, IReadOnlyDictionary<long, User> users)
public static TelegramDialog? ToDialog(
DialogBase dialog,
IReadOnlyDictionary<long, ChatBase> chats,
IReadOnlyDictionary<long, User> users)
{
if (dialog.Peer is null)
{
@@ -53,7 +56,10 @@ public static class TlMessageMapper
/// <param name="message">Сообщение (MessageBase).</param>
/// <param name="chats">Сущности чатов контейнера.</param>
/// <param name="users">Сущности пользователей контейнера.</param>
public static TelegramMessage? ToMessage(MessageBase message, IReadOnlyDictionary<long, ChatBase> chats, IReadOnlyDictionary<long, User> users)
public static TelegramMessage? ToMessage(
MessageBase message,
IReadOnlyDictionary<long, ChatBase> chats,
IReadOnlyDictionary<long, User> users)
{
if (message is not Message textMessage
|| string.IsNullOrWhiteSpace(textMessage.message)
@@ -103,7 +109,11 @@ public static class TlMessageMapper
/// <param name="topicId">Id темы форума (для обычных источников null).</param>
/// <param name="topicTitle">Название темы форума (для обычных источников null).</param>
/// <param name="now">Текущее время (фолбэк даты сообщения без времени, как python `now`).</param>
public static DiscoveryMessage? ToEvalMessage(MessageBase message, long? topicId, string? topicTitle, DateTimeOffset now)
public static DiscoveryMessage? ToEvalMessage(
MessageBase message,
long? topicId,
string? topicTitle,
DateTimeOffset now)
{
if (message is not Message textMessage
|| string.IsNullOrWhiteSpace(textMessage.message))
@@ -136,7 +146,10 @@ public static class TlMessageMapper
// signedId: Подписанный id диалога (фолбэк имени).
// chat: Сущность чата/канала (или null).
// user: Сущность пользователя (или null).
private static (string Name, string Handle, string Kind) DescribePeer(string signedId, ChatBase? chat, UserBase? user)
private static (string Name, string Handle, string Kind) DescribePeer(
string signedId,
ChatBase? chat,
UserBase? user)
{
if (chat is not null)
{
@@ -73,7 +73,10 @@ public sealed class WTelegramSessionClient : ISessionClient
/// <param name="apiId">api_id приложения Telegram.</param>
/// <param name="apiHash">api_hash приложения Telegram (же ключ внутреннего шифрования сессии).</param>
/// <param name="storedSession">Сохранённые байты сессии (null — новая сессия).</param>
public WTelegramSessionClient(int apiId, string apiHash, byte[]? storedSession)
public WTelegramSessionClient(
int apiId,
string apiHash,
byte[]? storedSession)
{
_apiId = apiId;
_apiHash = apiHash;
@@ -257,7 +260,10 @@ public sealed class WTelegramSessionClient : ISessionClient
}
/// <inheritdoc />
public async Task<IReadOnlyList<TelegramMessage>> GetMessagesAsync(string dialogId, int limit, CancellationToken cancellationToken)
public async Task<IReadOnlyList<TelegramMessage>> 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);
@@ -289,7 +295,10 @@ public sealed class WTelegramSessionClient : ISessionClient
// --- Discovery (план Task 11; discovery_search/info/read/join/leave L622873; Ruling 3/7) ---
/// <inheritdoc />
public async Task<IReadOnlyList<TelegramDialog>> SearchAsync(string query, int limit, CancellationToken cancellationToken)
public async Task<IReadOnlyList<TelegramDialog>> 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);
@@ -347,7 +356,10 @@ public sealed class WTelegramSessionClient : ISessionClient
}
/// <inheritdoc />
public async Task<DiscoveryReadResult> ReadForEvalAsync(string dialogId, int limit, CancellationToken cancellationToken)
public async Task<DiscoveryReadResult> ReadForEvalAsync(
string dialogId,
int limit,
CancellationToken cancellationToken)
{
// discovery_read L718760: limit ≤ 0 — пустой ok без сетевых вызовов (L730–732).
if (limit <= 0)
@@ -431,7 +443,11 @@ public sealed class WTelegramSessionClient : ISessionClient
// rawId: Raw id канала.
// unknown: Инфо по умолчанию (сущность недоступна).
// cancellationToken: Отмена операции.
private async Task<TelegramSourceInfo> GetChannelInfoAsync(string dialogId, long rawId, TelegramSourceInfo unknown, CancellationToken cancellationToken)
private async Task<TelegramSourceInfo> GetChannelInfoAsync(
string dialogId,
long rawId,
TelegramSourceInfo unknown,
CancellationToken cancellationToken)
{
Channel? cached;
lock (_entityCacheGate)
@@ -465,7 +481,10 @@ public sealed class WTelegramSessionClient : ISessionClient
// dialogId: Подписанный id.
// rawId: Raw id канала.
// cancellationToken: Отмена операции.
private async Task<int?> TryFetchChannelParticipantsAsync(string dialogId, long rawId, CancellationToken cancellationToken)
private async Task<int?> TryFetchChannelParticipantsAsync(
string dialogId,
long rawId,
CancellationToken cancellationToken)
{
try
{
@@ -485,7 +504,11 @@ public sealed class WTelegramSessionClient : ISessionClient
// rawId: Raw id группы (положительный).
// unknown: Инфо по умолчанию.
// cancellationToken: Отмена операции.
private async Task<TelegramSourceInfo> GetChatInfoAsync(string dialogId, long rawId, TelegramSourceInfo unknown, CancellationToken cancellationToken)
private async Task<TelegramSourceInfo> GetChatInfoAsync(
string dialogId,
long rawId,
TelegramSourceInfo unknown,
CancellationToken cancellationToken)
{
Chat? cached;
lock (_entityCacheGate)
@@ -537,7 +560,10 @@ public sealed class WTelegramSessionClient : ISessionClient
// dialogId: Подписанный id.
// group: Entity группы.
// participants: Число участников (null — определить не удалось).
private static TelegramSourceInfo DescribeGroup(string dialogId, Chat group, int? participants)
private static TelegramSourceInfo DescribeGroup(
string dialogId,
Chat group,
int? participants)
=> new(
dialogId,
DisplayName(dialogId, group),
@@ -550,7 +576,10 @@ public sealed class WTelegramSessionClient : ISessionClient
// dialogId: Подписанный id.
// rawId: Raw id пользователя.
// unknown: Инфо по умолчанию (сущность неизвестна — как entity-not-found прототипа).
private TelegramSourceInfo GetUserInfo(string dialogId, long rawId, TelegramSourceInfo unknown)
private TelegramSourceInfo GetUserInfo(
string dialogId,
long rawId,
TelegramSourceInfo unknown)
{
User? user;
lock (_entityCacheGate)
@@ -578,7 +607,10 @@ public sealed class WTelegramSessionClient : ISessionClient
// limit: Размер выборки (раскладывается по темам).
// cancellationToken: Отмена операции.
// Возвращает: Плоский список сообщений тем (пуст — темы не прочитались; фолбэк на ленту делает вызывающий).
private async Task<IReadOnlyList<DiscoveryMessage>> ReadForumTopicsAsync(InputPeer peer, int limit, CancellationToken cancellationToken)
private async Task<IReadOnlyList<DiscoveryMessage>> ReadForumTopicsAsync(
InputPeer peer,
int limit,
CancellationToken cancellationToken)
{
var outMessages = new List<DiscoveryMessage>();
try
@@ -659,7 +691,10 @@ public sealed class WTelegramSessionClient : ISessionClient
// dialogId: Подписанный id.
// channel: Entity канала из ответа полного чата.
// participants: Число участников (full_chat) либо null.
private static TelegramSourceInfo DescribeChannel(string dialogId, Channel channel, int? participants)
private static TelegramSourceInfo DescribeChannel(
string dialogId,
Channel channel,
int? participants)
=> new(
dialogId,
DisplayName(dialogId, channel),
@@ -952,7 +987,10 @@ public sealed class WTelegramSessionClient : ISessionClient
// dialogId: Подписанный id канала («-100…»).
// rawId: Raw id канала.
// cancellationToken: Отмена операции.
private async Task<InputChannel> ResolveInputChannelAsync(string dialogId, long rawId, CancellationToken cancellationToken)
private async Task<InputChannel> ResolveInputChannelAsync(
string dialogId,
long rawId,
CancellationToken cancellationToken)
{
lock (_entityCacheGate)
{
@@ -976,7 +1014,10 @@ public sealed class WTelegramSessionClient : ISessionClient
// isChannel: Признак канала («-100…»).
// isUser: Признак личного чата («+…»).
// rawId: Raw id (канала/пользователя).
private bool TryImportPeerFromCollector(bool isChannel, bool isUser, long rawId)
private bool TryImportPeerFromCollector(
bool isChannel,
bool isUser,
long rawId)
{
if (!isChannel && !isUser)
{
@@ -1022,7 +1063,11 @@ public sealed class WTelegramSessionClient : ISessionClient
// isChat: Признак базовой группы («-…»).
// isUser: Признак личного чата («+…»).
// rawId: Raw id (канала/группы/пользователя).
private InputPeer? BuildPeer(bool isChannel, bool isChat, bool isUser, long rawId)
private InputPeer? BuildPeer(
bool isChannel,
bool isChat,
bool isUser,
long rawId)
{
if (isChat)
{
@@ -1050,7 +1095,12 @@ public sealed class WTelegramSessionClient : ISessionClient
// 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)
private static bool TryParseSignedId(
string dialogId,
out bool isChannel,
out bool isChat,
out bool isUser,
out long rawId)
{
isChannel = false;
isChat = false;
@@ -473,7 +473,10 @@ public sealed class TelegramServiceImpl : TelegramService.TelegramServiceBase
// tenantId: Id тенанта.
// entries: Актуальный каталог диалогов.
// cancellationToken: Отмена операции.
private async Task SyncCatalogSilentlyAsync(string tenantId, IReadOnlyList<DialogEntry> entries, CancellationToken cancellationToken)
private async Task SyncCatalogSilentlyAsync(
string tenantId,
IReadOnlyList<DialogEntry> entries,
CancellationToken cancellationToken)
{
try
{