feat(alerts): алерты всей команде — детекторы, дедупликация, список (#15)
CI / ruff · mypy · pytest (pull_request) Successful in 4m49s
CI / ruff · mypy · pytest (pull_request) Successful in 4m49s
- детекторы по состоянию после каждого раунда снапшотов: краш, OOM, цикл рестартов, unhealthy, перегрузка ≥ 1 мин, мало места, Docker недоступен; пороги в config/alerts.yaml - AlertService: ключ kind+service, повтор в 10 мин — счётчик в исходном сообщении, «✅ восстановлено» ответом; разовые события (🛡) со счётчиком - рассылка всем активным (🛡 — только админам), темп ~25/с, RetryAfter, заблокировавший бота пропускается; alert_deliveries для правок и ответов - «🚨 Алерты»: активные + 7 дней по 10, карточка; кнопки «📜 Логи», «🩺 Статус» под алертом открывают новое сообщение, а не затирают алерт - 🛡 атаки через агента идут через общий сервис алертов - убраны заглушки «Раздел в разработке» — у всех разделов свои роутеры Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+16
-4
@@ -1,12 +1,13 @@
|
||||
"""Offline Telegram: a fake aiogram session that records outgoing API calls and fabricates replies."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from datetime import UTC, datetime
|
||||
from itertools import count
|
||||
from typing import Any
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.client.session.base import BaseSession
|
||||
from aiogram.exceptions import TelegramForbiddenError
|
||||
from aiogram.methods import EditMessageText, GetFile, GetMe, SendMessage, SendPhoto, TelegramMethod
|
||||
from aiogram.methods.base import TelegramType
|
||||
from aiogram.types import Chat, File, Message, Update, User
|
||||
@@ -20,6 +21,7 @@ class FakeSession(BaseSession):
|
||||
self.requests: list[TelegramMethod[Any]] = []
|
||||
self._ids = count(1000)
|
||||
self.files: dict[str, bytes] = {} # file_id -> content, for documents users "upload"
|
||||
self.blocked: set[int] = set() # chats that blocked the bot: sends there raise 403
|
||||
|
||||
async def make_request(
|
||||
self,
|
||||
@@ -28,6 +30,8 @@ class FakeSession(BaseSession):
|
||||
timeout: int | None = None, # noqa: ASYNC109 - signature fixed by aiogram BaseSession
|
||||
) -> TelegramType:
|
||||
self.requests.append(method)
|
||||
if isinstance(method, SendMessage) and method.chat_id in self.blocked:
|
||||
raise TelegramForbiddenError(method=method, message="Forbidden: bot was blocked by the user")
|
||||
if isinstance(method, GetFile):
|
||||
return File( # type: ignore[return-value]
|
||||
file_id=method.file_id, file_unique_id=method.file_id, file_path=f"documents/{method.file_id}"
|
||||
@@ -73,10 +77,11 @@ class FakeSession(BaseSession):
|
||||
class TgClient:
|
||||
"""Feeds updates through a real Dispatcher (filters, middlewares, FSM) on top of FakeSession."""
|
||||
|
||||
def __init__(self, dp: Dispatcher) -> None:
|
||||
self.dp = dp
|
||||
def __init__(self, dp: Dispatcher | Callable[[Bot], Dispatcher]) -> None:
|
||||
"""dp: a dispatcher, or a factory for one that needs the bot (alert broadcasts send through it)."""
|
||||
self.session = FakeSession()
|
||||
self.bot = Bot(BOT_TOKEN, session=self.session)
|
||||
self.dp = dp if isinstance(dp, Dispatcher) else dp(self.bot)
|
||||
self._update_ids = count(1)
|
||||
self._message_ids = count(1)
|
||||
|
||||
@@ -109,7 +114,13 @@ class TgClient:
|
||||
}
|
||||
await self._feed({"message": message})
|
||||
|
||||
async def press(self, tg_id: int, data: str, message_text: str = "section") -> None:
|
||||
async def press_markup(self, tg_id: int, data: str, markup: Any) -> None:
|
||||
"""Press a button on a message that carries this inline keyboard (some handlers look at it)."""
|
||||
await self.press(tg_id, data, reply_markup=markup.model_dump(exclude_none=True) if markup else None)
|
||||
|
||||
async def press(
|
||||
self, tg_id: int, data: str, message_text: str = "section", reply_markup: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
callback = {
|
||||
"id": str(next(self._update_ids)),
|
||||
"from": self.user(tg_id),
|
||||
@@ -121,6 +132,7 @@ class TgClient:
|
||||
"chat": {"id": tg_id, "type": "private"},
|
||||
"from": {"id": 42, "is_bot": True, "first_name": "tgit"},
|
||||
"text": message_text,
|
||||
**({"reply_markup": reply_markup} if reply_markup else {}),
|
||||
},
|
||||
}
|
||||
await self._feed({"callback_query": callback})
|
||||
|
||||
Reference in New Issue
Block a user