/wordle and /loldle now send the wordle and loldle BotFather games in an
unlimited mode: each player gets their own word or champion, saved after
every guess, with a New game button. Cards the bot did not record as
unlimited, including the daily cards, play the daily puzzle. The helper
commands keep working on the player's round.
/loldledaily mirrors /wordledaily: one champion per day at 07:00 ICT,
subscribe and unsubscribe, the morning recap and the live group results.
The daily and unlimited machinery is shared in util/guessgame, the old
in-chat wordle module is removed, and a one-time startup migration carries
each player's stats and unfinished round over without changing old data.
/wordledaily sends a BotFather game card whose page plays one shared word
per day, changing at 07:00 ICT. The server checks guesses, saves each
player's board after every guess and keeps NYT-style stats. A win is
reported with setGameScore, and in a group one live results message shows
everyone's colour grids.
/wordledaily_subscribe and /wordledaily_unsubscribe opt a chat or topic in
to the 07:00 push: yesterday's recap, opening with the answer and the
group's win streak, then today's card.
The HTML5 game plumbing that noitu used is now shared in util/htmlgame, and
the wordle word list and scoring live in wordle/wordlist.
BREAKING CHANGE: the Go module path is now github.com/tiennm99/tiennm99bot,
the default sticker pack is stickers_by_<bot username>, and the health body,
deploy DM, User-Agents and image names say tiennm99bot.
The subscriber store, the once-per-day claim, the terminal-error
classifier and the throttled fan-out with dead-chat pruning now live in
internal/modules/util/subscription so another module can offer an
opt-in daily push. Stored document shapes and keys are unchanged.
Correct comments that described the retired webhook transport, a removed
/cron route, the old KV store and wrapper types, and behaviour that has since
changed; drop plan and review labels; reword two startup log lines that
overstated or misnamed what they report.
/alias <name> saves a replied message under a name and /insert <name> sends it
back; /aliases lists every name and /unalias deletes one. Every Telegram format
is supported — sticker, photo, GIF, video, video note, audio, voice, document,
plain text — and each is kept as the file_id Telegram already issued, so nothing
is downloaded and an alias survives redeploys. The namespace is global and the
last assignment wins, matching the shared sticker pack; /unalias is open to
anyone for the same reason.
A saved name also works as its own command: /cheer rather than /insert cheer.
This needs two new seams in the module contract. Module.Fallback handles a
/command no module registered, and the dispatcher installs it after every
Command — the bot library returns the first matching handler, so code always
beats a name resolved at runtime, including an alias that shares a command
added in a later build. /alias refuses a name already in the registry for the
same reason, since such an alias would only reach /insert. An unknown command
stays silent: the fallback sees every unrecognised /foo in every chat, so
replying would make typos noisy and would confirm which names exist.
Module.Inline answers inline-mode queries — "@botname <prefix>" from any chat,
filtered by prefix and capped at Telegram's 50 results. Each result is a cached
inline type carrying the stored file_id, so the picker renders real previews
without an upload. Video notes are omitted because Telegram defines no
InlineQueryResultCachedVideoNote and substituting a plain video would change
what was saved. Inline mode must be enabled in BotFather before Telegram
delivers these updates.
Both slots are single-occupancy with conflict detection at Build. Auth.Permits
learns the inline sender so a gated inline handler would not deny everyone.
Build's command indexing and slot claiming move into addCommands/addSingletons,
keeping it under the project's cyclomatic cap.
Also restores the sticker module: /addsticker moves back out of util, which has
no store, into internal/modules/sticker as its only command.
/addsticker becomes a single stateless command in util, writing to one
env-configured set (STICKER_PACK_NAME, default miti99_by_miti99bot).
AddStickerToSet takes the set owner's user ID rather than the caller's, so
nothing is per-user any more: the sticker module's pack records, slug
reservations, pending deletes, per-user locks and its eight other commands are
removed with it.
The pack creates itself on first use. A positive STICKERSET_INVALID from the
add triggers createNewStickerSet owned by OWNER_ID, seeded with the sticker
that triggered it and titled with the slug half of the name; a name that is
occupied but unwritable is reported instead of taken over. The mandatory
"_by_<bot_username>" suffix is Telegram's own proof of authorship, so a
misconfigured pack name is refused offline before any API call. StickerSet
exposes no owner ID, so ownership is only provable when Telegram refuses.
Video, GIF, animation and video-note sources are transcoded to WEBM/VP9 with
ffmpeg: long edge scaled to exactly 512 in either direction, cut to 3s, capped
at 30fps, audio dropped, retried down a CRF ladder until under 256KB. Animated
and video stickers are copied by file_id with no conversion. Sticker format is
per-sticker since Bot API 7.2, so one pack holds all three.
ffmpeg cannot ship in distroless/static and Go has no VP9 encoder, so the
runtime base becomes alpine with apk add ffmpeg. The image grows from roughly
20MB to 213MB, and the transcode holds the single dispatcher worker — bounded
at 20s per encode and a 45s handler deadline for moving sources, against 10s
for stills.
A remote wheel render takes several seconds, during which /wheelofnames
looked unresponsive. Post "Spinning..." first, then let the result take
its place: the GIF replaces it (send-then-delete, since Telegram cannot
edit text into media) and any render or upload failure edits the same
message into the plain text winner. A rejected edit still falls back to a
fresh reply so the chat never stays stuck on "Spinning...".
No placeholder when no renderer is configured — the winner reply is
already immediate there and would only flash.
Adds SendText/EditText/DeleteMessage to chathelper; Reply now delegates
to SendText.
Stats handlers reused the single bounded update context for both upstream
price fetches and the final Telegram reply, while per-upstream HTTP timeouts
equalled the whole-handler budget. One slow upstream drained the deadline and
the reply failed with "context deadline exceeded" (observed on /stock_stats).
- add chathelper.FetchContext: fetches run under a child context that reserves
a tail of the deadline for the reply, which is sent on the original context
- cap kbs/coin/gold/vnappmob HTTP timeouts at 3s so one hung upstream cannot
consume the reply budget
- fetch held stock/coin prices concurrently (bounded) so total latency tracks
the slowest single fetch instead of their sum; per-symbol failures degrade to
a "no price" line and the summary still sends
The CF→AWS data migration (closed 2026-05-16) is long done and the
tooling isn't wired into any production path. Remove the one-shot binary,
its support package, and the migration runbook.
In live code, replace 'JS-parity' / 'same shape as JS' / 'cross-runtime
KV migration' comments with the real, stable reason for each behavior
(wire-format invariant, null-vs-zero distinction, CloudWatch alarm field
name, etc.). 24 files touched across lolschedule, loldle, wordle, twentyq,
trading, misc, util, server, metrics, ai, keylock.
- delete cmd/migrate_cf_data/
- delete internal/migration/
- delete docs/cf-to-aws-migration-runbook.md
Concurrency
- lolschedule: serialize subscriber Get→mutate→Put via state.subscribersMu;
the single-slot list was previously losing writes under concurrent
/lolschedule_subscribe.
- trading: PriceClient memoises its default *http.Client so /trade_stats
reuses TLS connections across held tickers.
Observability
- server/log_middleware: defer the req log line and recover panics so a
panicking cron handler still emits the structured req entry CloudWatch
filters on for 5xx alerting.
- server/router (cron): inner recover with cron-name context captures the
panicking job before the middleware's safety net does.
- telegram/webhook: rune-safe truncation in dispatch logs — Vietnamese,
Korean, and emoji previews no longer ship as garbled bytes.
- lolschedule/api_client: same rune-safe fix for error-body log truncation.
- telegram/webhook: gate the post-recover WriteHeader(200) so a panicking
handler that already touched w doesn't trigger superfluous-WriteHeader.
Correctness
- twentyq: clearGame error during solved-relaunch is logged instead of
silently swallowed (was a permanent deadlock vector on KV failure).
- misc /mstats: KV read failure replies "Could not load stats. Try again
later." to the user instead of returning into the dispatcher; matches the
pattern other modules use.
- migrate_cf_data trading-audit-dump: surface f.Close error so a truncated
JSONL never passes silently as a complete audit dump.
Operator ergonomics
- migrate_cf_data (all 4 subcommands): signal.NotifyContext for SIGINT /
SIGTERM. Ctrl-C mid-Scan now propagates cleanly instead of leaving a
half-converted DynamoDB table.
- ai/ratelimit: doc the Lambda-recycle memory bound to match keylock.Map
so a future reviewer doesn't re-flag the unbounded map.
I/O-changing (user-approved)
- lolschedule daily push auto-prunes subscribers whose Telegram error
matches a terminal marker (blocked / deactivated / chat gone). Transient
errors keep the chat on the list. Subscribe message updated to mention
the auto-cleanup.
- twentyq seed pool grown 50 → 178; repeat-collision threshold moves from
~9 plays to ~17 (birthday paradox).
- util /info flipped Public → Protected — chat/thread/sender IDs are no
longer enumerable by every group member.
- cmd/server WriteTimeout 6min → 75s (cron 60s + 15s slack). No-op on
Lambda; matters only for local non-Lambda runs.
- webhook + cron rejection paths drop response bodies (no fingerprintable
text for internet scanners hitting the public Function URL). Status
codes preserved for CloudWatch metrics; structured log lines carry the
rejection reason for operator triage.
Tests added: TestTruncateRunes, TestRunDailyPush_PrunesDeadSubscribers,
TestIsTerminalSendError, TestInfo_DeniedToNonOwner,
TestInfo_DeniedToChannelMessageNoFrom, plus owner-allowed counterparts.
Telegram routes outgoing messages with no message_thread_id to a forum
supergroup's General topic. Commands sent in a topic were being answered
in General. chathelper.Reply / ReplyHTML now take *models.Message and
forward both ChatID and MessageThreadID; help.go SendMessage and
loldle trySendSticker do the same. Adds regression tests asserting the
field is forwarded for forum topics and omitted for private/regular groups.
Rename:
- Go module github.com/tiennm99/miti99bot-go → github.com/tiennm99/miti99bot
- CloudFormation stack miti99bot-aws-port → miti99bot
- Drop "port", "Cloud Run", "GCP", "cutover", "Phase NN" framing from
active code and docs — project reads as canonical AWS-Lambda from now on.
AWS deploy guide + flow fix:
- New docs/deploy-aws-free-tier-guide.md — Ubuntu 24.04 ARM64 onboarding
with project-local venv (pip awscli + sam-cli), SSM secrets via read -s,
idempotent OIDC provider + role creation, $1 budget alarm.
- Drop sam build from the pipeline — provided.al2023 + makefile builder
expects a Makefile in CodeUri (build/lambda/, the output dir), so the
step always fails. sam deploy --template-file template.yaml now reads
the raw template and zips build/lambda/ directly.
- Rollback section rewritten — use continue-update-rollback /
cancel-update-stack / git-SHA redeploy. Drop the broken
--use-previous-template recipe.
- DynamoDB free-tier row corrected (on-demand is 2.5M read / 1M write
request units, not 25 RCU/WCU).
Updated:
- README.md fully rewritten (drops port/legacy framing, lists modules,
points new users at the free-tier guide).
- aws/README.md retitled "AWS account setup", phase numbers stripped.
- Makefile / .github/workflows/deploy.yml — sam deploy flow.
- samconfig.toml — stack_name = "miti99bot".
- Go comments — Cloud Run → Lambda, Cloud Scheduler → EventBridge
Scheduler, Cloud Logging → CloudWatch Logs.
- Struct field GCPProject → FirestoreProject (env GOOGLE_CLOUD_PROJECT
unchanged).
Plus advisory reports under plans/reports/ from the code-reviewer +
researcher passes that informed the fixes.
Verified: go vet ./..., go build ./..., go test ./... all green.
Removes six modules (loldle-ability/emoji/quote/splash, semantle, doantu)
and prunes the framework deps that were only there to serve them:
- ai.Embedder + Client.Embed + embeddingModel const (semantle only)
- Deps.Embedder + BuildOptions.Embedder
- Deps.Env + Build(env) param + ModuleEnv config field + PHOW2SIM allowlist (doantu only)
- internal/champname package (loldle now owns its lookup helpers directly)
- template.yaml: Phow2simAPIURL parameter + PHOW2SIM_API_URL Lambda env
Active catalog: util, misc, wordle, loldle, lolschedule, twentyq, trading.
go build / vet / test all pass.
- Add new modules: doantu, semantle, twentyq, ai (Gemini integration)
- Update module registry with new command registration
- Update tests and documentation for module system
- Update README with new module references
Phase 5 of the 2026-05-09 review remediation plan. Closes the
handler-layer test gap (5 modules at 0% coverage in the audit) and
ends the storage-package's CI t.Skip on Firestore emulator tests.
- internal/testutil: Update fixture builders (NewPrivateMessage,
NewGroupMessage, NewSupergroupMessage, NewChannelMessage) plus a
RecordingBot that wraps the real go-telegram/bot.Bot with an
httptest server. The bot library hits the test server instead of
Telegram; multipart form fields are captured per call. Tests assert
on Sent() / LastSent() / AssertSentText().
- Handler tests added: misc (4), util (7), wordle (10), loldle (9),
loldleemoji (8). Cover happy paths, error paths, auth gates,
group-vs-private subject keying, KV side effects.
- Coverage 44.7% -> 69.8% (verified via -coverprofile). All packages
now report coverage in CI output.
- CI: ci.yml installs cloud-firestore-emulator beta component and
starts it on localhost:8090 before go test. Sets
FIRESTORE_EMULATOR_HOST + GOOGLE_CLOUD_PROJECT env so the storage
package's emulator-gated tests execute instead of skipping.
go test -race -count=1 ./... clean across all 15 packages locally.
Phase 3 of the 2026-05-09 review remediation plan. Eliminates the
helper drift (subjectFor / argAfterCommand / nowMillis / reply /
replyHTML / winRate) that previously lived in 3-4 modules, plus the
loldle-specific normalize / findChampion / findByExactName.
- internal/modules/util/chathelper: SubjectFor, ArgAfterCommand,
NowMillis, Reply, ReplyHTML, WinRate. Single canonical SubjectFor
shape (group/supergroup -> chat ID, else user ID); WinRate uses
math.Round to match JS Math.round (the truncation drift caught in
Phase 5b/5c).
- internal/champname: Normalize + generic Find[T] / FindByExactName[T]
with name-extractor closure. Loldle and loldle-emoji both consume
via Champion / EmojiChampion.
Migrations: wordle, loldle, loldle-emoji, misc, util/info,
util/stickerid. Module-local lookup.go + normalize.go in loldle and
loldle-emoji deleted.
go test -race -count=1 ./... clean across all 12 packages. Net ~290
lines removed across handler files.
Phase 5a of go-port-cloud-run plan: port first 2 of 4 modules (wordle/loldle
deferred to later phase). Port util.go, info.go, help.go, stickerid.go and
misc.go with tests. /help renders registry view; /info exposes chat/thread/
sender ids; /stickerid (private) returns bot-scoped file_ids; /ping writes
last_ping KV ms-epoch JSON for byte-parity, /mstats reads it, /fortytwo is
easter egg.
Registry-pointer-in-Deps required for /help to access module registry—pointer
captured at factory time, stable post-Build. Static factory catalog moved from
modules pkg to cmd/server to break import cycle. Code-review fixes applied in
same session: /info nil-deref guard, KV wire-format parity.