Commit Graph
48 Commits
Author SHA1 Message Date
tiennm99 d90af97d55 feat(commands): simplify portfolio command names 2026-07-01 10:53:52 +07:00
tiennm99 b86aca6b1d feat: rename wc schedule commands 2026-07-01 10:42:56 +07:00
tiennm99 bdb8757dbe feat: rename lol schedule module 2026-07-01 10:38:49 +07:00
tiennm99 6eeb7ef447 feat(stats): use queryable Mongo usage records 2026-07-01 10:22:03 +07:00
tiennm99 564b6635d4 refactor(modules): remove twentyq genai integration 2026-06-29 10:07:21 +07:00
tiennm99 7d31c6d199 refactor(config): remove price URL env overrides 2026-06-29 09:23:20 +07:00
tiennm99 c3bbbd01fe feat(server): register command menu on startup 2026-06-29 01:08:51 +07:00
tiennm99 5077ff0181 feat(wc): add world cup schedule module 2026-06-29 00:59:30 +07:00
tiennm99 95d4b2227d fix(deploynotify): preserve coolify source commit 2026-06-29 00:20:04 +07:00
tiennm99 6e35c222d3 refactor(deploy): remove retired aws support 2026-06-29 00:11:42 +07:00
tiennm99 64a837b086 feat(deploynotify): notify owner on every startup, no SHA dedup
Match the store-scraper-bot behaviour: DM the owner once per boot regardless of
commit, and report an unknown SHA rather than staying silent. Drop the store-
based dedup and the empty-SHA skip; resolveCommitSHA defaults to "unknown".
2026-06-28 23:25:07 +07:00
tiennm99 2f4b1b7504 fix(deploynotify): read commit SHA from Coolify SOURCE_COMMIT runtime env
Baking SOURCE_COMMIT as a Docker build arg never worked: Coolify exposes it
as a runtime env var, not a build arg, so gitSHA was always empty and the owner
DM was skipped. Read SOURCE_COMMIT from the container env at startup (falling
back to the ldflags-baked gitSHA for local builds), forward it via the compose
environment, and drop the dead build-arg baking.
2026-06-28 23:08:17 +07:00
tiennm99 22a3d23ed4 refactor(server): delete webhook once on startup, no retry
The Bot API GET reliably clears the webhook, so the retry loop added for the
empty-body POST is no longer needed. One best-effort call; log on failure.
2026-06-28 22:52:55 +07:00
tiennm99 341a0b7a1d fix(telegram): clear webhook via Bot API GET to avoid empty-body POST
go-telegram's DeleteWebhook posts an empty multipart form (DropPendingUpdates
is omitempty); some networks answer that bodyless POST with an empty response,
failing to parse and leaving the webhook active so getUpdates 409s forever.
Replace it with a plain GET /deleteWebhook, which sends no body and parses the
JSON reliably. Keeps pending updates for a lossless cutover.
2026-06-28 22:41:49 +07:00
tiennm99 d84c7b619c fix(server): retry webhook clear on startup past transient empty-body error
Telegram's edge can return an empty body on the first request after a cold
container start, failing the single best-effort DeleteWebhook with 'unexpected
end of JSON input' and leaving a stale webhook that 409s getUpdates. Retry up
to 5 times with a 1s backoff so long polling is reliably unblocked.
2026-06-28 22:33:34 +07:00
tiennm99 7895630973 fix(server): clear Telegram webhook at startup before owner DM
Move DeleteWebhook ahead of the deploynotify owner DM and polling so the
webhook is cleared first thing once the bot is ready, instead of just before
getUpdates. DropPendingUpdates=false keeps the lossless cutover.
2026-06-28 22:18:28 +07:00
tiennm99 5bfee58964 refactor(server): drop HTTP /cron route; run crons in-process only
The in-process scheduler is the sole cron trigger on self-host, so the
/cron/{name} HTTP endpoint and its CRON_SHARED_SECRET are dead surface.
Reduce the HTTP server to GET / (health) and remove the cron secret config,
SSM binding, and now-unused timeout.
2026-06-28 22:15:04 +07:00
tiennm99 3e8d6bf2a7 refactor(storage): replace KVStore with generic typed DocStore[T]
Delete the byte-oriented KVStore/VersionedStore abstraction and the
DynamoDB/memory KV backends. Add a generic typed store (DocStore[T] with
Provider/Collection/Typed) persisting each value as a flattened native
Mongo document (storedDoc[T] via bson inline) — no value envelope.

- MongoDB is the only runtime backend; memory kept for tests/local.
- All modules + deploynotify use typed stores; persisted structs carry
  bson tags == json names (incl. nested lolschedule/wordle types).
- lolschedule wraps its array/scalar values in named structs.
- migrate-dynamo-to-mongo writes the flattened shape via Typed[bson.M]
  with wrap rules; Scan/--dry-run/--verify retained.

Verified: go vet/build clean; full go test green hermetically and
in-container vs real Mongo 7 + DynamoDB Local (storage integration +
migrator e2e).
2026-06-28 18:02:11 +07:00
tiennm99 f383533b07 refactor(storage): remove legacy firestore backend
Delete Firestore provider implementation and tests (5 files). Extract shared
key helpers to new internal/storage/keys.go module. Remove Firestore case
from server bootstrap and storage factory. Drop cloud.google.com/go/firestore
and related dependencies from go.mod. Remove firestore-emulator test targets
from Makefile. Update README storage section to reflect MongoDB-only backend.
2026-06-28 12:52:40 +07:00
tiennm99 884957cce4 feat(server): wire MongoDB + cron + polling; rename owner/admin envs
Wire MongoDB client and cron scheduler into server initialization. Rename
TELEGRAM_OWNER to TELEGRAM_ADMIN_ID and SLACK_OWNER to SLACK_ADMIN_ID
for clarity. Enable long-polling as default with cron scheduler for self-hosted.
2026-06-28 09:58:20 +07:00
tiennm99 b37a8dcd6f feat(gold): integrate VNAppMob SJC gold price with auto-refresh API key 2026-06-15 10:49:19 +07:00
tiennm99 e6ad2891dd refactor(stock): rename trading module to stock
Rename the `trading` module to `stock` across code, config, and docs for
naming consistency with the sibling coin/gold modules.

- Module: internal/modules/trading -> internal/modules/stock (package,
  factory, registry key in cmd/server)
- Commands: trade_* -> stock_* (telegram-commands.json)
- Env/SAM params: TRADING_INCOME_EVENTS_* -> STOCK_INCOME_EVENTS_*
- ModulesCSV updated in template.yaml, samconfig.toml, deploy.yml
- Add cmd/migrate-stock-key: idempotent, non-destructive DynamoDB
  partition copy (pk=trading -> pk=stock) with dry-run default

Production DynamoDB data already migrated (8 keys: 2 portfolios + 6
symbol-cache rows); old trading partition retained as rollback snapshot.
2026-06-12 14:52:12 +07:00
tiennm99 3cb176d5b2 feat(coin): add crypto paper trading 2026-06-12 10:47:02 +07:00
tiennm99 0a795b1436 feat(gold): add compare-and-swap portfolio updates with multi-backend support
Implement atomic UpdatePortfolio with retry pattern for concurrent-safe updates.
Add CAS support to DynamoDB, Firestore, Memory, and prefix stores.
2026-06-11 21:12:58 +07:00
tiennm99 2304657cb6 feat(gold): add gold paper trading module
Opt-in module for gold paper trading with VND currency.
Commands: /gold_topup, /gold_buy, /gold_sell, /gold_stats.
Pricing: spot XAU USD converted to VND per luong via GoldPrice.org + ExchangeRate-API.
Features: FX caching, dust normalization (1e-9), HTTPS-only URL validation,
per-user key locking, namespace isolation from trading module.
2026-06-11 17:13:02 +07:00
tiennm99 340172206a feat: add trade income events check 2026-06-05 14:21:38 +07:00
tiennm99 3f1f264e4a feat(stats): add command usage statistics module with persistence
Implement a new stats module for the Telegram bot that tracks per-command usage with persistent KV storage. The module provides a /stats command displaying usage sorted by popularity with a 4096-byte Telegram message cap. Includes CommandHook integration for post-dispatch tracking via background goroutine (2s bounded context), proper test coverage, and registry initialization. Updated server config with stats factory and reserved concurrent execution control to prevent TOCTOU issues.
2026-05-22 15:06:46 +07:00
tiennm99 a34320bc77 feat(deploynotify): DM owner once per new deployed git SHA
New internal/deploynotify package fires a single Telegram DM to
BOT_OWNER_ID on the first cold start that observes a new gitSHA.
Dedup via a DynamoDB KV record so repeat cold starts of the same
version stay silent. Send-then-write order means a failed Telegram
call doesn't permanently silence retries.

gitSHA is baked into the binary via `-ldflags "-X main.gitSHA=..."`
from Makefile; empty SHA (non-make builds) silently disables the
feature. No new env vars or IAM permissions.
2026-05-22 11:40:25 +07:00
tiennm99 a8ed67a0a3 refactor: audit-driven hygiene pass across modules and infra
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.
2026-05-16 13:35:00 +07:00
tiennm99 dbfee38232 Fix AWS Lambda deploy setup 2026-05-15 22:18:27 +07:00
tiennm99 f3b9891a54 refactor: rename module to miti99bot, canonicalize AWS deploy path
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.
2026-05-13 22:05:38 +07:00
tiennm99 f632c68956 refactor(modules): drop loldle variants, semantle, doantu and dead framework surface
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.
2026-05-11 16:02:06 +07:00
tiennm99 5697dd8293 feat(modules): add Bot to Deps + BuildOptions for cron handlers
- Extend module.Deps struct with optional Bot field
- Add Bot to registry.BuildOptions and thread through builders
- Pass bot instance from main.go into module factory options
- Enables cron handlers to send messages and access bot state
2026-05-10 03:06:01 +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 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 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 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 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 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 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 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 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 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 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 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