feat(mcp): download any message file via share link

This commit is contained in:
hh committed 2026-10-08 21:22:59 +02:00
1 parent 7f4d500302
commit 283eab1eb0
2 files changed
+162 -19

No files matched your search

+49 -19
View File
@@ -7,12 +7,19 @@ from fastmcp import FastMCP
from fastmcp.exceptions import ToolError
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
from fastmcp.tools.base import ToolResult
from mcp.types import CallToolRequestParams
from mcp.types import CallToolRequestParams, TextContent
from pydantic import BaseModel
from api.mcp.download import (
MAX_WAIT_SECONDS,
describe,
fetch_media,
inline_content,
share_media,
)
from api.mcp.format import build_transcript, load_notes, load_photos, resolve_names
from dependencies.container import container
from utils.jobs import enqueue
from utils.files import resolve_mime
from utils.read import annotations, chats, media, peers, presence, social, watches
from utils.read.accounts import self_user_id, unknown_account_hint
from utils.read.models import DEFAULT_LIMIT, Page
@@ -44,7 +51,7 @@ Typical flows:
`before_id` (first id you saw), to newer with `after_id` (last id you saw);
begin a full forward walk with `after_id=0`. Each transcript line starts
with its `#message_id`, and photos show as `[photo #message_id]` — pass that
id to `get_media` (with `fetch=True` to download) or to annotation tools.
id to `download_media` or to annotation tools.
Lines marked
"📝 [your private note, NOT in Telegram]" are the user's own annotations
attached locally via the web UI; they never existed in Telegram. Treat them
@@ -53,8 +60,11 @@ Typical flows:
transcripts; supports chat/sender/date filters and regex).
- Forensics: `get_deleted_messages` and `get_message_versions` recover content
removed or edited in Telegram but kept in the archive.
- Media: `get_media` returns metadata; pass `fetch=True` to enqueue a lazy
download if the file isn't stored yet.
- Files: `download_media` gets any message attachment (photo, video, voice,
document, ...). It pulls the file from Telegram if the archive lacks it and
returns a public link valid 24h that needs no auth (`curl -O <link>`), plus
the content itself for small images and text files. `get_media` returns
metadata only.
- Monitoring: `set_watch` creates a local rule, `list_watches` lists rules, and
`list_alerts` reads what those rules fired.
@@ -63,7 +73,8 @@ Watched bots:
own `account_id` and its chats, messages and media read through the same
tools as a user account. Nothing bot-specific to call.
Writes: everything is read-only except `set_watch`, the only allowed write.
Writes: everything is read-only except `set_watch` and the share links
`download_media` creates (visible and revocable in the web UI).
Paging: list/search tools accept `limit` and `offset`.
"""
@@ -239,21 +250,40 @@ async def get_message_versions(
@mcp.tool
async def get_media(
account_id: int, chat_id: int, message_id: int, fetch: bool = False
account_id: int, chat_id: int, message_id: int
) -> dict[str, Any] | None:
"""Get media metadata for a message; set fetch=True to enqueue lazy download."""
"""Get media metadata for a message; use download_media to get the file."""
item = await media.get_message_media(await _pool(), account_id, chat_id, message_id)
return item.model_dump(mode="json") if item else None
@mcp.tool(output_schema=None)
async def download_media(
account_id: int, chat_id: int, message_id: int, wait_seconds: int = 45
) -> list[Any]:
"""Download the file attached to a message: any photo, video, voice, document.
Pulls it from Telegram first if the archive doesn't have it yet, waiting up
to `wait_seconds`. Returns a public download link (valid 24h, no auth) and
inlines small images and text files.
"""
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")
wait = min(max(wait_seconds, 0), MAX_WAIT_SECONDS)
item = await fetch_media(pool, account_id, chat_id, message_id, wait)
if item is None or item.storage_key is None:
return [
TextContent(
type="text",
text="Still downloading from Telegram; call download_media again.",
)
]
mime = resolve_mime(item.kind, item.mime, item.file_name)
share = await share_media(pool, item, mime)
storage = await container.get(ContentAddressedStorage)
return [
TextContent(type="text", text=describe(item, mime, share)),
*inline_content(storage, item.storage_key, mime),
]
@mcp.tool