feat: add api and mcp
This commit is contained in:
@@ -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
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user