diff --git a/AGENTS.md b/AGENTS.md index fc59308..1135abc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Telegram bot (teloxide) that turns post links from X/Twitter, Pixiv, Bluesky, Misskey (misskey.io), and Bilibili dynamics into media messages (images, video, GIF) with the post's title, author, and tags. It supports batch media splitting, retry with persistence, inline queries, forward-channel rebinding with caption templates, and Pixiv ugoira→MP4 transcoding. README is in Chinese; user-facing bot strings are in English. The project is a Rust port of a Python predecessor (see `queue.rs` comments referencing `utils/task_queue.py`). -Two-crate Cargo workspace (both v1.9.3, edition 2024, resolver 3): +Two-crate Cargo workspace (both v1.9.4, edition 2024, resolver 3): - **`crates/x-media`** — library that fetches and normalizes media from the four sites. Pure, no Telegram knowledge. - **`crates/xmedia-bot`** — the bot binary: teloxide dispatcher, SQLite-backed chat state, persistent task queue. @@ -46,7 +46,7 @@ The `x-media` library: `site::fetch(url)` dispatches through the `SITES` registr | `crates/xmedia-bot/src/link_cache.rs` | `LinkCache`: SQLite-backed cache (`link_cache` table) of successfully sent posts — raw caption fields + the source media URLs + Telegram `file_id`s; repeat links re-send locally (no fetch/upload), TTL + prune; a permanent send failure *degrades* the entry instead of dropping it (the file ids go, the URLs stay, so the next request re-sends from those without a fetch), and a degraded entry that fails again is removed | | `crates/xmedia-bot/src/queue.rs` | `PersistentTaskQueue`: SQLite-backed queue (`tasks` table), `QUEUE_WORKERS = 4` concurrent workers (lease via `BEGIN IMMEDIATE` + `locked_until` TTL), retry→dead-letter, a `lease_token` fence: `lease_next` stamps a random token and every write-back (heartbeat, `delete`, `reschedule`, `mark_done`) is guarded by it, so a lease that expired and was re-leased cannot be written by its former holder — a lost lease stops the attempt instead (and the handler verifies ownership with a fresh read — `Lease`/`lease_still_held`, carried on a task-local — before running side effects a re-lease would duplicate: a channel forward, an edit prompt, the keep-alive release); a finished row's `DELETE`/reschedule retried and a failed delete falling back to a `done` tombstone (the lease query and the sweep only look at `pending`/`in_progress`, so a task that already ran cannot be resurrected and re-run), `runnable_rows`/`replace_payload` (the startup repair's read/rewrite path: it runs before the workers exist, which is why it needs no lease token), `notify_one` worker wakeup plus a separate `Notify` for the 30 s lease-expiry sweep (a shared one let the sweep steal the workers' wakeup permit; the sweep does notify the workers after it actually recovered a row, since a recovered task is due immediately while every worker may be parked on `notify` with no pending row to sleep on), `busy_timeout` on all connections | | `crates/xmedia-bot/src/ctx.rs` | `AppContext`: the injected collaborators (`sender` + `ChatStore`/`PersistentTaskQueue`/`LinkCache`/`Config`), `from_statics` for production and the `CONTEXT` static the worker closures hold. `test_support::TestStores` backs handler tests with a tempdir store set, and the module also carries the fixtures those tests share — the canonical cached post (`cached_photo`), the edit-before-forward prompt (`seed_prompt` with its `PROMPT_ID`/`FORWARDED_ID`) and a scripted API error (`api_error`) — so no two test modules keep their own copies | -| `crates/xmedia-bot/src/send/` | `send/mod.rs`: `Task`/`MediaItemPayload` payloads (`media: MediaRef`, i.e. `Source` URL-or-path vs `FileId` — one field used to carry both with a flag), `send_media_sequence`/`send_animation`/`forward_messages`; `send/error.rs`: the Bot API error policy (`SendError`/`Classification`, `classify_request_error`, the media-fetch/size markers); `send/input_media.rs`: payload → `InputFile`/`InputMedia` + `build_media_group` (caption on the first item only); `send/upload.rs`: the download-and-reupload fallback (`prepare_upload_item`/`send_batch_via_upload`, photo downscale handoff); `send/post_send.rs`: link-cache write, `KEEP_ALIVE` registry, `settle_task`, `post_send_actions`, `handle_task`/`dead_letter_notify` | +| `crates/xmedia-bot/src/send/` | `send/mod.rs`: `Task`/`MediaItemPayload` payloads (`media: MediaRef`, i.e. `Source` URL-or-path vs `FileId` — one field used to carry both with a flag), `send_media_sequence`/`send_animation`/`forward_messages`; `send/error.rs`: the Bot API error policy (`SendError`/`Classification`, `classify_request_error`, the media-fetch/size markers); `send/input_media.rs`: payload → `InputFile`/`InputMedia` + `build_media_group` (caption on the first item only); `send/upload.rs`: the download-and-reupload fallback (`prepare_upload_item`/`send_batch_via_upload`, photo downscale handoff); `send/post_send.rs`: link-cache write, `KEEP_ALIVE` registry, `settle_task`, `post_send_actions` (the edit-before-forward prompt replies to the post's own first media message — the one whose caption it later edits — rather than the user's link message, so one message carrying several links opens several distinguishable prompts), `handle_task`/`dead_letter_notify` | | `crates/xmedia-bot/src/media_sender/{mod.rs,test_support.rs}` | `MediaSender` trait: the user-flow surface (`send_media_group`/`send_animation`/`copy_messages`/`send_message`/`answer_callback_query`/`edit_message_text`/`edit_message_caption`/`delete_message`/`send_chat_action`/`answer_inline_query`) implemented by teloxide `Bot` (per-chat rate-limited) and by a recording `MockSender` in tests. Admin/setup APIs (`get_chat`, `set_my_commands`, …) stay on the concrete `Bot`. `test_support.rs` (cfg(test)-only) holds the scripted `MockSender` and `fake_api` (the stand-in API the real-`Bot` tests drive) | | `crates/xmedia-bot/src/rate_limit.rs` | Two token buckets paced before sends reach the API so batch forwards don't trip flood control: one per chat (`CAPACITY = 20`, ~20 msg/min refill) and one bot-wide (`acquire_global`, 30/s — Telegram's per-bot ceiling, invisible to any per-chat bucket and only binding when a batch fans out over many chats). `prune_idle` drops the per-chat buckets that refilled while unheld | diff --git a/Cargo.lock b/Cargo.lock index 9963870..2294b95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2690,7 +2690,7 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "x-media" -version = "1.9.3" +version = "1.9.4" dependencies = [ "bytes", "dotenv", @@ -2710,7 +2710,7 @@ dependencies = [ [[package]] name = "xmedia-bot" -version = "1.9.3" +version = "1.9.4" dependencies = [ "bytes", "dotenv", diff --git a/README.en.md b/README.en.md index f863ca2..ccd357c 100644 --- a/README.en.md +++ b/README.en.md @@ -121,7 +121,7 @@ Telegram only accepts ports 443/80/88/8443. | `/help` | List all commands and usage (this command table) | | `/set_forward_channel ` | Set the forward channel: `@channel` or channel ID; media messages are forwarded to it automatically afterwards | | `/remove_forward_channel` | Remove the forward channel | -| `/edit_before_forward` | Toggle "edit before forward": when enabled, the bot posts a prompt after forwarding; replying to it edits the first forwarded message's caption (or tapping a template button applies one), then `↩️ Confirm` forwards and `🛑 Skip` drops this forward; the prompt states its expiry and is marked expired in place when it lapses (nothing is forwarded) | +| `/edit_before_forward` | Toggle "edit before forward": when enabled, the bot posts a prompt after forwarding; replying to it edits the first forwarded message's caption (or tapping a template button applies one), then `↩️ Confirm` forwards and `🛑 Skip` drops this forward; the prompt states its expiry and is marked expired in place when it lapses (nothing is forwarded); it replies to the post's own first media message — the one it edits — so several links in one message keep their prompts distinguishable | | `/set_template ` | Reply to a message containing `[]` to save it as a named template; `[]` is replaced by the original post link when forwarding (used with "edit before forward"). Names are limited to 55 UTF-8 bytes, bodies to 1024 escaped characters, and 50 templates per chat | | `/remove_template ` | Remove a template (names are listed by `/settings`; the prompt's keyboard shows at most 60) | | `/settings` | Show this chat's configuration: forward channel, edit-before-forward, per-site caption formats, saved templates | diff --git a/README.md b/README.md index 33f8508..72868ae 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Telegram 只接受 443/80/88/8443 端口。 | `/help` | 查看全部命令及用法(即本文档的命令表) | | `/set_forward_channel <频道>` | 设置转发频道,参数为 `@频道名` 或频道 ID;设置后发送的媒体消息会自动转发到该频道 | | `/remove_forward_channel` | 取消转发频道 | -| `/edit_before_forward` | 开关「转发前编辑」:开启后,转发成功后 bot 会发一条提示消息,回复它可修改第一条转发消息的 caption(或点击模板按钮套用模板),再点 `↩️ Confirm` 才会真正转发,`🛑 Skip` 放弃本次转发;提示消息写明过期时间,过期后原地标记为已过期且不会转发 | +| `/edit_before_forward` | 开关「转发前编辑」:开启后,转发成功后 bot 会发一条提示消息,回复它可修改第一条转发消息的 caption(或点击模板按钮套用模板),再点 `↩️ Confirm` 才会真正转发,`🛑 Skip` 放弃本次转发;提示消息写明过期时间,过期后原地标记为已过期且不会转发;提示消息引用本贴自己的第一条媒体消息(即它要改的那条),所以一条消息里发多个链接时各自对应、不会混淆 | | `/set_template <名称>` | 回复一条含 `[]` 的消息,将其保存为命名模板;转发时 `[]` 会被替换为原帖链接(配合「转发前编辑」使用)。模板名称最长 55 个 UTF-8 字节、正文最长 1024 个转义后字符,每聊天最多 50 个模板 | | `/remove_template <名称>` | 删除某个模板(名称见 `/settings`;提示消息的模板按钮最多显示 60 个) | | `/settings` | 查看本聊天配置:转发频道、转发前编辑开关、各站点 caption 格式、模板列表 | diff --git a/crates/x-media/Cargo.toml b/crates/x-media/Cargo.toml index ecca9f8..74b1d1d 100644 --- a/crates/x-media/Cargo.toml +++ b/crates/x-media/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "x-media" -version = "1.9.3" +version = "1.9.4" edition = "2024" [dependencies] diff --git a/crates/xmedia-bot/Cargo.toml b/crates/xmedia-bot/Cargo.toml index c61af79..6a58280 100644 --- a/crates/xmedia-bot/Cargo.toml +++ b/crates/xmedia-bot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xmedia-bot" -version = "1.9.3" +version = "1.9.4" edition = "2024" [dependencies]