Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
81 lines
3.3 KiB
Python
81 lines
3.3 KiB
Python
"""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)
|