feat(mcp): download any message file via share link
This commit is contained in:
1 parent
7f4d500302
commit
283eab1eb0
2 files changed
+162
-19
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user