Больше двух параметров — каждый на отдельной строке (закрывающая скобка в конце последнего); два и меньше — в одну строку. Правило добавлено в docs/spec/Код-стайл-Дейл.md; применено к 628 сигнатурам в 253 файлах.
131 lines
8.0 KiB
C#
131 lines
8.0 KiB
C#
using System.Text.Json;
|
||
using Deal.Modules.Settings.Application.Abstractions;
|
||
using Deal.Modules.Settings.Application.Models;
|
||
using Deal.Modules.Settings.Application.Registrars;
|
||
using Deal.Modules.Settings.Application.Services;
|
||
using Deal.Modules.Discovery.Application.Abstractions;
|
||
using Deal.Modules.Discovery.Application.Exceptions;
|
||
using Deal.Modules.Discovery.Application.Extensions;
|
||
using Deal.Modules.Discovery.Application.Models;
|
||
using Deal.Modules.Discovery.Application.Registrars;
|
||
|
||
namespace Deal.Modules.Discovery.Application.Services;
|
||
|
||
/// <summary>
|
||
/// Бан-гард авто-вступлений Discovery: суточный лимит, flood-день, стоп-кран (1:1 ban_guard.py L1–81, план Task 18, Ruling 10).
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Суточный лимит (discJoinLimit, дефолт 50) считается по DiscLog: число событий event='join_auto' за текущие
|
||
/// UTC-сутки (дискретность совпадает с логом mark_joined авто-вступления — дискретный счётчик 1:1 с python
|
||
/// L29–35). При FloodWait от Telegram воркер зовёт <see cref="NoteFloodAsync"/> — блокировка авто-вступлений до
|
||
/// конца суток (внутренний ключ SettingsKeys.DiscFloodDay, epoch-ms начала суток). Стоп-кран discPaused —
|
||
/// ручная пауза вступлений. <see cref="CanAutoJoinAsync"/> — общий вердикт «разрешено ли следующее авто-
|
||
/// вступление». Настройки читаются типизированным снимком TenantSettingsSnapshot (C30): отсутствие
|
||
/// строки/повреждение → SettingsDefaults.
|
||
/// <para>
|
||
/// Паузу между вступлениями (discJoinDelayMin..Max, 50–70 с) гард НЕ держит — её делает <see cref="IDiscoveryPacer"/>
|
||
/// (рандом недоступен тестам; 1:1 ban_guard.wait_join_delay L44–56).
|
||
/// </para>
|
||
/// </remarks>
|
||
public sealed class DiscoveryBanGuard
|
||
{
|
||
private readonly IDiscoveryStore _store;
|
||
private readonly ISettingsStore _settings;
|
||
private readonly Func<DateTimeOffset> _utcNow;
|
||
|
||
/// <summary>
|
||
/// Создаёт бан-гард.
|
||
/// </summary>
|
||
/// <param name="store">Хранилище Discovery (счётчик DiscLog за сутки).</param>
|
||
/// <param name="settings">KV-настройки тенанта (discJoinLimit/discFloodDay/discPaused).</param>
|
||
/// <param name="utcNow">Источник текущего времени (UTC; тесты передают фиксированные «часы», эталон
|
||
/// FakeDiscoveryStore). По умолчанию — <see cref="DateTimeOffset.UtcNow"/>.</param>
|
||
public DiscoveryBanGuard(
|
||
IDiscoveryStore store,
|
||
ISettingsStore settings,
|
||
Func<DateTimeOffset>? utcNow = null)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(store);
|
||
ArgumentNullException.ThrowIfNull(settings);
|
||
_store = store;
|
||
_settings = settings;
|
||
_utcNow = utcNow ?? (() => DateTimeOffset.UtcNow);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Ручной стоп-кран авто-вступлений (setting discPaused; ban_guard.global_paused L69–71).
|
||
/// </summary>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>True — вступления на паузе (воркер не делает сетевых шагов).</returns>
|
||
public async Task<bool> GlobalPausedAsync(CancellationToken ct)
|
||
{
|
||
TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct);
|
||
return settingsSnapshot.GetBool(SettingsKeys.DiscPaused, SettingsDefaults.DiscPaused);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Авто-вступления за текущие UTC-сутки (ban_guard.joins_today_auto L29–35).
|
||
/// </summary>
|
||
/// <remarks>События DiscLog event='join_auto' с CreatedAt ≥ начала текущих UTC-суток (все тенанты держат
|
||
/// времена UTC; счётчик учитывает и строки ручных вступлений с auto=false? — нет: фильтр по событию join_auto).</remarks>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Число авто-вступлений сегодня (0 — вступлений нет).</returns>
|
||
public Task<int> JoinsTodayAutoAsync(CancellationToken ct)
|
||
=> _store.CountLogEventAsync(DiscoveryLogEvents.JoinAuto, StartOfDayUtc(), ct);
|
||
|
||
/// <summary>
|
||
/// Была ли flood-блокировка в текущие UTC-сутки (ban_guard.flood_today L64–66).
|
||
/// </summary>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>True — <see cref="NoteFloodAsync"/> фиксировалась сегодня (стоп до конца суток).</returns>
|
||
public async Task<bool> FloodTodayAsync(CancellationToken ct)
|
||
{
|
||
TenantSettingsSnapshot snapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct);
|
||
return IsFloodToday(snapshot.GetLong(SettingsKeys.DiscFloodDay, 0), StartOfDayMs());
|
||
}
|
||
|
||
/// <summary>
|
||
/// Разрешено ли авто-вступление: лимит не исчерпан, нет flood на сегодня, нет стоп-крана
|
||
/// (ban_guard.can_auto_join L38–41).
|
||
/// </summary>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>True — следующее авто-вступление допустимо.</returns>
|
||
public async Task<bool> CanAutoJoinAsync(CancellationToken ct)
|
||
{
|
||
// Один типизированный снимок настроек на вердикт (C30): discJoinLimit/discFloodDay/discPaused
|
||
// читаются одним GetAllAsync вместо трёх GetAsync.
|
||
TenantSettingsSnapshot snapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct);
|
||
int limit = snapshot.GetInt(SettingsKeys.DiscJoinLimit, SettingsDefaults.DiscJoinLimit);
|
||
int joined = await JoinsTodayAutoAsync(ct).ConfigureAwait(false);
|
||
bool floodToday = IsFloodToday(snapshot.GetLong(SettingsKeys.DiscFloodDay, 0), StartOfDayMs());
|
||
bool paused = snapshot.GetBool(SettingsKeys.DiscPaused, SettingsDefaults.DiscPaused);
|
||
return joined < Math.Max(1, limit) && !floodToday && !paused;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Фиксирует FloodWait: блокировка авто-вступлений до конца суток (ban_guard.note_flood L59–62).
|
||
/// </summary>
|
||
/// <remarks>Запись идемпотентна в течение суток: DiscFloodDay = epoch-ms начала текущих UTC-суток
|
||
/// (повторный note_flood в тот же день перезаписывает тем же значением).</remarks>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task NoteFloodAsync(CancellationToken ct)
|
||
=> _settings.SetAsync(SettingsKeys.DiscFloodDay, JsonSerializer.Serialize(StartOfDayMs()), ct);
|
||
|
||
// Начало текущих UTC-суток (python _start_of_day_ms L23–26).
|
||
private DateTimeOffset StartOfDayUtc()
|
||
{
|
||
DateTimeOffset now = _utcNow().ToUniversalTime();
|
||
return new DateTimeOffset(now.Year, now.Month, now.Day, 0, 0, 0, TimeSpan.Zero);
|
||
}
|
||
|
||
// Начало текущих UTC-суток в epoch-ms (python L25).
|
||
private long StartOfDayMs() => StartOfDayUtc().ToUnixTimeMilliseconds();
|
||
|
||
// Flood-день совпал с текущими сутками (ban_guard.flood_today L64–66: 0 — блокировок не было).
|
||
// floodDay: Epoch-ms начала суток flood-блокировки (0 — не было).
|
||
// startOfDayMs: Начало текущих UTC-суток в epoch-ms.
|
||
// Возвращает: True — flood фиксировался сегодня.
|
||
private static bool IsFloodToday(long floodDay, long startOfDayMs)
|
||
=> floodDay != 0 && floodDay == startOfDayMs;
|
||
}
|