Commit Graph

36 Commits

Author SHA1 Message Date
tiennm99 ce99633e25 feat(modules): port new command modules + update registry
- 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
2026-05-10 02:29:58 +07:00
tiennm99 fb553cfe93 docs(aws): bootstrap + ops cheatsheets
- aws/README.md: one-time AWS account setup (account ID, IAM, OIDC)
- aws/iam-github-oidc-trust.json: GitHub OIDC trust policy template
- docs/deploy-aws.md: steady-state deployment operations guide
2026-05-10 02:29:52 +07:00
tiennm99 c07d764aa2 feat(deploy): AWS SAM template + Makefile + GitHub Actions
- AWS SAM CloudFormation template for Lambda + DynamoDB + EventBridge
- SAM config for us-east-1 deployment with guided parameters
- Unified Makefile: build-lambda, dynamodb-local, sam-* targets
- GitHub Actions: OIDC trust + SAM deploy on push to main
- CI job: add iac stage (sam validate)
- .gitignore: build/, bin/, .aws-sam/, samconfig.local.toml
2026-05-10 02:29:49 +07:00
tiennm99 070894444e feat(server): wire KV_PROVIDER env selection (memory|firestore|dynamodb)
- Add environment-based provider selection in main.go
- Support memory (test), Firestore (GCP), and DynamoDB (AWS) backends
2026-05-10 02:29:43 +07:00
tiennm99 91b27783cc feat(storage): add DynamoDB KV provider sibling to Firestore
- Implement DynamoDB client wrapper and KV provider
- Full CRUD test coverage for both local and AWS modes
- AWS SDK v2 dependency added to go.mod/go.sum
2026-05-10 02:29:39 +07:00
tiennm99 29a1bface0 docs(plans): research AWS vs GCP free tier + create AWS port plan
- Research reports comparing AWS/GCP free tiers and greenfield trade-offs
- New AWS port plan with 7 phases (infrastructure, deployment, integration)
- Mark GCP plan deploy phases as superseded by new AWS plan
2026-05-10 02:29:35 +07:00
tiennm99 3aab95daf0 feat(observability): request log middleware + in-memory metrics
Phase 11 partial of the go-port-cloud-run plan. Code-side
observability hooks ready ahead of Phase 01 GCP rollout.

- internal/server/log_middleware.go: HTTP middleware that wraps the
  router and emits {msg:"req", method, path, status, ms} per
  request. statusRecorder defaults to 200 when the inner handler
  doesn't call WriteHeader (Go writes 200 implicitly on first body
  write). Wired into server.New so /, /webhook, /cron/* all log.
- internal/metrics/counters.go: in-memory Registry with
  IncCommand/IncError/IncAI. Atomic Int64 per name + RWMutex on the
  map; steady-state increments are mutex-free. Periodic Run flushes
  via the project logger every 60s and one final flush on ctx done.
  Empty flush is silent (no-noise default).
- Dispatcher instrumented: every command invocation calls
  metrics.IncCommand; every handler error calls
  metrics.IncError("handler-error"). Logger keeps the full error
  detail; counters keep the rate.
- cmd/server/main.go: go metrics.Run(rootCtx) so the flush loop
  cancels with SIGTERM and emits the trailing window before exit.

Test coverage: 12 new tests (7 metrics, 3 middleware, 2 default-
registry round-trip). go test -race -count=1 ./... clean
(20 packages); golangci-lint clean.

Soak / cold-start measurement / log-based metrics setup deferred to
post-deployment (Phase 01 prerequisite).
2026-05-09 17:30:15 +07:00
tiennm99 ffb2cb0844 ci: bump golangci-lint v2.2.2 -> v2.12.2 for Go 1.25 support
v2.2.2 was built with go1.24; .golangci.yml's 'go: 1.25' target
fails its config-load check. v2.12.x is built against go1.25.
2026-05-09 17:19:49 +07:00
tiennm99 74f8a65595 ci: bump golangci-lint-action to v7; drop firestore emulator
- golangci-lint-action@v6 doesn't support golangci-lint v2.x; v7 does.
  This was the only thing breaking CI; subsequent steps were skipped.
- Firestore emulator step removed for now — storage_test.go skips when
  FIRESTORE_EMULATOR_HOST is unset, and the gcloud emulator install
  adds 30-60s of CI time for tests not yet on the merge-gating path.
  Re-add when storage-layer changes need emulator coverage.
2026-05-09 17:18:36 +07:00
tiennm99 6fa01ba5f1 feat(modules): port lolschedule (5 commands; daily-push cron deferred)
Phase 6e (final sub-phase of port-plan Phase 06). LoL esports match
schedule via lolesports.com's persisted API.

- internal/modules/lolschedule:
  - api_client.go: HTTP client with cache-first lookup (120s fresh
    window, 60-min stale fallback). Cache record shape matches JS so
    cross-runtime KV migration round-trips.
  - parse_date.go: ICT-anchored date parser. Accepts dd-mm-yyyy,
    dd/mm/yyyy, ddmmyyyy; trailing month/year may be omitted (default
    to current ICT month/year). Rejects impossible dates (Apr 31, Feb
    29 in non-leap, etc.).
  - format.go: Today (grouped by league) and Week (grouped by league
    -> day) renderers. Major-league filter (LCK/LPL/LEC/LCS/Worlds/
    MSI/etc.) keeps replies under Telegram's 4096-char limit. All
    user-influenced strings HTML-escaped.
  - subscribers.go: Idempotent add/remove/list keyed by chat id.
  - handlers.go: 5 commands (`/lolschedule [date]`,
    `/lolschedule_today`, `/lolschedule_week`,
    `/lolschedule_subscribe`, `/lolschedule_unsubscribe`).
- 22 tests across api-client (cache hit / miss / stale fallback /
  hard fail / show filter / non-JSON), parse-date (full and
  short formats, defaults, rejections, ICT anchor), format (event
  line states, league ordering, week grouping, HTML escape, major
  filter), subscribers (idempotent add/remove), handlers (HTML
  reply, error path, subscribe/unsubscribe round-trip).

Daily-push cron deferred to Phase 09 (Cloud Scheduler). Subscribers
are still collected so the push lights up the moment the cron infra
lands. Deps doesn't currently expose a *bot.Bot reference; that is
the prerequisite that Phase 09 will solve.

go test -race -count=1 ./... clean (19 packages); golangci-lint clean.
2026-05-09 17:14:01 +07:00
tiennm99 4955797f2b feat(modules): port loldle-splash
Phase 6d of the go-port-cloud-run plan. Adds the fourth loldle
variant — guess the champion from a splash art (any skin, including
non-Default).

- internal/modules/loldlesplash: champions.go (embed splashes.json,
  10557-line DDragon-sourced pool with SplashChampion + Skin types),
  state.go (gameState gains a `skinId` field so the same splash
  shows across guesses; default 4 guesses — splash is harder than
  ability since non-Default skins are in rotation), handlers.go
  (sendPhoto path uses the DDragon CDN splash URL via
  models.InputFileString), loldlesplash.go (Module Factory).
- Reuses internal/modules/util/chathelper and internal/champname.
- 4 commands wired: loldle_splash (public), loldle_splash_giveup
  (public), loldle_splash_stats (public), loldle_splash_setmax
  (private).
- 13 tests: lookup (embed shape + DDragon URL prefix + Default skin
  invariant), state (skinId round-trip + JS-wire-format decode +
  default 4), handlers (sendPhoto with correct URL, win, unknown,
  giveup with skin label, stats, setmax owner + non-owner).

go test -race -count=1 ./... clean (18 packages); golangci-lint
clean.
2026-05-09 17:00:24 +07:00
tiennm99 dd4e86a5de feat(modules): port loldle-ability
Phase 6c of the go-port-cloud-run plan. Adds the third loldle variant
— guess the champion from a single ability icon (passive or Q/W/E/R).

- internal/modules/loldleability: champions.go (embed
  abilities.json, 5334-line DDragon-sourced pool with
  AbilityChampion + Ability types), state.go (gameState gains a
  `slot` field so the same icon shows across guesses in a round),
  render-free handlers.go (sendPhoto path uses
  models.InputFileString with the DDragon CDN URL directly — no
  binary upload), loldleability.go (Module Factory).
- Reuses internal/modules/util/chathelper and internal/champname
  (same shared layer the other variants use).
- 4 commands wired: loldle_ability (public), loldle_ability_giveup
  (public), loldle_ability_stats (public), loldle_ability_setmax
  (private).
- 14 tests: lookup (embed shape + DDragon URL prefix + slot
  coverage), state (slot round-trip + JS-wire-format decode +
  streak), handlers (sendPhoto with correct URL, win, unknown
  champion, duplicate, giveup with slot label, stats, setmax owner
  + non-owner).
- gocyclo cap nudged 20 -> 22 to accommodate handleAbility's
  pre-flight validation branch.

go test -race -count=1 ./... clean (17 packages); golangci-lint
clean.
2026-05-09 16:53:26 +07:00
tiennm99 53ce1113eb feat(modules): port loldle-quote
Phase 6b of the go-port-cloud-run plan. Same shape as loldle-emoji but
with quote-pool data and default 6 guesses (vs emoji's 5).

- internal/modules/loldlequote: champions.go (embed quotes.json),
  state.go (game/stats/config persistence), render.go (italic clue +
  guess list, HTML-escaped), handlers.go (handleQuote / handleGiveup /
  handleStats / handleSetMax), loldlequote.go (Module Factory).
- Reuses internal/modules/util/chathelper and internal/champname so
  the new module adds no helper duplication.
- 4 commands wired in cmd/server/main.go: loldle_quote (public),
  loldle_quote_giveup (public), loldle_quote_stats (public),
  loldle_quote_setmax (private).
- 17 tests: lookup (embed + redaction-marker check), render
  (escapes), state (round-trip + JS-wire-format decode + streak
  sequence), handlers (no-arg / win / unknown / duplicate / giveup /
  stats / setmax owner+nonowner).

go test -race -count=1 ./... clean across all 16 packages;
golangci-lint clean.
2026-05-09 16:45:51 +07:00
tiennm99 8ce754e2b0 chore(plans): mark fix-all-review-findings plan completed 2026-05-09 16:33:43 +07:00
tiennm99 84f660d9d9 chore(tooling): golangci-lint + govulncheck + defensive guards
Phase 6 of the 2026-05-09 review remediation plan. Bundle of small
hygiene fixes — none individually urgent but better folded together
than scattered across follow-ups.

- .golangci.yml: enable errcheck/govet/gosec/staticcheck/unused/
  ineffassign/gocyclo/misspell/revive. Tuned to the codebase style
  (no universal exported-doc requirement, gocyclo cap at 20 to
  accommodate handler dispatch). 0 issues across the tree.
- ci.yml: add golangci-lint job + govulncheck (informational).
- Defensive guards:
  - registry.go: Module.Name mismatch now errors at Build instead of
    silently overwriting (TestBuild_RejectsFactoryNameMismatch).
  - cmd/server/main.go: PORT env validated numerically + 0..65535.
  - firestore_provider.go: For() re-validates module name; invalid
    names return an invalidStore whose every op errors with
    ErrInvalidModuleName.
- Dead code removal:
  - wordle: gameTTLSeconds const + pickDaily/hashDJB2/todayUTC
    helpers + their tests deleted (pickDaily was unused;
    daily.go renamed pick_random.go).
- Dependency: golang.org/x/net v0.52.0 -> v0.54.0 (resolves
  GO-2026-4918 HTTP/2 infinite-loop CVE).
- Deferred from the original phase plan: Docker digest pinning
  (Dependabot handles), per-handler file splits (largest file 279 LOC;
  splits would churn for marginal gain).

go test -race -count=1 ./... clean (15 packages); golangci-lint run
clean (0 issues).
2026-05-09 16:33:21 +07:00
tiennm99 b3dea5fe21 test(handlers): integration tests + recording bot + emulator on CI
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.
2026-05-09 16:19:38 +07:00
tiennm99 6368bc80ce feat(log): structured slog.JSONHandler + 22-site rewire
Phase 4 of the 2026-05-09 review remediation plan.

- internal/log: thin facade over log/slog.NewJSONHandler writing to
  stdout. Cloud Run's Cloud Logging integration auto-parses level,
  time, msg fields. Honours LOG_LEVEL env (debug|info|warn|error).
  Re-exports Info/Warn/Error/Fatal/Debug/With ergonomics.
- Migrated all 22 stdlib log call sites: cmd/server/main.go (17),
  internal/server/router.go (2), internal/modules/dispatcher.go (1),
  internal/telegram/webhook.go (1), internal/modules/misc/misc.go (1).
  Format-string args replaced with structured key/value attrs.
- Closes log-injection class (J3 from security audit) — slog escapes
  newlines and quotes inside field values, so attacker-controlled
  strings cannot synthesise fake log records (test:
  TestNewlineEscaping_NoLogInjection).

go test -race -count=1 ./... clean across all 13 packages. Zero
stdlib log imports remain outside internal/log.
2026-05-09 16:01:00 +07:00
tiennm99 5c367399c5 refactor(modules): extract shared chathelper + champname packages
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.
2026-05-09 15:52:40 +07:00
tiennm99 9a3108a1c4 feat(server): high-priority hardening + critical blockers
Phase 1+2 of the 2026-05-09 review remediation plan:

- Go-version alignment (Dockerfile/go.mod) + 4 nil-deref guards + CI
  docker-build step (Phase 1, c89aa1c carried over).
- Env allowlist: secretEnvKeys denylist replaced; modules opt-in via
  RequiredEnv. Future API keys do not auto-leak.
- Visibility enforcement: dispatcher gates Private/Protected commands
  via BOT_OWNER_ID / ADMIN_USER_IDS; non-permitted callers are silently
  denied.
- Panic recovery in webhook handler; logs runtime/debug.Stack and
  returns 200 to prevent Telegram retry storm.
- Cron timeout reduced 5m -> 60s.
- MaxBytesError handled separately from generic decode errors so 413
  from MaxBytesReader is not shadowed by a 400.
- Emoji clue HTML-escaped defensively in loldle-emoji renderer.
- Tests added for dispatcher Auth.Permits + webhook panic recovery.
2026-05-09 15:52:15 +07:00
tiennm99 c89aa1c0dc chore(plans): mark Phase 6a partial; log code-review findings
Update phase status to reflect loldle-emoji port completion. Document
code-review findings in dedicated report, noting per-subject keylock
strategy and JS-wire-format decode verification as key validation gates.
2026-05-09 12:19:27 +07:00
tiennm99 d9cafdcb30 feat(modules): port loldle-emoji
Phase 6a of go-port-cloud-run; first of 5 sub-cooks for Phase 6 loldle
variants. Implements binary right/wrong scoring (no attribute compare).
Per-subject keylock and math/rand.Intn applied from the start, lessons
from prior phase reviews. JS-wire-format decode test added per
code-review concern F#1, locking the migration contract. Helpers
(normalize/subjectFor/argAfterCommand) duplicated from classic loldle;
extraction earmarked for 6b prep.
2026-05-09 12:19:22 +07:00
tiennm99 9e95db2851 refactor(modules): allow hyphens in module names
Relax module name regex to accept hyphens, preparing for hyphenated
loldle variants (loldle-emoji, loldle-quote, etc.) ported from upstream
JS sources. Storage prefix delimiter ':' remains rejected. Telegram
command names use separate stricter regex (commandNameRe) and are
unaffected.
2026-05-09 12:19:16 +07:00
tiennm99 998016f7f9 chore(plans): mark Phase 5 done; log review findings
Completed Phase 5c: loldle module ported with full comparison engine,
keylock extracted for module reuse, and winRate rounding aligned with JS
behavior. Code review flagged and fixed render-alignment golden tests.
Updated phase status and plan progress tracking.
2026-05-09 09:57:00 +07:00
tiennm99 a5ab68da95 feat(modules): port loldle classic + fix winRate truncation
Ported loldle game module with full classic-mode comparison engine:
4 commands (3 public + /loldle_setmax private), 7-attribute comparison
(gender/species/range_type/resource/regions/positions/release_date) with
exact/multi/year scoring, 172-champion dictionary, and sticker pools by
outcome.

Fixed winRate display discrepancy: JS uses Math.round but Go was using
int(...) truncation. Applied math.Round in both loldle and wordle
handlers. Rendered output now matches expected percentages (e.g. 67%
instead of 66%).

Includes comparison/lookup/flavor/state/render golden tests, keylock
fan-out tests, and strict render-alignment validation.
2026-05-09 09:56:54 +07:00
tiennm99 58861a7e3c refactor(modules): extract per-subject lock to internal/keylock
Extracted per-subject RWMutex primitive into dedicated internal/keylock
package to allow reuse across game modules. Wordle previously held local
subjectLocks; loldle module (Phase 5c) would duplicate this type without
the extraction. Placing keylock as a peer to storage/telegram (not nested
under modules/) matches its cross-module scope.
2026-05-09 09:56:47 +07:00
tiennm99 d8aa3ea36e build: claude-code project settings + ignore agent runtime state
Added .claude/settings.json with git push authorization.
Updated .gitignore to exclude .claude/agent-memory/ from tracking.
2026-05-09 09:35:32 +07:00
tiennm99 ae11f0eb53 chore(plans): mark Phase 5b done, log review findings
Updated phase-05-port-simple-modules.md with completion status and
linked code-reviewer report documenting the two bugs fixed during
implementation (defaultRNG race, Get→mutate→Put logical race).
Updated plan.md to reflect Phase 5b completion in roadmap.
2026-05-09 09:35:28 +07:00
tiennm99 6ee8118ad9 feat(modules): port wordle module + per-subject locking
Phase 5b of go-port-cloud-run plan. Port 14855-word dictionary
(89 KB, byte-identical to JS source) and four wordle commands
(/wordle, /wordle_new, /wordle_giveup, /wordle_stats).

KV wire-format parity: GameState/Stats JSON match JS shape;
*int64 LastResultAt for null-value compatibility. Two real bugs
caught and fixed: (1) defaultRNG data race in handlers — switched
to math/rand.Intn (mutex-protected package-level); (2) Get→mutate→Put
logical race in groups — added per-subject sync.Mutex map to serialize
access. TTL deferred (Firestore has no expirationTtl equiv — Phase 11 GC).
2026-05-09 09:35:23 +07:00
tiennm99 3736e80d11 chore(plans): mark Phase 5a partial, log review findings
Update phase-05 status to reflect partial completion: util and misc modules
ported with tests passing, code-review feedback incorporated. Add code-reviewer
report documenting nil-deref and KV wire-format fixes. Update plan.md to track
progress: Phase 5a done (4/4 todos), Phase 5b (wordle/loldle) pending.
2026-05-09 08:25:01 +07:00
tiennm99 0584b094d1 feat(modules): port util + misc; expose Registry to handlers
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.
2026-05-09 08:24:52 +07:00
tiennm99 747533abe8 chore(plans): mark Phase 04 done + record review findings
Updated phase-04-firestore-kv.md to reflect completion with all success
criteria validated. Updated plan.md to mark Phase 04 status as complete.
Recorded code-reviewer findings documenting security hardening, abstraction
quality, and emulator test coverage from Phase 04 implementation review.
2026-05-08 23:51:32 +07:00
tiennm99 28a9676690 feat(storage): Firestore KVStore + KVProvider abstraction
Phase 04 of go-port-cloud-run plan. Introduces KVProvider abstraction
with memory backend (via Prefixed wrapper) and Firestore backend (via
collection-per-module isolation). Backend selection gated by env vars:
GOOGLE_CLOUD_PROJECT or FIRESTORE_EMULATOR_HOST → Firestore, else memory.
Emulator-gated tests via `make test-emulator`. Security hardened: emulator
fallback project ID, prefix validation on List, length-in-bytes docs.
2026-05-08 23:51:24 +07:00
tiennm99 7f77b0a8a9 chore(plans): update phase 02-03 status and record code review findings
Phase 02 (repo bootstrap, partial) and Phase 03 (module framework)
completed and reviewed. Cloud Run deployment deferred to Phase 01.
Updates plan progress, phase completion notes, and code review report
documenting security hardening and test coverage decisions.
2026-05-08 23:27:21 +07:00
tiennm99 25a5f37d3d feat(server,modules): bootstrap server and module framework
Implements Phases 02 (partial) and 03 of the go-port-cloud-run plan.
Introduces module framework with per-module KV prefix isolation,
health check endpoint, request timeout protection, and comprehensive
test coverage. Cloud Run deployment deferred to Phase 01.

Security hardening: constant-time secret comparison, cron auth bridge,
and secrets stripped from dependency environment exports. Includes
Dockerfile, GitHub CI workflow (vet + race + build), and integration
tests for module lifecycle.
2026-05-08 23:27:12 +07:00
tiennm99 4ae676e851 chore: switch to Apache-2.0 license; import plans from upstream miti99bot 2026-05-08 22:48:34 +07:00
tiennm99 328ce45cac Initial commit 2026-05-08 22:44:45 +07:00