"""BanGuard: квоты, паузы и защита от flood для авто-вступлений Discovery. Суточный лимит (discJoinLimit) считается по disc_log (event='join_auto') за текущие UTC-сутки; между вступлениями выдерживается случайная пауза (discJoinDelayMin/Max). При FloodWait от Telegram ставится блокировка до конца суток (discFloodDay), плюс есть ручной стоп-кран (discPaused). """ from __future__ import annotations import asyncio import random from datetime import datetime, timezone from ..db import store _KEY_JOIN_LIMIT = "discJoinLimit" _KEY_DELAY_MIN = "discJoinDelayMin" _KEY_DELAY_MAX = "discJoinDelayMax" _KEY_FLOOD_DAY = "discFloodDay" _KEY_PAUSED = "discPaused" def _start_of_day_ms() -> int: """Начало текущих UTC-суток в миллисекундах.""" start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) return int(start.timestamp() * 1000) def joins_today_auto() -> int: """Авто-вступления за текущие UTC-сутки (disc_log, event='join_auto').""" row = store.scalar( "SELECT count(*) FROM disc_log WHERE event = 'join_auto' AND created_at >= ?", [_start_of_day_ms()], ) return int(row or 0) def can_auto_join() -> bool: """Разрешено ли авто-вступление: лимит не исчерпан, нет flood на сегодня, нет паузы.""" limit = int(store.get_setting(_KEY_JOIN_LIMIT) or 0) return joins_today_auto() < limit and not flood_today() and not global_paused() async def wait_join_delay() -> None: """Случайная пауза перед авто-вступлением (сек, из настроек). Защита инварианта min <= max: настройки мог изменить один конец интервала (single-key PATCH), поэтому при инверсии концы меняются местами. """ low = float(store.get_setting(_KEY_DELAY_MIN) or 0) high = float(store.get_setting(_KEY_DELAY_MAX) or 0) if low <= 0 and high <= 0: return # обе настройки не заданы — паузы нет (крайний случай) if low > high: low, high = high, low await asyncio.sleep(random.uniform(low, high)) def note_flood() -> None: """Зафиксировать FloodWait: блокировка авто-вступлений до конца суток.""" store.set_setting(_KEY_FLOOD_DAY, _start_of_day_ms()) def flood_today() -> bool: """Была ли flood-блокировка в текущие UTC-сутки.""" return int(store.get_setting(_KEY_FLOOD_DAY) or 0) == _start_of_day_ms() def global_paused() -> bool: """Ручной стоп-кран авто-вступлений (setting discPaused).""" return bool(store.get_setting(_KEY_PAUSED)) def set_global_pause(v: bool) -> None: store.set_setting(_KEY_PAUSED, bool(v)) def search_pause() -> float: """Пауза между поисковыми запросами Telegram (сек).""" return random.uniform(2.0, 4.0)