Compare commits

..
Author SHA1 Message Date
dependabot[bot] cd141ac2ef build(deps): bump docker/login-action from 4.7.0 to 4.8.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.7.0 to 4.8.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/81c6f93d4bc991e510b342a00b419b3bb4efc5c6...8aa5831fb8d48d8f76594c7ddde61ae58019f550)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-12 10:14:27 +00:00
YoursFunny 153b43c84d chore: bump version to 1.9.4 2026-10-31 18:56:27 +08:00
YoursFunny 132fdd2999 fix: reply the edit-before-forward prompt to the post's own media message
One user message carrying several links opens one prompt per post; every
prompt used to reply to that shared link message, so they were
indistinguishable. The prompt now replies to the first media message it
edits, falling back to the link message when there is none.
2026-10-31 18:53:29 +08:00
YoursFunny 8f6e78395b Merge pull request #9 from TheFunny/dependabot/cargo/parking_lot-0.13.0
build(deps): bump parking_lot from 0.12.5 to 0.13.0
2026-10-31 18:52:09 +08:00
YoursFunny 190cfd5e4b Merge pull request #13 from TheFunny/dependabot/cargo/rusqlite-0.41.0
build(deps): bump rusqlite from 0.40.2 to 0.41.0
2026-10-31 18:50:52 +08:00
dependabot[bot] f11ceb29e1 build(deps): bump rusqlite from 0.40.2 to 0.41.0
Bumps [rusqlite](https://github.com/rusqlite/rusqlite) from 0.40.2 to 0.41.0.
- [Release notes](https://github.com/rusqlite/rusqlite/releases)
- [Changelog](https://github.com/rusqlite/rusqlite/blob/master/Changelog.md)
- [Commits](https://github.com/rusqlite/rusqlite/compare/v0.40.2...v0.41.0)

---
updated-dependencies:
- dependency-name: rusqlite
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 10:48:52 +00:00
YoursFunny 1ec78b1c8d Merge pull request #11 from TheFunny/dependabot/cargo/html-escape-0.3.0
build(deps): bump html-escape from 0.2.15 to 0.3.0
2026-10-31 18:47:19 +08:00
YoursFunny d7bec116bf Merge pull request #12 from TheFunny/dependabot/cargo/tokio-1.54.1
build(deps): bump tokio from 1.53.3 to 1.54.1
2026-10-31 18:47:02 +08:00
YoursFunny 214b51de28 Merge pull request #10 from TheFunny/dependabot/github_actions/Swatinem/rust-cache-2.9.3
build(deps): bump Swatinem/rust-cache from 2.9.2 to 2.9.3
2026-10-31 18:46:56 +08:00
YoursFunny d132885fdc Merge pull request #8 from TheFunny/dependabot/github_actions/docker/login-action-4.7.0
build(deps): bump docker/login-action from 4.6.0 to 4.7.0
2026-10-31 18:46:51 +08:00
dependabot[bot] 0d35109f45 build(deps): bump tokio from 1.53.3 to 1.54.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.53.3 to 1.54.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.53.3...tokio-1.54.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.54.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-29 10:15:15 +00:00
dependabot[bot] 6b6f1f6437 build(deps): bump html-escape from 0.2.15 to 0.3.0
Bumps [html-escape](https://github.com/magiclen/html-escape) from 0.2.15 to 0.3.0.
- [Commits](https://github.com/magiclen/html-escape/compare/v0.2.15...v0.3.0)

---
updated-dependencies:
- dependency-name: html-escape
  dependency-version: 0.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-29 10:15:09 +00:00
dependabot[bot] 50c71f516b build(deps): bump Swatinem/rust-cache from 2.9.2 to 2.9.3
Bumps [Swatinem/rust-cache](https://github.com/swatinem/rust-cache) from 2.9.2 to 2.9.3.
- [Release notes](https://github.com/swatinem/rust-cache/releases)
- [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md)
- [Commits](https://github.com/swatinem/rust-cache/compare/6323deb102c322ba6fcbdcafc7e3dddab59af2b6...d1527c8f67ef9af357290475d4af4b0fab8f12e0)

---
updated-dependencies:
- dependency-name: Swatinem/rust-cache
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-29 10:15:04 +00:00
dependabot[bot] 8d46879fed build(deps): bump parking_lot from 0.12.5 to 0.13.0
Bumps [parking_lot](https://github.com/Amanieu/parking_lot) from 0.12.5 to 0.13.0.
- [Release notes](https://github.com/Amanieu/parking_lot/releases)
- [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Amanieu/parking_lot/compare/parking_lot-v0.12.5...parking_lot-v0.13.0)

---
updated-dependencies:
- dependency-name: parking_lot
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-29 10:15:03 +00:00
dependabot[bot] cd719eba3a build(deps): bump docker/login-action from 4.6.0 to 4.7.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/dbcb813823bdd20940b903addbd779551569679f...81c6f93d4bc991e510b342a00b419b3bb4efc5c6)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-29 10:14:55 +00:00
11 changed files with 56 additions and 36 deletions
+2 -2
View File
@@ -124,7 +124,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2
# `--locked` on every cargo invocation: the version bump edits
# Cargo.lock by hand (AGENTS.md), so a stale lock must fail here instead
# of being silently re-resolved — otherwise CI tests a different
@@ -162,7 +162,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2
# All #[ignore]d live tests, by the "live" name filter, in both crates:
# the bot crate has two of its own (repair refetch, text-only link)
# whose fetches are network-bound but whose sends go through MockSender.
+1 -1
View File
@@ -161,7 +161,7 @@ jobs:
-
name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
+2 -2
View File
@@ -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 |
Generated
+20 -21
View File
@@ -717,9 +717,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "html-escape"
version = "0.2.15"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9356095b4b41197bba32173600e1582792cda618f65d12f68e2e77d273413c5"
checksum = "491194d601b4fa9fac3c9571e014070a0ad1bc3392d1bc10419695196a8b3cc3"
[[package]]
name = "http"
@@ -1114,15 +1114,15 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.189"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "libsqlite3-sys"
version = "0.38.2"
version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8"
checksum = "14a5f01b87b92705d07b433c06f96cbc764637d81a0bffb5734ce17cf0f9a2b3"
dependencies = [
"cc",
"pkg-config",
@@ -1149,9 +1149,9 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "lock_api"
version = "0.4.14"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
checksum = "65d91b22da179613dcf4d6ec8349563f52a363f6849e03b00dd0a185ec4a59af"
dependencies = [
"scopeguard",
]
@@ -1271,9 +1271,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "parking_lot"
version = "0.12.5"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
checksum = "a9cf937ca4e1d0b7578784559063422fc39ae7c5a00f02471f2614d5486fd226"
dependencies = [
"lock_api",
"parking_lot_core",
@@ -1281,11 +1281,10 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.12"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
checksum = "9cfff060068cc18aa7815010fcaf80c68c7fb9e85fc7f5c0f9bf142c21d19826"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
@@ -1570,9 +1569,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.18"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
checksum = "580c875845b41ecb1564ad1ae4703044cef774145e3d7a94dc2b3fa7c97868fd"
dependencies = [
"bitflags 2.13.2",
]
@@ -1704,9 +1703,9 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.40.2"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3"
checksum = "9c026b4444579e8d754556c6c75d040d69c4a44283cdb648ab8a8d5965c2cfa8"
dependencies = [
"bitflags 2.13.2",
"fallible-iterator",
@@ -2217,9 +2216,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
[[package]]
name = "tokio"
version = "1.53.3"
version = "1.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
dependencies = [
"bytes",
"libc",
@@ -2691,7 +2690,7 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "x-media"
version = "1.9.3"
version = "1.9.4"
dependencies = [
"bytes",
"dotenv",
@@ -2711,7 +2710,7 @@ dependencies = [
[[package]]
name = "xmedia-bot"
version = "1.9.3"
version = "1.9.4"
dependencies = [
"bytes",
"dotenv",
+1 -1
View File
@@ -121,7 +121,7 @@ Telegram only accepts ports 443/80/88/8443.
| `/help` | List all commands and usage (this command table) |
| `/set_forward_channel <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 <name>` | 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 <name>` | 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 |
+1 -1
View File
@@ -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 格式、模板列表 |
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "x-media"
version = "1.9.3"
version = "1.9.4"
edition = "2024"
[dependencies]
@@ -8,7 +8,7 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus
serde = { version = "1", features = ["derive"] }
serde_json = "1"
regex = "1.12"
html-escape = "0.2"
html-escape = "0.3"
url = "2.5.2"
bytes = "1"
# ugoira frame zips are stored/deflate only; everything in zip's default
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "xmedia-bot"
version = "1.9.3"
version = "1.9.4"
edition = "2024"
[dependencies]
@@ -12,11 +12,11 @@ log = "0.4"
pretty_env_logger = "0.5"
dotenv = "0.15"
url = "2.5.2"
html-escape = "0.2"
rusqlite = { version = "0.40", features = ["bundled"] }
html-escape = "0.3"
rusqlite = { version = "0.41", features = ["bundled"] }
rand = "0.10"
tempfile = "3"
parking_lot = "0.12"
parking_lot = "0.13"
bytes = "1"
png = "0.18"
zune-jpeg = "0.5"
@@ -227,6 +227,8 @@ pub(crate) struct MockSender {
cursor: Mutex<usize>,
calls: Mutex<Vec<&'static str>>,
messages: Mutex<Vec<String>>,
/// The reply target of every `send_message`, in order.
message_replies: Mutex<Vec<Option<i64>>>,
captions: Mutex<Vec<String>>,
answers: Mutex<Vec<Option<String>>>,
/// `(chat, message, text)` of every text rewrite, in order.
@@ -281,6 +283,7 @@ impl MockSender {
cursor: Mutex::new(0),
calls: Mutex::new(Vec::new()),
messages: Mutex::new(Vec::new()),
message_replies: Mutex::new(Vec::new()),
captions: Mutex::new(Vec::new()),
answers: Mutex::new(Vec::new()),
edited_texts: Mutex::new(Vec::new()),
@@ -301,6 +304,12 @@ impl MockSender {
self.messages.lock().clone()
}
/// The reply target of every plain message sent, in order (`None` = no
/// reply).
pub(crate) fn message_replies(&self) -> Vec<Option<i64>> {
self.message_replies.lock().clone()
}
/// Captions passed to `edit_message_caption`, in order.
pub(crate) fn captions(&self) -> Vec<String> {
self.captions.lock().clone()
@@ -415,11 +424,14 @@ impl MediaSender for MockSender {
&self,
_chat_id: ChatId,
text: String,
_reply_to: Option<MessageId>,
reply_to: Option<MessageId>,
_reply_markup: Option<InlineKeyboardMarkup>,
) -> BoxFuture<'_, Result<i64, RequestError>> {
Box::pin(async move {
self.messages.lock().push(text);
self.message_replies
.lock()
.push(reply_to.map(|id| id.0 as i64));
match self.next("send_message") {
Outcome::MessageOk => Ok(MockSender::SENT_ID),
Outcome::MessageErr => Err(self.error()),
+3
View File
@@ -1646,6 +1646,9 @@ mod tests {
.expect("the prompt record must be stored");
assert_eq!(record.url, "https://x.com/u/status/1");
assert_eq!(record.forward_message_ids, vec![10, 11]);
// The prompt replies to the post's first media message, so several
// links in one user message still get distinguishable prompts.
assert_eq!(sender.message_replies(), vec![Some(10)]);
}
#[tokio::test]
+7 -1
View File
@@ -301,12 +301,18 @@ pub(crate) async fn post_send_actions(ctx: &AppContext<'_>, task: &Task, message
"\n({hidden} more templates not shown — /remove_template to prune.)"
));
}
// Reply to this post's own first media message rather than the user's
// link message: one message carrying several links opens several
// prompts, and replying them all to that one message made each
// indistinguishable from the others. The media message is the thing
// the prompt actually edits and forwards.
let prompt_reply_to = message_ids.first().copied().unwrap_or(reply_to);
let prompt = ctx
.sender
.send_message(
ChatId(chat_id),
text,
Some(MessageId(reply_to as i32)),
Some(MessageId(prompt_reply_to as i32)),
Some(keyboard),
)
.await;