feat(telegram): capture document/file uploads #28

Merged
weiwen merged 2 commits from sandcastle/issue-24 into main 2026-07-05 12:03:44 +08:00
Owner

What changed and why

Implements issue #24: evie's Telegram handler now accepts documents and file uploads, not just photos and plain text.

Key changes


  • Added — downloads the file via the Bot API, saves it to a named temp directory (so pi sees the original filename), and returns an Extraction::Content with the file path embedded in the turn text.
  • Image documents (MIME type image/*) are additionally base64-encoded and passed as inline ImageContent so pi can inspect them visually, consistent with how msg.photo() is handled.
  • Non-image documents are passed as a [Document file: /path] reference only.
  • 20 MB hard cap (MAX_DOCUMENT_SIZE_BYTES) matching the Telegram Bot API getFile limit; the bot replies "File too large" and bails out cleanly instead of failing silently.
  • Replaced the Option<(String, Vec<ImageContent>, Option<NamedTempFile>)> return from extract_message_content with an Extraction enum (Content, FileTooLarge, Unhandled) to make the error case explicit.
  • Replaced raw NamedTempFile guard with a TempGuard enum (File / Dir) — documents need a TempDir so the file keeps its original name inside it; photos still use NamedTempFile.
  • sanitize_filename() strips any directory component from the Telegram-supplied filename to prevent path traversal before constructing the temp path.
  • attachment_text() consolidates the caption + path formatting used by both photo and document branches (extracted in the follow-up refactor commit).

  • Added ImageContent::with_mime_type() constructor so image documents can carry their actual MIME type instead of always hardcoding image/jpeg.

Decisions worth noting

  • The temp directory (not just the file) is kept alive via TempGuard::Dir for the full pi turn because dropping the TempDir would delete the directory and invalidate the path reference handed to pi.
  • Image documents go through both code paths (inline bytes and file path) to stay consistent with the photo flow and give pi maximum context.

Reviewer checklist

  • TempGuard lifetime: the guard is held in _temp_guard in handle_message, which is dropped after the pi call returns — confirm that's the right scope.
  • 20 MB cap: doc.file.size is a u32 in teloxide; the constant is also u32, so no truncation.
  • sanitize_filename edge-case tests pass (empty, ., .., absolute paths, traversal).

Closes #24

## What changed and why Implements issue #24: evie's Telegram handler now accepts documents and file uploads, not just photos and plain text. ### Key changes **** - Added — downloads the file via the Bot API, saves it to a named temp directory (so pi sees the original filename), and returns an `Extraction::Content` with the file path embedded in the turn text. - Image documents (MIME type `image/*`) are additionally base64-encoded and passed as inline `ImageContent` so pi can inspect them visually, consistent with how `msg.photo()` is handled. - Non-image documents are passed as a `[Document file: /path]` reference only. - 20 MB hard cap (`MAX_DOCUMENT_SIZE_BYTES`) matching the Telegram Bot API `getFile` limit; the bot replies "File too large" and bails out cleanly instead of failing silently. - Replaced the `Option<(String, Vec<ImageContent>, Option<NamedTempFile>)>` return from `extract_message_content` with an `Extraction` enum (`Content`, `FileTooLarge`, `Unhandled`) to make the error case explicit. - Replaced raw `NamedTempFile` guard with a `TempGuard` enum (`File` / `Dir`) — documents need a `TempDir` so the file keeps its original name inside it; photos still use `NamedTempFile`. - `sanitize_filename()` strips any directory component from the Telegram-supplied filename to prevent path traversal before constructing the temp path. - `attachment_text()` consolidates the caption + path formatting used by both photo and document branches (extracted in the follow-up refactor commit). **** - Added `ImageContent::with_mime_type()` constructor so image documents can carry their actual MIME type instead of always hardcoding `image/jpeg`. ### Decisions worth noting - The temp directory (not just the file) is kept alive via `TempGuard::Dir` for the full pi turn because dropping the `TempDir` would delete the directory and invalidate the path reference handed to pi. - Image documents go through both code paths (inline bytes _and_ file path) to stay consistent with the photo flow and give pi maximum context. ### Reviewer checklist - [ ] `TempGuard` lifetime: the guard is held in `_temp_guard` in `handle_message`, which is dropped after the pi call returns — confirm that's the right scope. - [ ] 20 MB cap: `doc.file.size` is a `u32` in teloxide; the constant is also `u32`, so no truncation. - [ ] `sanitize_filename` edge-case tests pass (empty, `.`, `..`, absolute paths, traversal). Closes #24
Task completed: Telegram document handler so pi can read uploaded files.
PRD: issue #24.

Key decisions:
- Introduced TempGuard enum (File/Dir variants) to generalize the
  single NamedTempFile guard to also cover TempDir, used when a document
  is saved under its original filename in a temp directory.
- Introduced Extraction enum (Content/FileTooLarge/Unhandled) replacing
  the previous Option return from extract_message_content, cleanly
  expressing the three outcomes without overloading error handling.
- Pre-checks doc.file.size > 20 MB (MAX_DOCUMENT_SIZE_BYTES); if so
  returns FileTooLarge and handle_message replies with a friendly message.
- Filename sanitized via Path::file_name() to strip path traversal; falls
  back to "document" for empty/./ .. inputs.
- image/* mime type routes through existing vision/ImageContent base64
  path (new ImageContent::with_mime_type constructor); all other types
  inject [Document file: <path>] with caption as the instruction.
- Temp dir (TempGuard::Dir) is held alive for the full pi turn via
  _temp_guard binding in handle_message, mirroring photo behaviour.

Files changed:
- src/pi.rs: add ImageContent::with_mime_type constructor
- src/telegram/mod.rs: TempGuard, Extraction enums; sanitize_filename,
  mime_is_image helpers; extract_document_content; updated
  extract_message_content and handle_message; 6 unit tests

Blockers/notes:
- No Rust toolchain in sandbox; CI must validate compilation and tests.
  Logic follows established patterns (photo download, TempGuard, enums).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor: consolidate attachment text formatting in telegram handler
Some checks failed
CI / check (pull_request) Failing after 1m13s
14ba30f999
Extract the repeated caption+marker formatting (photo, document-image,
document-other) into a single attachment_text helper, add unit tests for
it, and switch mime_is_image to the idiomatic is_some_and. No behavior
change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
weiwen merged commit e4476dcb8e into main 2026-07-05 12:03:44 +08:00
weiwen deleted branch sandcastle/issue-24 2026-07-05 12:03:44 +08:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
weiwen/evie!28
No description provided.