feat(watcher,api,frontend): archive who writes to watched bots over mtproto

This commit is contained in:
hh committed 2026-09-18 15:05:01 +02:00
1 parent 089a31db8d
commit 4bd031f21e
30 files changed
+1863 -10

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
from typing import Annotated
import asyncpg
from dishka.integrations.fastapi import DishkaRoute, FromDishka
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from utils.read import bots
from utils.read.models import DEFAULT_LIMIT, BotStats, BotUpdateView, BotView, Page
from watcher import tokens
router = APIRouter(prefix="/api/bots", tags=["bots"], route_class=DishkaRoute)
INVALID_TOKEN = "token must look like 123456:ABC-DEF..." # noqa: S105
class BotAdd(BaseModel):
token: str
@router.get("")
async def list_bots(pool: FromDishka[asyncpg.Pool]) -> list[BotView]:
return await bots.list_bots(pool)
@router.post("", status_code=201)
async def add_bot(pool: FromDishka[asyncpg.Pool], body: BotAdd) -> BotView:
bot_id = tokens.bot_id_of(body.token)
if bot_id is None:
raise HTTPException(status_code=422, detail=INVALID_TOKEN)
tokens.save(bot_id, body.token)
await bots.register_bot(pool, bot_id)
await bots.notify_bots_changed(pool)
bot = await bots.get_bot(pool, bot_id)
if bot is None:
raise HTTPException(status_code=500, detail="bot was not registered")
return bot
@router.delete("/{bot_id}", status_code=204)
async def remove_bot(pool: FromDishka[asyncpg.Pool], bot_id: int) -> None:
if not await bots.remove_bot(pool, bot_id):
raise HTTPException(status_code=404, detail="unknown bot")
await bots.notify_bots_changed(pool)
@router.get("/{bot_id}/stats")
async def stats(
pool: FromDishka[asyncpg.Pool],
bot_id: int,
days: Annotated[int, Query()] = 90,
senders: Annotated[int, Query()] = 20,
) -> BotStats:
result = await bots.stats(pool, bot_id, days=days, senders=senders)
if result is None:
raise HTTPException(status_code=404, detail="unknown bot")
return result
@router.get("/{bot_id}/updates")
async def list_updates(
pool: FromDishka[asyncpg.Pool],
bot_id: int,
sender_id: Annotated[int | None, Query()] = None,
chat_id: Annotated[int | None, Query()] = None,
incoming_only: Annotated[bool, Query()] = True,
limit: Annotated[int, Query()] = DEFAULT_LIMIT,
offset: Annotated[int, Query()] = 0,
) -> list[BotUpdateView]:
page = Page(limit=limit, offset=offset)
return await bots.list_updates(
pool,
bot_id,
sender_id=sender_id,
chat_id=chat_id,
incoming_only=incoming_only,
limit=page.capped_limit,
offset=page.offset,
)