feat: add api and mcp

This commit is contained in:
hh
2026-05-30 01:32:35 +02:00
parent 6a5cde6ae4
commit c40e720163
30 changed files with 2354 additions and 31 deletions
View File
+233
View File
@@ -0,0 +1,233 @@
from collections.abc import Sequence
from datetime import datetime
from typing import Any
import asyncpg
from fastmcp import FastMCP
from pydantic import BaseModel
from dependencies.container import container
from utils.jobs import enqueue
from utils.read import annotations, chats, media, peers, presence, social, watches
from utils.read.models import DEFAULT_LIMIT, Page
from utils.search.models import SearchFilters
from utils.search.repository import search_messages
mcp: FastMCP = FastMCP("beavergram")
async def _pool() -> asyncpg.Pool:
return await container.get(asyncpg.Pool)
def _dump(items: Sequence[BaseModel]) -> list[dict[str, Any]]:
return [item.model_dump(mode="json") for item in items]
@mcp.tool
async def search_messages_tool(
account_id: int,
query: str | None = None,
chat_id: int | None = None,
sender_id: int | None = None,
has_media: bool | None = None,
date_from: datetime | None = None,
date_to: datetime | None = None,
regex: str | None = None,
limit: int = DEFAULT_LIMIT,
offset: int = 0,
) -> list[dict[str, Any]]:
"""Full-text search over message text and STT transcripts."""
filters = SearchFilters(
account_id=account_id,
query=query,
chat_id=chat_id,
sender_id=sender_id,
has_media=has_media,
date_from=date_from,
date_to=date_to,
regex=regex,
limit=limit,
offset=offset,
)
return _dump(await search_messages(await _pool(), filters))
@mcp.tool
async def list_chats(
account_id: int, limit: int = DEFAULT_LIMIT, offset: int = 0
) -> list[dict[str, Any]]:
"""List archived chats with message counts and last activity."""
page = Page(limit=limit, offset=offset)
return _dump(await chats.list_chats(await _pool(), account_id, page))
@mcp.tool
async def get_chat_history(
account_id: int,
chat_id: int,
limit: int = DEFAULT_LIMIT,
offset: int = 0,
include_deleted: bool = True,
) -> list[dict[str, Any]]:
"""Read archived messages of a chat, newest first."""
return _dump(
await chats.get_chat_history(
await _pool(),
account_id,
chat_id,
Page(limit=limit, offset=offset),
include_deleted=include_deleted,
)
)
@mcp.tool
async def get_deleted_messages(
account_id: int,
chat_id: int | None = None,
limit: int = DEFAULT_LIMIT,
offset: int = 0,
) -> list[dict[str, Any]]:
"""List messages that were deleted in Telegram but kept in the archive."""
return _dump(
await chats.get_deleted_messages(
await _pool(), account_id, Page(limit=limit, offset=offset), chat_id=chat_id
)
)
@mcp.tool
async def get_message_versions(
account_id: int, chat_id: int, message_id: int
) -> list[dict[str, Any]]:
"""Get the edit history of a message."""
return _dump(
await chats.get_message_versions(await _pool(), account_id, chat_id, message_id)
)
@mcp.tool
async def get_media(
account_id: int, chat_id: int, message_id: int, fetch: bool = False
) -> dict[str, Any] | None:
"""Get media metadata for a message; set fetch=True to enqueue lazy download."""
pool = await _pool()
item = await media.get_message_media(pool, account_id, chat_id, message_id)
if item is None:
return None
if fetch and not item.downloaded:
await enqueue(
pool,
account_id,
"fetch_media",
{"chat_id": chat_id, "message_id": message_id},
)
return item.model_dump(mode="json")
@mcp.tool
async def get_callbacks(
account_id: int, chat_id: int, message_id: int
) -> list[dict[str, Any]]:
"""Get bot inline-button callback data (hex) for a message."""
items = await social.get_callbacks(await _pool(), account_id, chat_id, message_id)
return _dump(items)
@mcp.tool
async def presence_history(
account_id: int,
peer_id: int,
date_from: datetime | None = None,
date_to: datetime | None = None,
limit: int = DEFAULT_LIMIT,
offset: int = 0,
) -> list[dict[str, Any]]:
"""Get online/offline status history of a peer."""
return _dump(
await presence.presence_history(
await _pool(),
account_id,
peer_id,
Page(limit=limit, offset=offset),
date_from=date_from,
date_to=date_to,
)
)
@mcp.tool
async def get_peer_history(account_id: int, peer_id: int) -> list[dict[str, Any]]:
"""Get name/username/avatar change history of a contact."""
return _dump(await peers.get_peer_history(await _pool(), account_id, peer_id))
@mcp.tool
async def get_stories(
account_id: int,
peer_id: int | None = None,
limit: int = DEFAULT_LIMIT,
offset: int = 0,
) -> list[dict[str, Any]]:
"""List archived stories of contacts."""
return _dump(
await peers.get_stories(
await _pool(), account_id, Page(limit=limit, offset=offset), peer_id=peer_id
)
)
@mcp.tool
async def get_annotations(
account_id: int,
chat_id: int | None = None,
message_id: int | None = None,
limit: int = DEFAULT_LIMIT,
offset: int = 0,
) -> list[dict[str, Any]]:
"""Read user annotations on messages (read-only via MCP)."""
return _dump(
await annotations.list_annotations(
await _pool(),
account_id,
Page(limit=limit, offset=offset),
chat_id=chat_id,
message_id=message_id,
)
)
@mcp.tool
async def set_watch(
account_id: int,
kind: str,
params: dict[str, Any] | None = None,
enabled: bool = True,
) -> dict[str, Any]:
"""Create a local watch rule (the only local write MCP is allowed)."""
watch = await watches.create_watch(
await _pool(), account_id, kind, params or {}, enabled=enabled
)
return watch.model_dump(mode="json")
@mcp.tool
async def list_watches(account_id: int) -> list[dict[str, Any]]:
"""List local watch rules."""
return _dump(await watches.list_watches(await _pool(), account_id))
@mcp.tool
async def list_alerts(
account_id: int,
seen: bool | None = None,
limit: int = DEFAULT_LIMIT,
offset: int = 0,
) -> list[dict[str, Any]]:
"""List fired alerts from watch rules."""
return _dump(
await watches.list_alerts(
await _pool(), account_id, Page(limit=limit, offset=offset), seen=seen
)
)