Commit Graph
469 Commits
Author SHA1 Message Date
YoursFunny 9d40f3bc50 fix: bound template storage and callback data 2026-09-25 01:49:31 +08:00
YoursFunny 2045c3cea1 fix: keep photo memory permit until processing ends 2026-09-25 01:49:30 +08:00
YoursFunny 09656810fe fix: prune persisted expired prompts 2026-09-25 01:49:30 +08:00
YoursFunny 786bafca42 fix: account for photo processing peak memory 2026-09-25 01:49:30 +08:00
YoursFunny 18da6e97c2 fix: reject oversized local media before upload 2026-09-25 01:49:29 +08:00
YoursFunny 7b816d4153 fix: split long message forwards 2026-09-25 01:49:29 +08:00
YoursFunny d8b9a06453 fix: bound direct command fetches 2026-09-25 01:40:09 +08:00
YoursFunny 4e0f1b834e fix: restrict media download hosts 2026-09-25 01:40:09 +08:00
YoursFunny 21437d1843 fix: bound queue panic retries 2026-09-25 01:11:28 +08:00
YoursFunny a54029c19b fix: propagate startup repair database errors 2026-09-25 01:06:09 +08:00
YoursFunny c6120b7cc0 fix: make sqlite migrations atomic 2026-09-24 21:47:48 +08:00
YoursFunny 7b4e273533 fix: bound site JSON response bodies 2026-09-24 21:25:03 +08:00
YoursFunny 6a87583cc0 fix: honor bsky HLS retry-after 2026-09-24 21:01:36 +08:00
YoursFunny 1ba64e858d fix: enforce inline result limit centrally 2026-09-24 21:00:00 +08:00
YoursFunny 008d31b9bd test: cover inline answer edge cases 2026-09-24 20:57:08 +08:00
YoursFunny 66ec14662c style: simplify inline answer returns 2026-09-24 20:57:08 +08:00
YoursFunny 8ae7d9947d fix: honor media retry-after delay 2026-09-24 20:57:08 +08:00
YoursFunny 28e8fec68d fix: rate limit plain chat operations 2026-09-24 20:57:08 +08:00
YoursFunny b2b01721b2 fix: debounce inline generations and cap results 2026-09-24 20:40:27 +08:00
YoursFunny 85c3be5ef5 fix: always answer inline query 2026-09-24 20:33:23 +08:00
YoursFunny 5e7a87c0dd fix: send text posts as HTML 2026-09-24 20:29:04 +08:00
YoursFunny 6a2ba6a50a fix: handle empty media and prompt persistence 2026-09-24 19:51:50 +08:00
YoursFunny fb354c9434 test: handle chat state Result in callback 2026-09-24 19:46:23 +08:00
YoursFunny bc7f249c20 fix: filter local animation cache sources 2026-09-24 19:44:48 +08:00
YoursFunny 403b84996f test: cover ugoira expansion budgets 2026-09-24 19:42:50 +08:00
YoursFunny f08b187964 fix: drop unsafe upstream media entries 2026-09-24 19:39:46 +08:00
YoursFunny 8428468712 fix: surface chat state read failures 2026-09-24 19:37:29 +08:00
YoursFunny 412359e84c fix: do not cache local media paths 2026-09-24 18:52:18 +08:00
YoursFunny 438ced278f fix: align CI tags and webhook healthcheck 2026-09-24 18:49:20 +08:00
YoursFunny 704b14f05a fix: bound ffmpeg encoding processes 2026-09-24 18:46:09 +08:00
YoursFunny 1f9da14772 fix: bound ugoira archive expansion 2026-09-24 18:44:21 +08:00
YoursFunny 4694e60ee3 fix: reject upstream local media paths 2026-09-24 18:41:23 +08:00
YoursFunny 88dc0ccb9e fix: stop persistent queue before URL workers 2026-09-24 18:37:17 +08:00
YoursFunny b563b949bb fix: avoid overwriting chat state after read failure 2026-09-24 18:35:52 +08:00
YoursFunny e7855b02fe fix: fence queue dead-letter side effects 2026-09-24 18:33:21 +08:00
YoursFunny 5e265f2413 chore(log): align levels with the documented convention
Three places disagreed with AGENTS' own level rules: get_chat/get_chat_administrators failures during /set_channel printed at error even though the usual cause is a mistyped channel name the user fixes on the spot (error is reserved for dead letters and the like — now warn, same as other user-served degradations); the edit-before-forward callback arrival line logged at info while the sibling arrival lines for commands and inline queries log at debug (now debug, still carries the escaped payload); and the two upload-fallback lines logged at info while photo.rs's parallel degradations log at warn (now warn both — Telegram failing to fetch our media is a degradation that leaves the user served). AGENTS' convention sentence is now explicit about all three instead of the ambiguous '(fallback, retry enqueue, dead-letter is error)'.
2026-09-24 15:38:25 +08:00
YoursFunny 5060b760ba fix(fetch): cap one fetch at a 900-second total budget
The per-request CLIENT timeout and the per-chunk DOWNLOAD_IDLE_TIMEOUT both reset on progress, so neither bounded the fetch as a whole: a URL that keeps dripping (a trickle the idle timeout reads as life) could hold one of the eight FETCH_SLOTS for effectively ever, and the queue behind the slots is what then stalls. The attempts loop now runs inside tokio::time::timeout(900 s) — generous for a genuinely large ugoira zip on a honest slow link (minutes), fatal for a drip — and answers Transient with the cache key, so the retry and its jittered backoff take over. The permit is taken outside the timeout: the slot is released on either path. Audit low finding 'url_workers long tail' (process-wide total-deadline option).
2026-09-24 15:38:25 +08:00
YoursFunny 7019f34801 fix(send): refuse a file id at the upload fallback's door
download_to_temp guarded against a file id, but prepare_upload_item answered a non-http media_url first — and item_url maps FileId to the id itself, so the id was presented as a local *path* and died later with a confusing file-open error instead of a classification. The guard now sits at the entry, download_to_temp takes the already-narrowed URL (the inner guard deleted as dead), and the test pins the refusal as Permanent before any I/O. The file also carries item10's download-classification arm (RateLimited is retryable there too) — same file, landed here to keep each tree compiling.
2026-09-24 15:35:55 +08:00
YoursFunny d05450d88a fix(fetch): jitter the retry backoff and honor a429's Retry-After
Two gaps in fetch_with_attempts: the sleep was the bare 1 << attempt, so every worker that failed together (a source coming back, a shared proxy blip) also recovered on the same tick and re-stamped the source; and a429 arrived as an anonymous Transient, its Retry-After header — the one place a source tells you exactly how long it wants silence — dropped on the floor.

retry_wait(attempt, roll, err) is the pure decision: doubling base plus a random slice of itself ([base, 2x base)), floored at a named Retry-After. status_error now takes the response, reads the seconds-form header and returns the new FetchError::RateLimited { site, retry_after_secs } (HTTP-date parses to None and stays transient); the pure table moved to classify_status so tests still build it from bare status codes. The delay is capped at MAX_RETRY_AFTER_SECS = 60 — the header is server-supplied and must not park one of the eight fetch slots.

Everywhere a Transient meant 'retryable' the new variant joins: the Site trait default, pixiv's override (plus its ugoira-zip mapping), bsky's HLS second attempt, the download classifier, and the user-facing message arm. Tests pin the429 rows (with and without the header, cap included) and retry_wait's math; AGENTS' retries bullet and variant list follow.
2026-09-24 15:35:55 +08:00
YoursFunny 053ae0ec25 fix(send): charge the photo download window to the memory budget
A non-photo body charged the process-wide budget for as long as download_to_temp held it; a photo charged nothing until its prepare step, so the download itself — up to MAX_PHOTO_DOWNLOAD_BYTES per item, six items per batch, eight prep slots process-wide — was memory the MEMORY_BUDGET comment promised was bounded but was not (~6x32 MiB on top of the accounted512 MiB). Photos now reserve their own download cap for that window; the prepare step still charges header probe + decode buffer, the only overlap that is actually held in memory together. Net accounting stays within the declared budget instead of exceeding it whenever a batch of large photos downloads at once (audit: upload.rs photo window uncharged).
2026-09-24 15:29:17 +08:00
YoursFunny d1f2ae09b2 fix(rate-limit): charge plain messages and chat actions to the global bucket
send_media_group, send_animation and copy_messages paced themselves against both buckets; send_message and send_chat_action paced against neither. A dead-letter storm — or one error reply per failed item, plus the action refresh loop's repeated typing requests — could therefore burst past Telegram's per-bot30/s ceiling with only429s left to absorb it, exactly what the bot-wide bucket exists to prevent. Both now acquire one global token like their siblings (per-chat pacing for messages already happens at their call sites; chat actions are cheap and frequent, so only the global bucket applies to them).
2026-09-24 15:28:59 +08:00
YoursFunny 715d6a59b0 fix(photo): sum dimensions in u64 so a huge header cannot wrap
plan_photo, target_dims and both pipeline branches added the two u32 dimensions before comparing against PHOTO_MAX_DIMENSION_SUM: u32::MAX + 2 wrapped to 1 and read 'within limits' — the plan said AsIs, and an absurd-sized image would go to Telegram untouched (in debug builds the addition panics instead). The four production sums now widen to u64 first; the test pins the wrap case as TooLarge. PNG caps each dimension at 2^31-1, so a spec-valid file cannot reach this today — the raw header is parsed before any crate validation, which is exactly where a hostile file lands (audit: photo.rs u32 wrap).
2026-09-24 15:25:19 +08:00
YoursFunny ef8a1dae15 fix(handlers): honour the TTL when a caption-edit reply arrives
A button press on an edit-before-forward prompt already dropped records past EDIT_MESSAGE_TTL lazily (before the 300 s sweep clears them); a text reply to the same prompt did not — it rewrote the caption from a record the sweep was about to treat as dead, so the two paths disagreed about what 'expired' means. The reply path now applies the same rule: drop the stale record, return false, and let the message flow on as if no prompt existed. The test seeds a prompt90000 s old (past the TTL under any config a parallel test can hold) and pins all three effects: no edit consumed, no API call, record gone.
2026-09-24 15:23:02 +08:00
YoursFunny ac34759b02 fix(state): report a failed chat-state write to /set_format
set()/update() logged a failed DB write and returned nothing: the cache already held the new value, so /set_format answered 'Format set' for a change that vanishes on the next restart — a promise the retry queue's enqueue path is already forbidden from making. Both now return whether the write landed, and /set_format's two save paths (set and reset) answer 'in memory only … lost on restart' when it did not. The flag travels as the second tuple element; callers that only relay the value destructure it, callers that ignore it are untouched. A false cannot be forced in a test without a pool-failure seam, so the honest path is pinned by the code, not a fixture.
2026-09-24 15:21:05 +08:00
YoursFunny c420c95165 fix(docker): refuse a root or non-numeric LOCAL_USER_ID
LOCAL_USER_ID=0 passed every check and reached `setpriv --reuid=0`: the bot would run root while looking properly configured, and useradd -o accepted the duplicate uid without complaint. A non-numeric value failed later inside useradd behind `|| true`, which hid the real cause. The entrypoint now rejects both up front with a message naming the variable, and the id/uid expansions are quoted so a value with spaces cannot word-split into extra useradd arguments (audit SEC-007).

Verified by sourcing the entrypoint under a faked `id`: uid 0, 'abc' and '12 3' each exit 1 with the refusal line, while a non-root caller passes straight through to exec. AGENTS and .env.example state the constraint.
2026-09-24 15:12:22 +08:00
YoursFunny 4d6ec7924b fix(handlers): escape user-supplied text in log lines
A Telegram display name, callback payload or channel handle is attacker-controlled text, and it went straight into info/error lines: one embedded newline forged a second log entry, and control characters could hide inside a line (log injection, audit SEC-006). handlers::log_escape replaces newlines, carriage returns and other control characters with visible escapes — clean input borrows, so logging allocates nothing extra — and every site that prints such a value uses it: the callback arrival line, the set-forward-channel info line (name + handle) and both get_chat error lines. The test pins that no raw newline can survive; AGENTS' logging convention names the helper.
2026-09-24 15:02:31 +08:00
YoursFunny 9bae46eb9a chore(compose): pin acme-companion to its paired release
The companion was the file's only unpinned image: it runs with a rw docker.sock, so 'latest' let tomorrow's upstream change deploy itself on the next pull, with full visibility into this stack's containers and environment. 2.8.2 is the release paired with the already-pinned nginx-proxy 1.11.6 — the two projects cut releases a day apart (1.11.6 on 2026-07-16, 2.8.2 on 2026-07-17).
2026-09-24 15:00:45 +08:00
YoursFunny cb91913088 docs(agents): correct the CI, coverage and cert-mount claims
Five statements had drifted from the repo:

- the test-suite comment claimed no CI test step exists; ci.yml has run
  cargo test --workspace --locked all along (the audit's H5).
- the untested list still named config.rs as fully untested although three
  parsing tests pin its webhook truth table, TTL fallback and blank secrets.
- the commands.rs executor was said to need a real Bot; its three tests run
  through the scripted MockSender, and it joins the driven-through-mocks list.
- model.rs was called untested although every adapter fixture deserializes
  into those DTOs.
- the volume line claimed ./cert is mounted; the shipped compose mounts only
  ./data, and WEBHOOK_CERT (a Telegram-facing self-signed upload) needs the
  operator to add mount and env line themselves, which .env.example already
  says. The live-test census also gained site/download.rs (2) and the bot
  crate's repair.rs/urls.rs pair.
2026-09-24 14:54:07 +08:00
YoursFunny 2b6cb506c7 refactor(download): fold apply_media_headers into media_request
apply_media_headers had exactly one caller — media_request, the documented 'one choke point' every download already goes through — so the site-header walk sits inside that choke point now instead of one indirection past it, and the two doc comments merge into the one that describes both rules (guard + headers).
2026-09-24 04:37:54 +08:00
YoursFunny c49e2175a1 refactor(site): drop RenderData's url field, source_url already carries it
Every adapter built RenderData { url } from the exact value it then moved into Fetched.source_url — a copy of the same canonical URL held in two places, feeding one placeholder. caption_with reads {url} from self.source_url now and the field is gone from the struct (bsky's and twitter's halves of this change landed with their caption commits; their render builders no longer set it either).
2026-09-24 04:37:54 +08:00