Commit Graph
212 Commits
Author SHA1 Message Date
tiennm99 dbcce0e1b7 fix(gold): drop redundant embedded selectors in portfolio tests 2026-06-11 21:49:03 +07:00
tiennm99 9d680c58ad fix(storage): report missing CAS support as unsupported instead of conflict
- prefix.go: return errors.ErrUnsupported when inner store lacks CompareAndSwap, enabling fail-fast instead of infinite retries
- Adds comprehensive CAS semantics test coverage for all KV backends:
  * memory_kv_test.go: new tests for basic operations and CAS failures
  * prefix_test.go: tests for wrapped CAS errors and unsupported operations
  * dynamodb_kv_test.go, firestore_kv_test.go: CAS failure scenarios
- portfolio_test.go: test retry exhaustion, business-error short-circuit, concurrent updates, and fail-fast on unsupported CAS
2026-06-11 21:40:16 +07:00
tiennm99 7578438aeb 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 a68a766ba5 feat(gold): add /gold_price command showing spot price in USD and VND
Read-only price lookup. Displays XAU USD/oz, USD/VND rate, and VND/luong.
No arguments, no portfolio mutation, no keylock needed.
2026-06-11 18:13:39 +07:00
tiennm99 254bf47dc1 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 2bf3cb2eee fix: simplify trade income event fetcher 2026-06-05 14:26:57 +07:00
tiennm99 460ad740bc feat: add trade income events check 2026-06-05 14:21:38 +07:00
tiennm99 93db76d9a4 feat: register trade income commands 2026-06-05 13:58:22 +07:00
tiennm99 5d7074f2dd feat(trading): show remaining VND after buy/sell success 2026-06-05 08:48:45 +07:00
tiennm99 35f81c6f13 chore: drop CF→AWS migration tooling and stale JS-port references
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
2026-05-25 09:39:17 +07:00
tiennm99 87a8371dcb fix(lolschedule): route daily push to originating forum topic
Subscribers previously stored only chatID; the daily-push cron sent
without MessageThreadID so Telegram routed to General regardless of the
topic the user subscribed from.

- Storage shape: []Subscriber{ChatID, ThreadID} keyed by (ChatID, ThreadID)
  so the same chat can subscribe in multiple topics independently.
- Decoder accepts legacy []int64 rows (ThreadID=0); successful mutations
  rewrite the slot in the new shape, no manual migration needed.
- Cron forwards MessageThreadID on every send.
- Auto-prune classifies terminal errors: chat-wide kills (bot blocked,
  deactivated, kicked, deleted, group upgraded) wipe every entry for the
  ChatID; topic-only "have no rights to send" prunes just (ChatID, ThreadID).
2026-05-25 09:39:01 +07:00
tiennm99 b6f6728b41 feat(stats): add per-user analytics with /stats subcommands
Extend the stats module to track per-user counts in addition to the
existing per-command totals. CommandHook now receives the originating
*models.Update so the hook can attribute invocations to a user; only
the stats module consumes this hook today.

Schema (sort keys under pk="stats"):
  count:<cmd>            existing, per-command total
  user:<id>              new, per-user total with cached username
  pair:<cmd>:<id>        new, per (command, user) pair

Subcommands (all public):
  /stats                 top commands (unchanged)
  /stats users           top users overall
  /stats user <name>     top commands for that user
  /stats cmd <name>      top users of that command

When the sender has no Telegram username the per-user/pair writes are
skipped — the global per-command counter still increments. View helpers
fan out KV reads in parallel to stay inside the webhook deadline. The
existing read-modify-write race is unchanged; closing it would need
atomic UpdateItem ADD on the KV interface.
2026-05-22 18:01:12 +07:00
tiennm99 e4781ed256 fix(deploy): include stats in CI ModulesCSV override
SAM CLI's --parameter-overrides replaces samconfig.toml's overrides
rather than merging them, so every CI deploy reset the stack's
ModulesCSV to the prior value (which predated the stats module).
The live Lambda's MODULES env was missing stats, so /stats was
dispatched but never handled — the module factory was never called.
2026-05-22 17:27:26 +07:00
tiennm99 15d2e53270 fix(stats): fan out per-key GetItem to avoid handler deadline timeout
Sequential List + N GetItem made /stats latency scale with the number
of tracked commands. On a cold Lambda container with ~25 commands the
cumulative DynamoDB round-trips can exceed the 10s webhook handler
deadline; the trailing chathelper.Reply -> b.SendMessage then fails on
a cancelled ctx and the dispatcher only logs the error, leaving the
user with no reply at all.

Fan the per-key GetJSONs out into goroutines joined by sync.WaitGroup.
Wall-clock latency collapses to ~1 round-trip while preserving the
per-key error isolation (a single GetJSON failure still drops only its
own row). Storage interface unchanged; no race regression vs. the prior
per-item write pattern.
2026-05-22 16:36:57 +07:00
tiennm99 4ddaeb5536 fix(deploy): hardcode non-secret CFN params instead of sed-parsing samconfig
The sed-based extraction from samconfig.toml worked locally (BusyBox sed)
but failed in CI with GNU sed:
  sed: -e expression #1, char 40: invalid reference \1 on 's' command's RHS

After YAML literal and bash single-quote unescaping, the sed pattern's
parens lacked the BRE \(...\) escaping, so there was no capture group
and \1 was invalid. Rather than fight escape layers for a non-secret
value, just inline the IDs in the workflow.
2026-05-22 15:43:07 +07:00
tiennm99 03171db605 fix(deploy): pass BotOwnerID and AdminUserIDs from samconfig.toml in CI
CI's --parameter-overrides replaces (not merges with) samconfig.toml's
parameter_overrides, so BOT_OWNER_ID was empty in the deployed Lambda.
deploynotify silently skipped sending the post-deploy DM, and all
Protected/Private commands were denied.

Extract the values from samconfig.toml at deploy time so the file
remains the single source of truth for non-secret deploy params.

Also add 'stats' to samconfig.toml's ModulesCSV so local `sam deploy`
doesn't regress the stats module.
2026-05-22 15:39:42 +07:00
tiennm99 1a2dfc5717 fix(stats): unblock deploy and register /stats in Telegram command menu
- Remove ReservedConcurrentExecutions: 1 (account lacks unreserved
  concurrency headroom; deploy rolled back). Accept best-effort
  counter semantics as documented.
- Add /stats to aws/telegram-commands.json so it appears in the
  Telegram client's / autocomplete after the next deploy.
2026-05-22 15:30:28 +07:00
tiennm99 d5c88afc4f fix(dispatcher): place nolint:gosec on go statement, not inside goroutine 2026-05-22 15:14:07 +07:00
tiennm99 5717d73e5c fix(stats): resolve golangci-lint G118 and QF1001 violations 2026-05-22 15:10:26 +07:00
tiennm99 cb319626f3 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 3b8d266aca chore(plans): add deploy-notify-owner plan (completed)
Plan + 2 phase files for the deploynotify feature. Both phases
marked completed.
2026-05-22 11:40:34 +07:00
tiennm99 44c9c047b8 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 d264c1eed9 fix(lolschedule): paginate older pages to cover past days in week range
The default lolesports schedule page is anchored near "now", so a
calendar-aligned /lolschedule_week issued midweek was silently dropping
events from Mon–Wed: we only walked pages.newer. fetchSchedulePage now
also returns pages.older, and fetchEventsInRange walks older until the
earliest collected event is ≤ from, then walks newer until ≥ to. Page
budget bumped 3 → 8 to accommodate dense regular-season weeks.
2026-05-22 11:03:28 +07:00
tiennm99 a389cb2dfd fix(lolschedule): align /lolschedule_week to ICT calendar week (Mon–Sun)
Previously a rolling 7-day window from today; now snaps to the Monday
of the current ICT week so the reply matches what users mean by "this
week". Adds ictWeekStartOf helper plus week-boundary tests.
2026-05-22 10:36:53 +07:00
tiennm99 e9f184dbd0 chore(plans): remove completed/superseded plans and orphaned reports
Delete 7 plan dirs (6 implemented, 1 superseded) and 19 reports tied to
removed or non-existent plans. Active plans (aws-port, pre-deploy-wrapup,
cf-data-migration, trongtruonghop, iam-least-privilege, go-port-cloud-run)
and their referenced reports are preserved.
2026-05-18 17:56:59 +07:00
tiennm99 cb9abd2d50 fix(deploy): quote FlexibleTimeWindow.Mode as string (EarlyValidation)
YAML 1.1 implicit conversion: bare 'OFF' parses as boolean false.
AWS::Scheduler::Schedule.FlexibleTimeWindow.Mode requires the string
'OFF' or 'FLEXIBLE'. CFN's EarlyValidation hook rejects the boolean,
causing 'Failed to create changeset' on every deploy attempt.

Verified by manual create-change-set against the SAM-packaged
template: Mode: false -> FAILED; Mode: "OFF" -> CREATE_COMPLETE
with 4 changes (BotFunction modify, LolscheduleDailyPushSchedule add,
SchedulerExecutionRole policy modify, MonthlyBudget remove).
2026-05-18 17:26:19 +07:00
tiennm99 31f0459ddd docs(security): bootstrap README reflects scoped deploy policy (F1, F2)
Step 4 now uses put-role-policy with the committed JSON instead of
attaching 10x FullAccess managed policies.

New subsections:
- Updating the deploy policy: edit JSON, apply out-of-band, commit,
  with jq -S structural-diff drift check (AWS normalizes JSON
  server-side, so byte-diff false-positives).
- Trust policy invariants: procedure to add a sub claim safely, and
  documented rationale for why pull_request is excluded.

Section 7 bullet 3 (replace broad policies) marked done.

Plan: plans/260518-1019-iam-least-privilege/phase-05-update-bootstrap-docs.md
2026-05-18 16:57:25 +07:00
tiennm99 a7729b7db7 feat(security): scoped IAM inline policy + rollback script for github-deploy-miti99bot (F1)
Replaces the 10x *FullAccess managed policies with a single
stack-scoped inline policy (miti99bot-deploy) on the role.

Policy boundaries:
- All resource ARNs scoped to miti99bot* (covers future miti99bot-dev)
- iam:PassRole conditioned on iam:PassedToService = lambda + scheduler
- iam:UpdateAssumeRolePolicy excluded (no trust-rewrite escalation)
- iam:AttachRolePolicy excluded (SAM uses inline PutRolePolicy)
- Wildcards limited to actions with no resource-level support
  (sts:GetCallerIdentity, s3:ListAllMyBuckets,
  cloudformation:ListStacks, cloudformation:ValidateTemplate)

Rollback: aws/iam-rollback-fullaccess.sh re-attaches all 10
FullAccess policies with retry-on-throttle + final verification.

Apply via Phase 4 two-stage cutover (dual-attach trial then detach).
Policy is committed but NOT yet attached -- Phase 4 applies it.

Plan: plans/260518-1019-iam-least-privilege/phase-03-draft-custom-policy.md
Audit: plans/reports/code-reviewer-260518-1019-security-aws-infra.md
2026-05-18 16:57:15 +07:00
tiennm99 62a228c42b fix(security): narrow OIDC trust to main only (F2)
Drop refs/heads/dev and pull_request from the sub allowlist on
github-deploy-miti99bot trust policy. Only pushes to main can now
assume the deploy role.

dev was dormant (ci.yml has only contents:read; deploy.yml triggers
on push:main only). pull_request was dormant too (no workflow has
id-token:write under PR trigger). This closes the latent F2 path.

Plan: plans/260518-1019-iam-least-privilege/phase-01-narrow-oidc-trust-f2.md
Audit: plans/reports/code-reviewer-260518-1019-security-aws-infra.md
2026-05-18 16:57:04 +07:00
tiennm99 0a048e8f5a docs(security): audit reports + IAM least-privilege plan + project policy
Captures the 2026-05-18 security review session output:

- plans/reports/code-reviewer-260518-1019-security-aws-infra.md
- plans/reports/code-reviewer-260518-1019-security-go-app.md
- plans/reports/researcher-260518-1019-security-dependencies.md
- docs/deploy-aws-free-tier-guide.md (adds free-tier hard rule +
  accepted security trade-offs as project standards)

Plan for the two HIGH-severity findings (F1, F2) targeting
github-deploy-miti99bot OIDC role: plans/260518-1019-iam-least-privilege/.
Plan was red-team-reviewed (15 findings applied) and validate-interviewed
(4 decisions recorded). Zero unresolved contradictions. Implementation
not yet started; phase 1 is standalone and lowest risk.

Other audit findings (F3 CORS, F4 root handler, F5-F16) deferred to
future commits; rationale in audit report.
2026-05-18 16:50:38 +07:00
tiennm99 afa1b71a56 refactor(deploy): swap cron to Scheduler + synthetic HTTP event (free tier only)
Supersedes c726c0a (never deployed). EventBridge ApiDestination has no
free tier — $0.20/M, rounds to $0 at our 30 invokes/month but still a
billable line item, which violates the project's hard free-tier rule.

Replace 4 resources with 2:
  - drop: CronConnection, LolscheduleDailyPushApiDestination,
          EventBridgeInvokeRole, LolscheduleDailyPushRule
  - add:  SchedulerExecutionRole, LolscheduleDailyPushSchedule

Schedule fires cron(0 1 * * ? *) at Lambda directly (free tier:
14M invocations/mo). Target.Input carries a synthetic Function URL v2
event so Lambda Web Adapter proxies it to /cron/lolschedule_daily_push
unchanged. The X-Cron-Token header is interpolated from the NoEcho
CronSharedSecret parameter.

Accepted trade-off: token value is visible to anyone with
scheduler:GetSchedule on the schedule. IAM gates that; the project's
security policy treats secret-in-Scheduler-Input as acceptable.
2026-05-18 15:36:59 +07:00
tiennm99 539e5bbefa feat(deploy): wire lolschedule cron via EventBridge Rule + ApiDestination
CloudFormation's AWS::Scheduler::Schedule Target schema has no property
for HTTPS universal invocation (URL, method, headers) — confirmed
against AWS docs. Switch to the legacy EventBridge Rule path which
supports HTTP targets natively via ApiDestination:

- AWS::Events::Connection: API_KEY auth, presents X-Cron-Token header.
  ApiKeyValue stored in EventBridge service-linked secret on stack
  update (no per-invoke SSM fetch, AWS-managed secret fees).
- AWS::Events::ApiDestination: POST to ${FunctionUrl}cron/lolschedule_daily_push.
- AWS::Events::Rule: cron(0 1 * * ? *) — daily 01:00 UTC / 08:00 ICT.
  Targets ApiDestination with retry x2, 600s max age, DLQ to CronDLQ.
- EventBridgeInvokeRole replaces SchedulerExecutionRole (events.amazonaws.com
  principal, events:InvokeApiDestination scoped to this destination only).

NoEcho CronSharedSecret CFN parameter restored; GHA fetches the SSM
SecureString and passes via --parameter-overrides so the value never
appears in template source or stack events.

Free-tier preserved: 1 invocation/day, well under EventBridge Rules +
ApiDestinations free quotas.
2026-05-18 15:14:06 +07:00
tiennm99 e93a2750a2 revert(deploy): roll back EventBridge HTTPS schedule attempts
Reverts 585d996 + c70b9d0. Both CFN deploys failed at changeset
validation; prod stack was never mutated. Approach A
(AWS::Scheduler::Schedule with arn:aws:scheduler:::http-invoke target)
is unimplementable in pure CloudFormation — the Target schema has no
property for HTTP endpoint/method/headers, regardless of name.
Replacement landing in a follow-up commit.

Restored: plans/reports/brainstorm-260517-1411-eventbridge-schedule-fix.md
(useful design context even though Approach A invalidated).
2026-05-18 15:11:18 +07:00
tiennm99 b0f5539f57 fix(deploy): pass cron secret via CFN parameter, not ssm-secure resolve
CloudFormation rejects {{resolve:ssm-secure}} inside
AWS::Scheduler::Schedule HttpInvokeArgs.HeaderParameters (documented
property-allowlist limitation). Add a NoEcho CronSharedSecret parameter
and fetch the SSM SecureString in CI, then pass via --parameter-overrides.
Lambda runtime SSM-fetch flow unchanged.
2026-05-18 13:04:30 +07:00
tiennm99 3578f65ba6 feat(deploy): wire EventBridge schedule for lolschedule daily push
Adds AWS::Scheduler::Schedule resource invoking the Function URL via
HTTPS universal target (arn:aws:scheduler:::http-invoke) at 01:00 UTC
daily. Token sourced from SSM via {{resolve:ssm-secure}}; failures
retried twice and DLQ'd to existing CronDLQ. Closes the deferred
phase-05 of 260510-0234-pre-deploy-wrapup.
2026-05-18 11:46:50 +07:00
tiennm99 464dfd47b9 feat(misc): add /trongtruonghop disclaimer command
Vietnamese disclaimer one-liner. Interpolates a target name (default "VNG")
and the sender — @username when set, else tg://user?id link with the display
name. User-supplied text HTML-escaped; reply routed through ReplyHTML to keep
forum-topic threading.
2026-05-16 14:51:26 +07:00
tiennm99 145a7261f5 refactor(plans): code-review audit reports — modules + infra
Captures the two parallel code-reviewer reports that drove the
audit-driven hygiene pass in the previous commit. Kept for future
reviewers and as the trail of which findings were applied vs.
deferred as cosmetic.
2026-05-16 13:35:11 +07:00
tiennm99 d77b478b67 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 64dac77c2e fix(reply): forward message_thread_id so replies stay in the same forum topic
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.
2026-05-16 12:10:56 +07:00
tiennm99 53e2b22f12 ci: bump golangci-lint-action to v9 (last Node 20 straggler)
v9 is the first Node 24-native release and still accepts golangci-lint
>= v2.1.0, so the pinned v2.12.2 binary keeps working. Removes the
final Node 20 deprecation annotation from the CI run.
2026-05-16 11:56:54 +07:00
tiennm99 b3fda15937 ci: bump actions to Node 24-native versions
GitHub forces Node 20 actions to run on Node 24 starting 2026-06-02
and removes Node 20 entirely on 2026-09-16. Bump every action that
emitted the deprecation warning to the latest major that natively
declares runs.using: node24:

  actions/checkout                 v4 -> v6
  actions/setup-go                 v5 -> v6
  aws-actions/setup-sam            v2 -> v3
  aws-actions/configure-aws-credentials v4 -> v6

Verified action.yml runtime declarations (node24) for checkout v6
and setup-go v6. configure-aws-credentials v5 changed only invalid-
boolean-input handling, which we don't pass. setup-sam v3 is solely
the Node bump. golangci-lint-action stays at v7 (already Node 24,
never appeared in the deprecation warning).
2026-05-16 11:53:01 +07:00
tiennm99 96c0083772 fix(dispatcher): match /cmd@botname so commands work in groups
The go-telegram/bot v1.20.0 MatchTypeCommand does byte-exact equality
on the full bot_command entity, which in groups is "cmd@botname"
(length 15 for /help@miti99bot) — never equal to the registered name
"cmd". All 27 commands silently missed when used in group chats.

Swap RegisterHandler(..., MatchTypeCommand, ...) for a local matchFunc
that strips the @suffix before comparing. Telegram only routes
/cmd@otherbot to otherbot, so the suffix is safe to drop
unconditionally.

Add a structured `dispatch` log line in the webhook (update_id,
chat_id, chat_type, text preview) so the next silent-drop symptom
shows up in CloudWatch without code archaeology — the existing
[TGBOT] [UPDATE] line only prints struct pointers.

12 new TestMatchCommand sub-tests cover DM, group, mid-text, wrong
command, non-command entity, and bounds edge cases.
2026-05-16 11:36:11 +07:00
tiennm99 0dd53a0a69 fix(migration): satisfy errcheck on Close + Fprint* in migration toolchain
Pre-existing 9 errcheck violations introduced by d67517e blocked
golangci-lint on every CI run after the migration toolchain landed.

- Wrap defer Close() in anonymous func to match house style
  (cf. internal/modules/lolschedule/api_client.go:162).
- Mark Fprint*/Fprintln to io.Writer with _, _ = (best-effort writes
  to os.Stdout / bytes.Buffer; errors not actionable for callers).

No behavior change. go vet, golangci-lint, go test ./... all clean
locally.
2026-05-16 11:02:15 +07:00
tiennm99 91901a3ba3 ci(deploy): auto-register Telegram webhook + commands after SAM deploy
Append two steps to .github/workflows/deploy.yml that POST setWebhook
and setMyCommands against the freshly-deployed Function URL, reading
credentials from SSM. Mirrors `make telegram-setup` but inlined to
avoid the Makefile's --profile admin assumption.

Token and webhook-secret are masked via ::add-mask:: before any echo.
Jobs fail loudly on Telegram API errors via `jq -e .ok`.

Mark the manual setWebhook snippets in docs/deploy-aws.md and
docs/deploy-aws-free-tier-guide.md as break-glass.
2026-05-16 10:55:43 +07:00
tiennm99 fc74474c83 docs(plans): close cf→aws phases 01-02 and amend locked decisions
- phase-01: mark complete; record live D1+KV inventory outcome (9 durable
  KV keys, trading source is KV not D1, retired modules skipped)
- phase-02: mark complete; toolchain verified end-to-end against prod CF
- phase-03: rewrite to flat KV copy (D1-transform branch dropped)
- plan.md: amend staging-only decision to permit pre-cutover bulk import
  against the live table while it has no AWS-served writes
- runbook: lock matrix and trading source, document Phase 02 commands
2026-05-16 10:33:46 +07:00
tiennm99 aeff00ac7e feat(migration): cf→aws migration toolchain + DynamoDB value as String
Operator-run tooling that moves durable Cloudflare KV data into the live
AWS DynamoDB table, plus a runtime swap of the `value` attribute from
Binary to String so payloads are human-readable in the AWS console.

- cmd/migrate_cf_data: subcommands inventory, kv-import (idempotent via
  attribute_not_exists), trading-audit-dump, convert-value-to-string
- internal/migration: policy allowlist, CF KV+D1 REST clients, DynamoDB
  writer, report formatter with per-prefix counts + tests
- internal/storage/dynamodb_kv.go: Put writes MemberS, Get reads MemberS;
  dropped empty-bytes sentinel (DynamoDB allows empty strings)
2026-05-16 10:33:35 +07:00
tiennm99 7bac818ac6 fix(plans): align cf-to-aws migration and cutover docs
Lock the Cloudflare-to-AWS migration matrix and runbook against the live DynamoDB runtime shape, and gate AWS cutover on verified migration parity instead of assuming a symmetric rollback.
2026-05-15 23:54:06 +07:00
tiennm99 09c8e563b2 fix(ci): pin deploy role ARN for OIDC auth
The deploy workflow no longer relies on a missing AWS_ACCOUNT_ID secret,
preventing invalid assume-role ARNs and matching the repo's documented AWS setup.
2026-05-15 22:36:15 +07:00
tiennm99 ecf92faf22 Fix AWS Lambda deploy setup 2026-05-15 22:18:27 +07:00
tiennm99 ac435aa090 fix(ci): clear sam lint + golangci-lint findings
- template.yaml: drop redundant DependsOn (W3005) — Ref in LoggingConfig
  already implies BotFunctionLogGroup ordering.
- dynamodb_kv.go: remove unused errIsTableMissing and orphaned errors import.
- trading/prices.go: wrap deferred resp.Body.Close to satisfy errcheck.
2026-05-14 09:57:12 +07:00