Daily Bluesky bot for AT Mot. Invites players and congratulates yesterday's solvers.
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: add scheduled worker entry, cron triggers, and docs

+106
+55
README.md
··· 1 + # atmot-bot 2 + 3 + Daily Bluesky bot for [AT Mot](https://atmot.herve.bzh). Posts once per language 4 + each day just after the UTC puzzle rollover: invites players to today's puzzle and 5 + congratulates yesterday's solvers (by count only). 6 + 7 + Runs as a stateless Cloudflare Worker with two Cron Triggers (EN 00:10 UTC, FR 8 + 00:11 UTC). Counts are read from the public Constellation backlink index; there is 9 + no database. The bot duplicates a small set of the app's frozen constants 10 + (`src/config.ts`) rather than importing the app. 11 + 12 + ## Develop 13 + 14 + ```sh 15 + npm install 16 + npm test # vitest — composer + facets + puzzle math 17 + npm run typecheck 18 + npm run dev # wrangler dev --test-scheduled (see Dry run below) 19 + ``` 20 + 21 + ## Dry run (no real post) 22 + 23 + Create a gitignored `.dev.vars`: 24 + 25 + ``` 26 + DRY_RUN = "1" 27 + ATMOT_BOT_IDENTIFIER = "atmot.herve.bzh" 28 + ATMOT_BOT_APP_PASSWORD = "dry-run-unused" 29 + ``` 30 + 31 + Then `npm run dev` and, in another shell: 32 + 33 + ```sh 34 + curl "http://localhost:8787/__scheduled?cron=10+0+*+*+*" # EN 35 + curl "http://localhost:8787/__scheduled?cron=11+0+*+*+*" # FR 36 + ``` 37 + 38 + The composed post is logged instead of published. 39 + 40 + ## Deploy 41 + 42 + The bot posts as **@atmot.herve.bzh** using a Bluesky **app password** (Settings → 43 + Privacy and security → App passwords — not the account password). 44 + 45 + ```sh 46 + npx wrangler login 47 + npx wrangler secret put ATMOT_BOT_IDENTIFIER # e.g. atmot.herve.bzh 48 + npx wrangler secret put ATMOT_BOT_APP_PASSWORD # the app password 49 + npm run deploy 50 + ``` 51 + 52 + The free Workers plan is sufficient: each language runs in its own scheduled 53 + invocation, so each gets the full 50-subrequest budget. Solver counting samples up 54 + to `SOLVER_SAMPLE_CAP` (20) records per language; beyond that the count is hedged 55 + (e.g. "20+"). On the paid plan, raise `SOLVER_SAMPLE_CAP` in `src/config.ts` to ~200.
+45
src/index.ts
··· 1 + import { puzzleNumberFor, type Lang } from './config.js'; 2 + import { yesterdayCounts } from './counts.js'; 3 + import { composePost, postMarker } from './compose.js'; 4 + import { createBotSession, alreadyPosted, publishPost } from './post.js'; 5 + 6 + export interface Env { 7 + ATMOT_BOT_IDENTIFIER: string; 8 + ATMOT_BOT_APP_PASSWORD: string; 9 + /** When "1", compose and log the post but do not authenticate or publish. */ 10 + DRY_RUN?: string; 11 + } 12 + 13 + /** Map the firing cron expression to a language (FR runs one minute after EN). */ 14 + function langForCron(cron: string): Lang { 15 + return cron.startsWith('11 ') ? 'fr' : 'en'; 16 + } 17 + 18 + export default { 19 + async scheduled(controller: ScheduledController, env: Env, _ctx: ExecutionContext): Promise<void> { 20 + const lang = langForCron(controller.cron); 21 + const todayN = puzzleNumberFor(); 22 + const yesterdayN = todayN - 1; 23 + 24 + try { 25 + const yesterday = yesterdayN >= 1 ? await yesterdayCounts(lang, yesterdayN) : null; 26 + const post = composePost({ lang, todayN, yesterday }); 27 + 28 + if (env.DRY_RUN === '1') { 29 + console.log(`[atmot-bot] DRY_RUN ${lang} #${todayN}:\n${post.text}`); 30 + return; 31 + } 32 + 33 + const session = await createBotSession(env.ATMOT_BOT_IDENTIFIER, env.ATMOT_BOT_APP_PASSWORD); 34 + if (await alreadyPosted(session, postMarker(lang, todayN))) { 35 + console.log(`[atmot-bot] ${lang} #${todayN} already posted; skipping`); 36 + return; 37 + } 38 + const uri = await publishPost(session, post); 39 + console.log(`[atmot-bot] posted ${lang} #${todayN}: ${uri}`); 40 + } catch (err) { 41 + console.error(`[atmot-bot] ${lang} #${todayN} failed:`, err); 42 + throw err; // surface the failure to Cloudflare's cron logs 43 + } 44 + }, 45 + };
+6
wrangler.toml
··· 1 + name = "atmot-bot" 2 + main = "src/index.ts" 3 + compatibility_date = "2026-06-25" 4 + 5 + [triggers] 6 + crons = ["10 0 * * *", "11 0 * * *"]