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 default sticker pack name and the lol User-Agent now follow whichever
bot runs the code, so a bot account change needs no code edit. The default
pack becomes miti99_by_<bot username>.
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.
Swap the lol module upstream from the lolesports.com gql persisted-query
client to PandaScore REST (/lol/matches, Bearer LOL_PANDASCORE_TOKEN,
free tier 1000 req/h). The gql transport broke whenever Riot redeployed
their frontend; PandaScore is a stable versioned contract.
ScheduleEvent, formatters, cron, and the bson cache shape are unchanged:
only the transport and response mapping moved. PandaScore league slugs
canonicalize to the existing major-league allowlist; results join to
opponents by team_id so reversed arrays cannot swap scores; outcomes are
declared only once upstream commits a winner, preserving the
score-pending rendering. A still-full final page now logs
lol_page_budget_exhausted and the live page budget covers 500 raw
matches per window.
Missing token short-circuits with lol_token_missing before any upstream
call; the 60-minute stale cache still covers outages. Document the new
env var and cancel the superseded Leaguepedia score-enrichment plan.
Riot retired esports-api.lolesports.com and revoked its public x-api-key
(403 for everyone), so fetch schedules from the lolesports.com /api/gql
gateway instead using the web client's registered homeEvents operation.
The persisted-query ID comes from the frontend's operations manifest;
the package doc records how to refresh it when Riot rotates it, and a
rotated ID logs lol_persisted_query_rotated instead of caching an empty
schedule. Date windows are padded a day each side and filtered to exact
instants locally because the gateway's date params have unspecified
timezone semantics.
lolesports drives event.state off the broadcast timeline but fills
result.outcome and result.gameWins from a separate per-game ingestion
path. A match therefore reads as "completed" for hours before gaining a
score, and in that window every team carries {"outcome": null,
"gameWins": 0}.
The renderer used `Result != nil` as its has-a-score test, which cannot
catch this: the result object is present, only its contents are empty.
The absent gameWins fell through to Go's zero value and printed as a
literal 0, so an unscored series was reported as a definitive draw
("MKOI 0-0 KC") with neither side bolded.
Gate the score on a declared outcome instead, and render unscored
finished matches as the matchup alone with a pending marker. An outcome
on either side is enough to trust the score - it proves the ingestion
ran. inProgress keeps rendering 0-0, which is truthful for a live series
that has not resolved its first game.
Also collapses the score extraction both branches duplicated into
shared helpers.