- Send a random sticker from WIN/LOSE/GIVEUP pools before the text reply
(errors swallowed so a rotten file_id never blocks the message).
- Win message includes attempt-count flavor ("First try!" / "Sharp!" /
"Close call!" / "Phew — last one!") and elapsed solve time.
- Lose and giveup messages now also include the champion's title.
- Extract stickers.js and flavor.js so handlers.js stays at ~200 LoC.
Reply to any sticker with /stickerid to get its file_id and
file_unique_id. Collects IDs for hard-coded sticker pools in other
modules (upcoming loldle win/lose/giveup reactions).
Private visibility keeps the command out of /help and the Telegram
slash menu.
- Remove /loldle_new; finished rounds (solve/giveup/out-of-guesses)
immediately roll into a fresh round.
- Render guesses as an HTML <pre> monospace table with auto-widthed
label column and a 🎯 Name row (uppercase champion name).
- Year direction uses ⬆️ / ⬇️.
The TCBS `apipubaws.tcbs.com.vn` host returns HTTP 404/500 for every
request, so every ticker resolved as "Unknown stock ticker" and /trade_buy
was unusable. Switch price + symbol resolution to the KBS public endpoint
that vnstock currently defaults to (`kbbuddywts.kbsec.com.vn/iis-server/
investment/stocks/{TICKER}/data_day`). KBS needs no auth, returns JSON,
and is Worker-compatible.
- `prices.fetchStockPrice` now queries KBS with a 14-day lookback window
(covers weekends/holidays) and drops the TCBS-specific ×1000 scaling;
KBS returns real VND.
- `symbols.resolveSymbol` delegates to `fetchStockPrice` for existence
checks — empty `data_day` means unknown ticker.
- Update test fetch stubs to match the `kbsec` host and KBS response
shape (`{ symbol, data_day: [{ c }] }`).
Users expect to scan a single region's upcoming week rather than a daily
fixture board. Reverses the nesting: /lolschedule_week now lists each
major league as a top-level section, then italicised ICT date headers
with that league's matches beneath.
- handlers.js header now reflects fan-out to subscribers, not a single chat.
- README "Time zone" references the correct command names and gains a
Subscribers section; Files section lists subscribers.js.
- formatEventLine's showLeague option is dead in production (renderToday
and renderWeek always group under a league header), so drop it and the
test that covered only the option toggle.
Replaces the single LOLSCHEDULE_CHAT_ID env var with a KV-backed
subscriber list. New commands /lolschedule_subscribe and
/lolschedule_unsubscribe let each chat opt in/out. The cron now fans
out to every subscriber via Promise.allSettled so one blocked chat
cannot break the others.
- Commands renamed: /lol_today → /lolschedule_today, /lol_week → /lolschedule_week.
- Today view groups events under a league header per section.
- Week view nests leagues under each ICT day.
- LEAGUE_ORDER gives tier-1 tournaments priority (Worlds / MSI / First Stand,
then LCK / LPL / LEC / LCS, etc).
- New cron "0 1 * * *" (08:00 ICT) pushes today's major-league schedule to
LOLSCHEDULE_CHAT_ID via the Telegram Bot API. Skips cleanly when chat id
or token is missing, or when today has no major-league matches.
Leaguepedia's anonymous IP rate limit is too aggressive for a bot even
from CF Worker egress (~1–2 req/min), and authenticated Fandom tokens
don't lift it. Switching to the lolesports.com getSchedule endpoint —
the same data feed powering the official site — removes the limit and
provides richer fields: state (unstarted/inProgress/completed), per-team
result.gameWins and outcome, league metadata, bestOf strategy.
Handlers simplify back to cache-first (120 s fresh / 1 h stale fallback)
with no cron needed. Results are filtered to major leagues (LCK, LPL,
LEC, LCS, worlds, msi, first_stand, LCP, CBLOL, EMEA Masters) to keep
the week view under Telegram's 4096-char message limit.
Swaps the best-effort console.warn for JSON log lines emitted via
console.log so Workers Observability + wrangler tail surface the real
cause (HTTP status, API error info, or non-JSON body) when /lol_today
and /lol_week fall into the error branch.
New module exposing /lol_today and /lol_week commands, backed by the
Leaguepedia Cargo API (MatchSchedule table). Renders scores for
played/live matches and ICT times for scheduled ones. Caches range
queries in KV (60s today, 300s week) with stale-fallback on fetch error.
Code fixes:
- trading/handlers + stats-handler: guard ctx.from?.id to prevent
cross-user state corruption when channel posts or inline queries lack
a sender
- trading/prices + trading/symbols: encodeURIComponent on ticker before
interpolating into TCBS API URLs
- trading/stats-handler: parallelize per-stock price fetches with
Promise.allSettled so N-stock portfolios don't stack serial latency
- loldle/handlers: guard target champion lookup against champions.json
refresh drift — start a fresh round or fall back to the stored id
- wordle + loldle: explicitly initialize giveup:false in startFreshGame
for stable state shape
- wordle/lookup: fix stale JSDoc that claimed null return
- biome: ignore auto-generated champions.json / champions-data.js /
words-data.js
- Apply formatter to src/index.js, loldle/handlers.js imports, and
loldle/compare.test.js (previously red)
Docs refresh:
- README: 105+ tests -> 200+; wordle/loldle described as real modules
- architecture: module tree updated, test count 105 -> 200, runtime
~500ms -> ~2s, stub list narrowed to misc only
- codebase-summary: module table rewritten (wordle/loldle now Complete
with real command lists and KV schema); test coverage table updated
- loldle/README: full rewrite matching the current implementation
(was describing the original stub)
- New docs/development-roadmap.md tracking upcoming features
(daily-mode for wordle + loldle, crypto/gold/forex trading, shared
picker util, handler-level tests, coverage reporting, staging env)
Tests: 200/200 passing. Lint: clean.
Replaces the wordle stub with a full implementation mirroring the loldle
module layout: compare/lookup/daily/render/state/handlers/index split,
per-subject KV state, standard 6 guesses, two-pass duplicate-letter
marking.
Commands: /wordle, /wordle_new, /wordle_giveup, /wordle_stats.
Word list (14,855 entries) sourced from dracos's gist
(https://gist.github.com/dracos/dd0668f281e685bad51479e5acaadb93) and
bundled via scripts/build-wordle-data.js. Credits in module README and
generated file headers.
Dispatcher test updated for the new command count (12 → 13).
* build(deps): bump vite and vitest
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) to 8.0.8 and updates ancestor dependency [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). These dependencies need to be updated together.
Updates `vite` from 5.4.21 to 8.0.8
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)
Updates `vitest` from 2.1.9 to 4.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest)
---
updated-dependencies:
- dependency-name: vite
dependency-version: 8.0.8
dependency-type: indirect
- dependency-name: vitest
dependency-version: 4.1.4
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
* feat(loldle): share game state per-chat in groups
Groups and supergroups now share one daily puzzle + one stats counter
across all members. Private chats remain per-user.
- state.js: renamed key arg from userId to subject (user|chat id)
- handlers.js: getSubject(ctx) picks user id in DM, chat id in groups
- /loldle_stats labels scope as "your" vs "group" accordingly
* feat(loldle): add /loldle_new + switch to self-paced rounds
- /loldle_new starts a new random round. If the previous round is not
solved/given-up, it's recorded as a loss (auto-giveup) before rerolling.
- Drop daily-seeded targets: each round picks a uniformly-random champion
(pickRandom in daily.js; pickDaily kept for future use).
- state.js: one active round per subject (no date in key). TTL raised to
7 days; streak = consecutive wins (round-based, not date-based).
- Register /loldle_new in module index; now 8 public loldle commands.
- Tests: add pickRandom cases; bump expected command count to 12.
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: tiennm99 <tiennm99@outlook.com>
Adds loldle module with classic-mode champion guessing. Ports comparison
logic from tiennm99/loldle (lib/classic-mode.js) and bundles champion
data from tiennm99/loldle-data. Adds GH Actions workflow that re-syncs
champions.json on cross-repo dispatch from loldle-data.
- Three public commands: /loldle, /loldle_giveup, /loldle_stats
- Per-user daily state + streak stats in KV (3-day TTL on games)
- champions-data.js wrapper sidesteps Node 24 / esbuild disagreement on
JSON import attributes; generator script + npm run build:loldle-data
- register script now tolerates missing .env.deploy (env-file-if-exists)
so Workers Builds can inject env vars directly
- fix(scripts): escape stray */ in migrate.js docstring that broke node
- 16 new unit tests (compare, daily, lookup); dispatcher test updated
for the new command set
- trading_trades table (migration 0001) persists every buy/sell via optional onTrade callback
- /history [n] command shows caller's last N trades (default 10, max 50), HTML-escaped
- daily cron at 0 17 * * * trims to 1000/user + 10000/global via FIFO delete
- persistence failure logs but does not fail the trade reply
Portfolio schema now uses meta object: { currency, assets, meta: { invested } }.
Migrates old totalvnd field automatically on load. The meta object provides
a clean place for future per-user metadata without polluting the top level.
Replace hardcoded 9-symbol registry with dynamic TCBS-based resolution.
Any VN stock ticker is now resolved on first use and cached in KV
permanently. Portfolio flattened from 4 category maps to single assets
map with automatic migration of old format. Crypto, gold, and currency
exchange disabled with "coming soon" message.
Replace hardcoded 0.5% spread with live buy/sell rates from BIDV bank
API. Buying USD uses bank's sell rate (higher), selling USD uses bank's
buy rate (lower). Reply shows both rates and actual spread percentage.
Topup now only accepts VND — users must convert to get other currencies.
Convert uses a 0.5% bid/ask spread: buying USD costs more VND (ask),
selling USD back gives less VND (bid). Simulates real forex behavior.
Paper trading system with 5 commands (trade_topup, trade_buy,
trade_sell, trade_convert, trade_stats). Supports VN stocks via TCBS,
crypto via CoinGecko, forex via ER-API, and gold via PAX Gold proxy.
Per-user portfolio stored in KV with 60s price caching. 54 new tests.
grammY-based bot with a module plugin system loaded from the MODULES env
var. Three command visibility levels (public/protected/private) share a
unified command namespace with conflict detection at registry build.
- 4 initial modules (util, wordle, loldle, misc); util fully implemented,
others are stubs proving the plugin system end-to-end
- util: /info (chat/thread/sender ids) + /help (pure renderer over the
registry, HTML parse mode, escapes user-influenced strings)
- KVStore interface with CFKVStore and a per-module prefixing factory;
getJSON/putJSON convenience helpers; other backends drop in via one file
- Webhook at POST /webhook with secret-token validation via grammY's
webhookCallback; no admin HTTP surface
- Post-deploy register script (npm run deploy = wrangler deploy && node
--env-file=.env.deploy scripts/register.js) for setWebhook and
setMyCommands; --dry-run flag for preview
- 56 vitest unit tests across 7 suites covering registry, db wrapper,
dispatcher, help renderer, validators, and HTML escaper
- biome for lint + format; phased implementation plan under plans/