Skip to content

fix: make new-post detection reliable and guard the manual trigger - #7

Merged
ofa1 merged 1 commit into
mainfrom
claude/repo-improvement-areas-2j6fgy
Aug 23, 2026
Merged

fix: make new-post detection reliable and guard the manual trigger#7
ofa1 merged 1 commit into
mainfrom
claude/repo-improvement-areas-2j6fgy

Conversation

@ofa1

@ofa1 ofa1 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

The cron loop could re-send the same audio, or miss a new one, and the HTTP entrypoint let anyone post to the chat. This fixes those four bugs, adds tests around them, and adds CI.

Bugs fixed

1. KV read failures caused repeated sends. getCountFromKV caught its own errors and returned 0. The handler read that as "nothing has ever been sent", so wordpressCount > 0 was true and the latest audio was re-posted on every 10-minute tick until KV recovered. Reads now propagate their errors, and the handler skips the cycle instead of sending.

2. KV write failures were silent. updateKVCount returned a Response with status 500 rather than throwing; the handler logged wasSuccessful = 'fail' and returned normally, so the next tick re-sent the same post. setLastSentAt throws, and the failure reaches the notifications chat.

3. Post counts were the wrong signal. Comparing the category's post count against KV missed the case where a post is deleted and another added — the count is unchanged, so the new reminder was never sent. The worker now stores the publish time of the last post it sent (fr:last-sent-at) and sends only when the latest post is strictly newer. Comparing times also means a deletion cannot make an older post look new, which an id comparison would have allowed. getCountFromWordpress is no longer needed, which also drops one API call per tick.

4. The manual trigger was unauthenticated. fetchHandler sent to the Telegram chat on any request, any method, any path — anyone who learned the workers.dev URL could spam the chat and pull a full audio download through the worker. It now requires POST and a TRIGGER_SECRET bearer token, compared without leaking where it diverges, and fails closed (503) when the secret is unset. It also records the timestamp on success, so a manual send is not repeated by the next cron run.

Also in this change

  • Errors were reported twice — send() notified and rethrew, and both callers notified again. Notification now happens once, at the top-level handlers.
  • Audio larger than Telegram's 50 MB bot limit is rejected before the whole file is buffered into memory, rather than failing opaquely mid-upload.
  • The two near-identical sendMessage bodies and three errorData as any casts collapse into shared helpers with a typed error shape.
  • Dead code removed: cleanMp3Filename and sendTelegramMessage (unused in src/), plus getCountFromWordpress.
  • Docs: README.md now covers secrets, deploy, local dev and the trigger; .env.example says these are Wrangler secrets rather than implying dotenv; the src/index.ts comment no longer references wrangler.toml.

Tests and CI

38 tests pass (was 11). New coverage: the send/skip/seed decisions in scheduledHandler including the KV-failure and failed-send paths, the trigger's auth and method checks, the KV round-trip and corrupt-value handling, and the date parsing.

.github/workflows/ci.yml runs typecheck and tests on pushes to main and on PRs, plus an npm run typecheck script. tsc --noEmit did not pass before this change: moduleResolution is now "bundler" so the vitest config's types resolve, and tests/env.d.ts types the test-only env bindings.

Deploy notes

  • KV key changed from fr (a count) to fr:last-sent-at (a timestamp). The first run after deploy finds the new key empty and seeds it without sending, so no reminder is re-posted. The old fr key is unused and can be deleted.
  • New secret: wrangler secret put TRIGGER_SECRET. Until it is set, the manual trigger returns 503; the cron path is unaffected.

What I did not change

  • The audio is still downloaded into the worker rather than handed to Telegram as a URL. Passing the URL would avoid the download entirely but loses the music-metadata duration/performer, so that tradeoff is yours to make.
  • No retry/backoff on outbound fetches. The 10-minute cron absorbs most transient failures; the manual trigger does not.

Generated by Claude Code

The cron loop could re-send the same audio, or miss a new one, and the HTTP
entrypoint let anyone post to the chat. Four related fixes:

- KV read failures no longer default to 0. getCountFromKV swallowed errors and
  returned 0, which the handler read as "nothing has ever been sent" and used to
  re-send the latest audio every 10 minutes until KV recovered. Reads and writes
  now propagate their errors.
- KV write failures are no longer silent. updateKVCount returned a Response with
  status 500 that the handler logged and ignored, so the next tick re-sent the
  same post. setLastSentAt throws instead.
- Detection compares publish times rather than post counts. A deleted post plus a
  new one left the category count unchanged, so the new reminder was never sent.
  The worker now stores the publish time of the last post it sent under
  fr:last-sent-at and sends only when the latest post is strictly newer, which
  also keeps a deletion from making an older post look new. On an empty
  namespace it seeds the timestamp without sending.
- The manual trigger requires POST and a TRIGGER_SECRET bearer token. It
  previously sent to the chat on any request to any path, and now also records
  the timestamp so a manual send is not repeated by the next cron run.

Also: errors are reported once instead of twice (send() and its callers both
notified), audio larger than Telegram's 50 MB bot limit is rejected before it is
buffered into memory, the duplicated sendMessage bodies and `as any` error casts
are collapsed into shared helpers, and the unused cleanMp3Filename,
sendTelegramMessage and getCountFromWordpress are removed.

Adds a GitHub Actions workflow running typecheck and tests, an npm run typecheck
script, and tests covering the send/skip/seed decisions, the trigger's auth, the
KV round-trip and the date parsing. tsc --noEmit did not pass before this change;
moduleResolution is now "bundler" so the vitest config's types resolve, and
tests/env.d.ts types the test-only env bindings.

Note for deploy: the KV key changed from `fr` (a count) to `fr:last-sent-at` (a
timestamp). The first run after deploy seeds the new key without sending, so no
reminder is re-posted; the old `fr` key is unused and can be deleted. Set the new
TRIGGER_SECRET with `wrangler secret put TRIGGER_SECRET` — until it is set the
manual trigger returns 503 and the cron path is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JN4sR41DwRmboRfngdiTLE
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
mhmic-telegram-cron 8a9fba0 Aug 23 2026, 09:16 PM

@ofa1
ofa1 requested a review from omarmosid August 23, 2026 21:18
@ofa1
ofa1 merged commit 341659c into main Aug 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants