From bdb8757dbe7a71fbb1aeb2a0a725969cbeeec1e3 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Wed, 1 Jul 2026 10:38:49 +0700 Subject: [PATCH] feat: rename lol schedule module --- Makefile | 2 +- README.md | 2 +- cmd/server/main.go | 27 +++-- docs/deploy-coolify-selfhosted.md | 8 +- .../{lolschedule => lol}/api_client.go | 24 ++-- .../{lolschedule => lol}/api_client_test.go | 4 +- internal/modules/{lolschedule => lol}/cron.go | 24 ++-- .../modules/{lolschedule => lol}/cron_test.go | 6 +- .../modules/{lolschedule => lol}/format.go | 20 ++-- .../{lolschedule => lol}/format_test.go | 2 +- .../modules/{lolschedule => lol}/handlers.go | 26 ++--- .../{lolschedule => lol}/handlers_test.go | 49 ++++---- .../lolschedule.go => lol/lol.go} | 20 ++-- .../{lolschedule => lol}/parse_date.go | 4 +- .../{lolschedule => lol}/parse_date_test.go | 2 +- internal/modules/lol/startup.go | 97 ++++++++++++++++ internal/modules/lol/startup_mongo_test.go | 108 ++++++++++++++++++ .../{lolschedule => lol}/subscribers.go | 12 +- .../{lolschedule => lol}/subscribers_test.go | 4 +- internal/modules/module.go | 2 +- internal/storage/mongo_doc_store_test.go | 10 +- telegram-commands.json | 14 +-- 22 files changed, 329 insertions(+), 138 deletions(-) rename internal/modules/{lolschedule => lol}/api_client.go (93%) rename internal/modules/{lolschedule => lol}/api_client_test.go (99%) rename internal/modules/{lolschedule => lol}/cron.go (92%) rename internal/modules/{lolschedule => lol}/cron_test.go (99%) rename internal/modules/{lolschedule => lol}/format.go (96%) rename internal/modules/{lolschedule => lol}/format_test.go (99%) rename internal/modules/{lolschedule => lol}/handlers.go (80%) rename internal/modules/{lolschedule => lol}/handlers_test.go (81%) rename internal/modules/{lolschedule/lolschedule.go => lol/lol.go} (72%) rename internal/modules/{lolschedule => lol}/parse_date.go (97%) rename internal/modules/{lolschedule => lol}/parse_date_test.go (99%) create mode 100644 internal/modules/lol/startup.go create mode 100644 internal/modules/lol/startup_mongo_test.go rename internal/modules/{lolschedule => lol}/subscribers.go (93%) rename internal/modules/{lolschedule => lol}/subscribers_test.go (99%) diff --git a/Makefile b/Makefile index 1cb322a..d60380f 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ test: ## Unit tests (no emulator required) MONGO_PORT ?= 27017 test-mongo: mongo-local ## Run MongoDB tests against a local Mongo container MONGODB_TEST_URL=mongodb://127.0.0.1:$(MONGO_PORT) LOG_LEVEL=error \ - go test -race -count=1 ./internal/storage/... ./internal/modules/stats/... + go test -race -count=1 ./internal/storage/... ./internal/modules/lol/... ./internal/modules/stats/... # ---- Lint / Vet ------------------------------------------------------------ diff --git a/README.md b/README.md index 33cb2cb..35ef1a0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Atlas via long polling and an in-process cron scheduler. | `misc` | `/ping`, `/mstats`, `/trongtruonghop` disclaimer | | `wordle` | Daily Wordle game | | `loldle` | League-of-Legends "guess the champion" | -| `lolschedule` | Pro-match schedule + daily push | +| `lol` | Pro-match schedule + daily push | | `wc` | World Cup schedule + silent daily push | | `stock` | VN-stocks paper trading | | `gold` | Gold paper trading (opt-in; primary VNAppMob SJC buy/sell VND/luong, fallback spot XAU) | diff --git a/cmd/server/main.go b/cmd/server/main.go index f70e679..46105ad 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -19,8 +19,8 @@ import ( "github.com/tiennm99/miti99bot/internal/modules" "github.com/tiennm99/miti99bot/internal/modules/coin" "github.com/tiennm99/miti99bot/internal/modules/gold" + "github.com/tiennm99/miti99bot/internal/modules/lol" "github.com/tiennm99/miti99bot/internal/modules/loldle" - "github.com/tiennm99/miti99bot/internal/modules/lolschedule" "github.com/tiennm99/miti99bot/internal/modules/misc" "github.com/tiennm99/miti99bot/internal/modules/stats" "github.com/tiennm99/miti99bot/internal/modules/stock" @@ -58,16 +58,16 @@ func resolveCommitSHA(envSourceCommit string) string { // import cycle (modules → util → modules). func factories() map[string]modules.Factory { return map[string]modules.Factory{ - "util": util.New, - "misc": misc.New, - "wordle": wordle.New, - "loldle": loldle.New, - "lolschedule": lolschedule.New, - "wc": wc.New, - "coin": coin.New, - "gold": gold.New, - "stock": stock.New, - "stats": stats.New, + "util": util.New, + "misc": misc.New, + "wordle": wordle.New, + "loldle": loldle.New, + lol.CollectionName: lol.New, + "wc": wc.New, + "coin": coin.New, + "gold": gold.New, + "stock": stock.New, + "stats": stats.New, } } @@ -96,6 +96,9 @@ func main() { defer closeProvider() systemColl := provider.Collection(systemstate.CollectionName) + if err := lol.InitStore(rootCtx, provider, systemColl); err != nil { + log.Fatal("lol storage init failed", "err", err) + } if err := stats.InitStore(rootCtx, provider.Collection("stats"), systemColl); err != nil { log.Fatal("stats storage init failed", "err", err) } @@ -124,7 +127,7 @@ func main() { } // In-process cron scheduler runs unconditionally so the long-lived container - // fires module crons (e.g. the lolschedule daily push) on their Schedule. + // fires module crons (e.g. lol daily push) on their Schedule. stopCron, err := cron.Run(rootCtx, reg) if err != nil { log.Fatal("cron scheduler init failed", "err", err) diff --git a/docs/deploy-coolify-selfhosted.md b/docs/deploy-coolify-selfhosted.md index aed59e1..bdad904 100644 --- a/docs/deploy-coolify-selfhosted.md +++ b/docs/deploy-coolify-selfhosted.md @@ -65,10 +65,14 @@ overrides are not supported in runtime env; modules use coded defaults. > document — `{ _id: , ...payload fields, version, updatedAt }` with no > `value` envelope. Payload fields are hoisted to the document root so they > expand and are queryable in Compass. The two non-object values are wrapped in a -> named field: schedule subscribers under `subscribers` (array) and the daily -> push date under `date`. Concurrency uses the `version` field (optimistic lock); +> named field: `lol` schedule subscribers under `subscribers` (array) and the +> daily push date under `date`. Concurrency uses the `version` field (optimistic lock); > `updatedAt` is a BSON Date. > +> The `lol` module uses the `lol` collection. First startup after the rename +> copies documents from the legacy `lolschedule` collection, drops that legacy +> collection, and records completion in `system`. +> > The `stats` collection uses queryable aggregate documents for command/user > counts and creates indexes on startup. First startup after the schema change > migrates legacy `count:`, `user:`, and `pair:` stats keys into the new shape, diff --git a/internal/modules/lolschedule/api_client.go b/internal/modules/lol/api_client.go similarity index 93% rename from internal/modules/lolschedule/api_client.go rename to internal/modules/lol/api_client.go index 9e829ed..be580e4 100644 --- a/internal/modules/lolschedule/api_client.go +++ b/internal/modules/lol/api_client.go @@ -1,4 +1,4 @@ -// Package lolschedule serves LoL esports match schedules via lolesports.com's +// Package lol serves LoL esports match schedules via lolesports.com's // persisted API plus a daily push to subscribers. // // Endpoint: https://esports-api.lolesports.com/persisted/gw/getSchedule @@ -8,7 +8,7 @@ // // Cache strategy: KV-backed cacheRecord with a 120s fresh window and a // 60-minute stale fallback (stale-while-error). -package lolschedule +package lol import ( "context" @@ -153,7 +153,7 @@ func (c *Client) baseURL() string { func (c *Client) fetchSchedulePage(ctx context.Context, pageToken string) ([]ScheduleEvent, string, string, error) { u, err := url.Parse(c.baseURL()) if err != nil { - return nil, "", "", fmt.Errorf("lolschedule parse url: %w", err) + return nil, "", "", fmt.Errorf("lol parse url: %w", err) } q := u.Query() q.Set("hl", "en-US") @@ -164,7 +164,7 @@ func (c *Client) fetchSchedulePage(ctx context.Context, pageToken string) ([]Sch req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { - return nil, "", "", fmt.Errorf("lolschedule build request: %w", err) + return nil, "", "", fmt.Errorf("lol build request: %w", err) } req.Header.Set("x-api-key", apiKey) req.Header.Set("User-Agent", userAgent) @@ -172,20 +172,20 @@ func (c *Client) fetchSchedulePage(ctx context.Context, pageToken string) ([]Sch resp, err := c.httpClient().Do(req) if err != nil { - return nil, "", "", fmt.Errorf("lolschedule do: %w", err) + return nil, "", "", fmt.Errorf("lol do: %w", err) } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { - return nil, "", "", fmt.Errorf("lolschedule read: %w", err) + return nil, "", "", fmt.Errorf("lol read: %w", err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { - log.Warn("lolschedule_fetch", "status", resp.StatusCode, "body", truncate(string(body), 500)) - return nil, "", "", fmt.Errorf("lolschedule API HTTP %d", resp.StatusCode) + log.Warn("lol_fetch", "status", resp.StatusCode, "body", truncate(string(body), 500)) + return nil, "", "", fmt.Errorf("lol API HTTP %d", resp.StatusCode) } var page schedulePage if err := json.Unmarshal(body, &page); err != nil { - return nil, "", "", fmt.Errorf("lolschedule decode: %w", err) + return nil, "", "", fmt.Errorf("lol decode: %w", err) } // Drop pre/post-show segments; they aren't matches. out := make([]ScheduleEvent, 0, len(page.Data.Schedule.Events)) @@ -301,14 +301,14 @@ func (c *Client) GetEventsCached(ctx context.Context, cache CacheStore, from, to if fetchErr == nil { rec := cacheRecord{Ts: now, Events: events} if err := cache.Put(ctx, key, rec); err != nil { - log.Warn("lolschedule_kv_put_fail", "err", err) + log.Warn("lol_kv_put_fail", "err", err) } return events, nil } // Upstream failed — fall back to stale cache if recent enough. if hasCached && cached.Events != nil && now-cached.Ts < staleMaxAge.Milliseconds() { - log.Warn("lolschedule_stale_fallback", "err", fetchErr) + log.Warn("lol_stale_fallback", "err", fetchErr) return cached.Events, nil } return nil, fetchErr @@ -333,4 +333,4 @@ func truncate(s string, maxLen int) string { // ErrEmptyResult is reserved for explicit "no events" scenarios where the // fetch succeeded but returned zero matches. Currently unused outside tests // but kept exported so callers can distinguish from network errors. -var ErrEmptyResult = errors.New("lolschedule: no events in range") +var ErrEmptyResult = errors.New("lol: no events in range") diff --git a/internal/modules/lolschedule/api_client_test.go b/internal/modules/lol/api_client_test.go similarity index 99% rename from internal/modules/lolschedule/api_client_test.go rename to internal/modules/lol/api_client_test.go index 020d13f..631c650 100644 --- a/internal/modules/lolschedule/api_client_test.go +++ b/internal/modules/lol/api_client_test.go @@ -1,4 +1,4 @@ -package lolschedule +package lol import ( "context" @@ -16,7 +16,7 @@ import ( // newCacheStore returns a fresh typed cache store backed by an in-memory // collection — the per-test equivalent of what the factory wires in production. func newCacheStore() CacheStore { - return storage.Typed[cacheRecord](storage.NewMemoryProvider().Collection("lolschedule")) + return storage.Typed[cacheRecord](storage.NewMemoryProvider().Collection("lol")) } // mkServer spins an httptest.Server returning the supplied JSON body for diff --git a/internal/modules/lolschedule/cron.go b/internal/modules/lol/cron.go similarity index 92% rename from internal/modules/lolschedule/cron.go rename to internal/modules/lol/cron.go index c7e1773..970fe28 100644 --- a/internal/modules/lolschedule/cron.go +++ b/internal/modules/lol/cron.go @@ -1,4 +1,4 @@ -package lolschedule +package lol import ( "context" @@ -79,7 +79,7 @@ func classifyTerminal(err error) terminalKind { // dailyPushCronName is the cron route segment + in-process scheduler key. // Must match the regex in internal/server/router.go (^[a-z0-9_]{1,32}$). -const dailyPushCronName = "lolschedule_daily_push" +const dailyPushCronName = "lol_daily_push" // dailyPushSchedule drives the in-process scheduler (internal/cron). Cron // expression is UTC; 01:00 UTC == 08:00 ICT. @@ -130,7 +130,7 @@ func (s *state) dailyPushCron() modules.Cron { // core logic is testable without an actual *bot.Bot. func (s *state) dailyPushHandler(ctx context.Context, deps modules.Deps) error { if deps.Bot == nil { - return errors.New("lolschedule daily push: deps.Bot is nil (BuildOptions.Bot not wired)") + return errors.New("lol daily push: deps.Bot is nil (BuildOptions.Bot not wired)") } return runDailyPush(ctx, s, deps.Bot) } @@ -174,10 +174,10 @@ func claimDailyPush(ctx context.Context, store PushDateStore, pushDay string) (b func runDailyPush(ctx context.Context, s *state, sender messageSender) error { subs, err := listSubscribers(ctx, s.subscribers) if err != nil { - return fmt.Errorf("lolschedule daily push: list subscribers: %w", err) + return fmt.Errorf("lol daily push: list subscribers: %w", err) } if len(subs) == 0 { - log.Info("lolschedule daily push: no subscribers, skipping") + log.Info("lol daily push: no subscribers, skipping") return nil } @@ -185,7 +185,7 @@ func runDailyPush(ctx context.Context, s *state, sender messageSender) error { to := addDays(from, 1) events, err := s.client.GetEventsCached(ctx, s.cache, from, to) if err != nil { - return fmt.Errorf("lolschedule daily push: fetch matches: %w", err) + return fmt.Errorf("lol daily push: fetch matches: %w", err) } filtered := FilterMajor(events) text := RenderToday(filtered, from) @@ -198,10 +198,10 @@ func runDailyPush(ctx context.Context, s *state, sender messageSender) error { pushDay := ictDayKey(from) won, err := claimDailyPush(ctx, s.pushDate, pushDay) if err != nil { - return fmt.Errorf("lolschedule daily push: claim date: %w", err) + return fmt.Errorf("lol daily push: claim date: %w", err) } if !won { - log.Info("lolschedule daily push: already pushed today, skipping", "date", pushDay) + log.Info("lol daily push: already pushed today, skipping", "date", pushDay) return nil } @@ -224,7 +224,7 @@ func runDailyPush(ctx context.Context, s *state, sender messageSender) error { ParseMode: models.ParseModeHTML, DisableNotification: disableNotification, }); err != nil { - log.Warn("lolschedule daily push send failed", + log.Warn("lol daily push send failed", "chat", sub.ChatID, "thread", sub.ThreadID, "err", err) failed++ switch classifyTerminal(err) { @@ -243,7 +243,7 @@ func runDailyPush(ctx context.Context, s *state, sender messageSender) error { // strictly an improvement even when the writes fail. pruned := pruneDeadSubscribers(ctx, s, deadChats, deadTopics) - log.Info("lolschedule daily push complete", + log.Info("lol daily push complete", "subscribers", len(subs), "sent", sent, "failed", failed, @@ -266,7 +266,7 @@ func pruneDeadSubscribers(ctx context.Context, s *state, chatWide map[int64]stru for chatID := range chatWide { n, err := removeAllForChat(ctx, s.subscribers, chatID) if err != nil { - log.Warn("lolschedule prune dead chat failed", "chat", chatID, "err", err) + log.Warn("lol prune dead chat failed", "chat", chatID, "err", err) continue } removed += n @@ -279,7 +279,7 @@ func pruneDeadSubscribers(ctx context.Context, s *state, chatWide map[int64]stru } ok, err := removeSubscriber(ctx, s.subscribers, sub.ChatID, sub.ThreadID) if err != nil { - log.Warn("lolschedule prune dead topic failed", + log.Warn("lol prune dead topic failed", "chat", sub.ChatID, "thread", sub.ThreadID, "err", err) continue } diff --git a/internal/modules/lolschedule/cron_test.go b/internal/modules/lol/cron_test.go similarity index 99% rename from internal/modules/lolschedule/cron_test.go rename to internal/modules/lol/cron_test.go index 39b5207..07c7ac2 100644 --- a/internal/modules/lolschedule/cron_test.go +++ b/internal/modules/lol/cron_test.go @@ -1,4 +1,4 @@ -package lolschedule +package lol import ( "context" @@ -60,7 +60,7 @@ func fixedNow() time.Time { // collection, matching what the factory wires in production. func newTestStore(t *testing.T) (SubscriberStore, PushDateStore, CacheStore) { t.Helper() - col := storage.NewMemoryProvider().Collection("lolschedule") + col := storage.NewMemoryProvider().Collection("lol") return storage.Typed[subscribersDoc](col), storage.Typed[lastPushDoc](col), storage.Typed[cacheRecord](col) @@ -434,7 +434,7 @@ func TestClassifyTerminal(t *testing.T) { func TestDailyPushHandler_NilBot_ReturnsError(t *testing.T) { s := newTestState(t) - deps := modules.Deps{Store: storage.NewMemoryProvider().Collection("lolschedule")} + deps := modules.Deps{Store: storage.NewMemoryProvider().Collection("lol")} err := s.dailyPushHandler(context.Background(), deps) if err == nil { t.Fatal("expected error when deps.Bot is nil, got nil") diff --git a/internal/modules/lolschedule/format.go b/internal/modules/lol/format.go similarity index 96% rename from internal/modules/lolschedule/format.go rename to internal/modules/lol/format.go index 1c02714..50cf0f3 100644 --- a/internal/modules/lolschedule/format.go +++ b/internal/modules/lol/format.go @@ -1,4 +1,4 @@ -package lolschedule +package lol import ( "fmt" @@ -27,15 +27,15 @@ var leagueOrder = []string{ // tournaments most viewers care about. Without this filter the API // returns 135+ events/week and replies blow past Telegram's 4096-char limit. var majorLeagueSlugs = map[string]bool{ - "lck": true, - "lpl": true, - "lec": true, - "lcs": true, - "worlds": true, - "msi": true, - "first_stand": true, - "lcp": true, - "cblol-brazil": true, + "lck": true, + "lpl": true, + "lec": true, + "lcs": true, + "worlds": true, + "msi": true, + "first_stand": true, + "lcp": true, + "cblol-brazil": true, "emea_masters": true, } diff --git a/internal/modules/lolschedule/format_test.go b/internal/modules/lol/format_test.go similarity index 99% rename from internal/modules/lolschedule/format_test.go rename to internal/modules/lol/format_test.go index 3a8aad9..f60cdb5 100644 --- a/internal/modules/lolschedule/format_test.go +++ b/internal/modules/lol/format_test.go @@ -1,4 +1,4 @@ -package lolschedule +package lol import ( "strings" diff --git a/internal/modules/lolschedule/handlers.go b/internal/modules/lol/handlers.go similarity index 80% rename from internal/modules/lolschedule/handlers.go rename to internal/modules/lol/handlers.go index 6872d5c..6705f57 100644 --- a/internal/modules/lolschedule/handlers.go +++ b/internal/modules/lol/handlers.go @@ -1,4 +1,4 @@ -package lolschedule +package lol import ( "context" @@ -12,7 +12,7 @@ import ( "github.com/tiennm99/miti99bot/internal/modules/util/chathelper" ) -// state captures everything a lolschedule handler needs at runtime. +// state captures everything a lol handler needs at runtime. type state struct { subscribers SubscriberStore pushDate PushDateStore @@ -22,7 +22,7 @@ type state struct { // uses time.Now via the default zero-value. nowFn func() time.Time // subscribersMu serializes Get→mutate→Put on the single subscribers store - // slot. Two concurrent /lolschedule_subscribe calls in the same + // slot. Two concurrent /lol_subscribe calls in the same // millisecond would otherwise race and drop one append. subscribersMu sync.Mutex } @@ -34,7 +34,7 @@ func (s *state) now() time.Time { return time.Now() } -// handleSchedule is /lolschedule [date] — matches for one ICT day. +// handleSchedule is /lol [date] — matches for one ICT day. // Empty arg → today. func (s *state) handleSchedule(ctx context.Context, b *bot.Bot, update *models.Update) error { msg := update.Message @@ -49,17 +49,7 @@ func (s *state) handleSchedule(ctx context.Context, b *bot.Bot, update *models.U return s.replyForRange(ctx, b, msg, parsed.Date, addDays(parsed.Date, 1), false) } -// handleToday is /lolschedule_today — today's matches. -func (s *state) handleToday(ctx context.Context, b *bot.Bot, update *models.Update) error { - msg := update.Message - if msg == nil { - return nil - } - from := ictDayStartOf(s.now()) - return s.replyForRange(ctx, b, msg, from, addDays(from, 1), false) -} - -// handleWeek is /lolschedule_week — the current ICT calendar week +// handleWeek is /lol_this_week — the current ICT calendar week // (Monday 00:00 ICT through the following Monday 00:00 ICT, exclusive). func (s *state) handleWeek(ctx context.Context, b *bot.Bot, update *models.Update) error { msg := update.Message @@ -75,7 +65,7 @@ func (s *state) handleWeek(ctx context.Context, b *bot.Bot, update *models.Updat func (s *state) replyForRange(ctx context.Context, b *bot.Bot, msg *models.Message, from, to time.Time, week bool) error { events, err := s.client.GetEventsCached(ctx, s.cache, from, to) if err != nil { - log.Error("lolschedule_fetch_fail", "err", err, "from", from, "to", to) + log.Error("lol_fetch_fail", "err", err, "from", from, "to", to) hint := "Could not fetch matches. Try again later." if week { hint = "Could not fetch this week's matches. Try again later." @@ -92,7 +82,7 @@ func (s *state) replyForRange(ctx context.Context, b *bot.Bot, msg *models.Messa return chathelper.ReplyHTML(ctx, b, msg, text) } -// handleSubscribe is /lolschedule_subscribe — opt the chat into the daily +// handleSubscribe is /lol_subscribe — opt the chat into the daily // digest delivered by the in-process cron handler. func (s *state) handleSubscribe(ctx context.Context, b *bot.Bot, update *models.Update) error { msg := update.Message @@ -113,7 +103,7 @@ func (s *state) handleSubscribe(ctx context.Context, b *bot.Bot, update *models. return chathelper.Reply(ctx, b, msg, "Already subscribed.") } -// handleUnsubscribe is /lolschedule_unsubscribe — opt out. +// handleUnsubscribe is /lol_unsubscribe — opt out. func (s *state) handleUnsubscribe(ctx context.Context, b *bot.Bot, update *models.Update) error { msg := update.Message if msg == nil { diff --git a/internal/modules/lolschedule/handlers_test.go b/internal/modules/lol/handlers_test.go similarity index 81% rename from internal/modules/lolschedule/handlers_test.go rename to internal/modules/lol/handlers_test.go index fb578ee..a637542 100644 --- a/internal/modules/lolschedule/handlers_test.go +++ b/internal/modules/lol/handlers_test.go @@ -1,4 +1,4 @@ -package lolschedule +package lol import ( "context" @@ -13,7 +13,7 @@ import ( "github.com/tiennm99/miti99bot/internal/testutil" ) -// installSchedule wires the lolschedule module to a recording bot, with a +// installSchedule wires the lol module to a recording bot, with a // custom upstream HTTP server returning bodyJSON for every request. nowMs // fixes the clock so date-based handlers are deterministic. // Returns the recording bot and the subscriber store for inspection in tests. @@ -26,7 +26,7 @@ func installSchedule(t *testing.T, bodyJSON string, nowMs int64) (*testutil.Reco t.Cleanup(upstream.Close) rb := testutil.NewRecordingBot(t) - col := storage.NewMemoryProvider().Collection("lolschedule") + col := storage.NewMemoryProvider().Collection("lol") s := &state{ subscribers: storage.Typed[subscribersDoc](col), @@ -36,13 +36,12 @@ func installSchedule(t *testing.T, bodyJSON string, nowMs int64) (*testutil.Reco nowFn: func() time.Time { return time.UnixMilli(nowMs).UTC() }, } mod := modules.Module{ - Name: "lolschedule", + Name: "lol", Commands: []modules.Command{ - {Name: "lolschedule", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleSchedule}, - {Name: "lolschedule_today", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleToday}, - {Name: "lolschedule_week", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleWeek}, - {Name: "lolschedule_subscribe", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleSubscribe}, - {Name: "lolschedule_unsubscribe", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleUnsubscribe}, + {Name: "lol", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleSchedule}, + {Name: "lol_this_week", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleWeek}, + {Name: "lol_subscribe", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleSubscribe}, + {Name: "lol_unsubscribe", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleUnsubscribe}, }, } reg := &modules.Registry{ @@ -76,9 +75,9 @@ const todayBody = `{ } }` -func TestHandleToday_RendersHTMLAndFiltersMajor(t *testing.T) { +func TestHandleSchedule_DefaultsToToday(t *testing.T) { rb, _ := installSchedule(t, todayBody, fakeNowMs) - rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(1, "/lolschedule_today")) + rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(1, "/lol")) got := rb.LastSent() if got.Method != "sendMessage" { @@ -96,7 +95,7 @@ func TestHandleToday_RendersHTMLAndFiltersMajor(t *testing.T) { func TestHandleSchedule_BadDateInput(t *testing.T) { rb, _ := installSchedule(t, todayBody, fakeNowMs) - rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(1, "/lolschedule notadate")) + rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(1, "/lol notadate")) got := rb.LastSent().Text() if !strings.Contains(got, "Invalid date") { @@ -106,7 +105,7 @@ func TestHandleSchedule_BadDateInput(t *testing.T) { func TestHandleWeek_RendersWeek(t *testing.T) { rb, _ := installSchedule(t, todayBody, fakeNowMs) - rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(1, "/lolschedule_week")) + rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(1, "/lol_this_week")) got := rb.LastSent().Text() if !strings.Contains(got, "→") { @@ -122,12 +121,12 @@ func TestHandleWeek_RendersWeek(t *testing.T) { func TestHandleSubscribe_AddsAndIsIdempotent(t *testing.T) { rb, subsStore := installSchedule(t, todayBody, fakeNowMs) - rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/lolschedule_subscribe")) + rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/lol_subscribe")) if got := rb.LastSent().Text(); !strings.HasPrefix(got, "✅") { t.Errorf("first subscribe should confirm; got %q", got) } rb.Reset() - rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/lolschedule_subscribe")) + rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/lol_subscribe")) if got := rb.LastSent().Text(); !strings.Contains(got, "Already subscribed") { t.Errorf("duplicate subscribe should report Already; got %q", got) } @@ -143,7 +142,7 @@ func TestHandleSubscribe_AddsAndIsIdempotent(t *testing.T) { // originating topic instead of General. func TestHandleSubscribe_ForumTopic_CapturesThreadID(t *testing.T) { rb, subsStore := installSchedule(t, todayBody, fakeNowMs) - upd := testutil.NewSupergroupMessage(555, 999, "/lolschedule_subscribe") + upd := testutil.NewSupergroupMessage(555, 999, "/lol_subscribe") upd.Message.MessageThreadID = 42 rb.Bot.ProcessUpdate(context.Background(), upd) @@ -162,13 +161,13 @@ func TestHandleUnsubscribe_ForumTopic_RemovesOnlyThatTopic(t *testing.T) { // Subscribe in two topics of the same supergroup. for _, tid := range []int{42, 99} { - upd := testutil.NewSupergroupMessage(555, 999, "/lolschedule_subscribe") + upd := testutil.NewSupergroupMessage(555, 999, "/lol_subscribe") upd.Message.MessageThreadID = tid rb.Bot.ProcessUpdate(context.Background(), upd) } // Unsubscribe in topic 42 only. - upd := testutil.NewSupergroupMessage(555, 999, "/lolschedule_unsubscribe") + upd := testutil.NewSupergroupMessage(555, 999, "/lol_unsubscribe") upd.Message.MessageThreadID = 42 rb.Bot.ProcessUpdate(context.Background(), upd) @@ -181,14 +180,14 @@ func TestHandleUnsubscribe_ForumTopic_RemovesOnlyThatTopic(t *testing.T) { func TestHandleUnsubscribe(t *testing.T) { rb, _ := installSchedule(t, todayBody, fakeNowMs) - rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/lolschedule_subscribe")) + rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/lol_subscribe")) rb.Reset() - rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/lolschedule_unsubscribe")) + rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/lol_unsubscribe")) if got := rb.LastSent().Text(); got != "Unsubscribed." { t.Errorf("unsubscribe reply = %q, want 'Unsubscribed.'", got) } rb.Reset() - rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/lolschedule_unsubscribe")) + rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/lol_unsubscribe")) if got := rb.LastSent().Text(); !strings.Contains(got, "weren't subscribed") { t.Errorf("idempotent unsubscribe reply = %q", got) } @@ -201,7 +200,7 @@ func TestHandleSchedule_UpstreamFailureGivesFriendlyError(t *testing.T) { defer upstream.Close() rb := testutil.NewRecordingBot(t) - col := storage.NewMemoryProvider().Collection("lolschedule") + col := storage.NewMemoryProvider().Collection("lol") s := &state{ subscribers: storage.Typed[subscribersDoc](col), pushDate: storage.Typed[lastPushDoc](col), @@ -209,14 +208,14 @@ func TestHandleSchedule_UpstreamFailureGivesFriendlyError(t *testing.T) { client: &Client{HTTP: upstream.Client(), URL: upstream.URL}, nowFn: func() time.Time { return time.UnixMilli(fakeNowMs).UTC() }, } - cmd := modules.Command{Name: "lolschedule_today", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleToday} + cmd := modules.Command{Name: "lol", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleSchedule} reg := &modules.Registry{ - Modules: []modules.Module{{Name: "lolschedule", Commands: []modules.Command{cmd}}}, + Modules: []modules.Module{{Name: "lol", Commands: []modules.Command{cmd}}}, AllCommands: map[string]modules.Command{cmd.Name: cmd}, } modules.Install(rb.Bot, reg, modules.Auth{}) - rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(1, "/lolschedule_today")) + rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(1, "/lol")) if got := rb.LastSent().Text(); !strings.Contains(got, "Could not fetch") { t.Errorf("expected friendly fetch-error reply; got %q", got) } diff --git a/internal/modules/lolschedule/lolschedule.go b/internal/modules/lol/lol.go similarity index 72% rename from internal/modules/lolschedule/lolschedule.go rename to internal/modules/lol/lol.go index 57b86be..2c6de77 100644 --- a/internal/modules/lolschedule/lolschedule.go +++ b/internal/modules/lol/lol.go @@ -1,12 +1,12 @@ -package lolschedule +package lol import ( "github.com/tiennm99/miti99bot/internal/modules" "github.com/tiennm99/miti99bot/internal/storage" ) -// New is the lolschedule module Factory. The 5 user-facing commands plus the -// daily-push cron (lolschedule_daily_push at 08:00 ICT, fan-out to +// New is the lol module Factory. The 4 user-facing commands plus the +// daily-push cron (lol_daily_push at 08:00 ICT, fan-out to // subscribers) are wired here. The cron handler reads deps.Bot at invoke // time — main.go must wire BuildOptions.Bot for the cron to function; // without it the handler fails fast with a clear error. @@ -20,31 +20,25 @@ func New(deps modules.Deps) modules.Module { return modules.Module{ Commands: []modules.Command{ { - Name: "lolschedule", + Name: "lol", Visibility: modules.VisibilityPublic, Description: "LoL matches for a date (dd-mm-yyyy, dd/mm/yyyy, ddmmyyyy; default today)", Handler: s.handleSchedule, }, { - Name: "lolschedule_today", - Visibility: modules.VisibilityPublic, - Description: "Today's LoL esports matches (scores if played)", - Handler: s.handleToday, - }, - { - Name: "lolschedule_week", + Name: "lol_this_week", Visibility: modules.VisibilityPublic, Description: "LoL esports matches for this week (Mon–Sun, ICT)", Handler: s.handleWeek, }, { - Name: "lolschedule_subscribe", + Name: "lol_subscribe", Visibility: modules.VisibilityPublic, Description: "Get the daily LoL schedule digest at 08:00 ICT", Handler: s.handleSubscribe, }, { - Name: "lolschedule_unsubscribe", + Name: "lol_unsubscribe", Visibility: modules.VisibilityPublic, Description: "Stop receiving the daily LoL schedule digest", Handler: s.handleUnsubscribe, diff --git a/internal/modules/lolschedule/parse_date.go b/internal/modules/lol/parse_date.go similarity index 97% rename from internal/modules/lolschedule/parse_date.go rename to internal/modules/lol/parse_date.go index 3fff097..b1ac0f2 100644 --- a/internal/modules/lolschedule/parse_date.go +++ b/internal/modules/lol/parse_date.go @@ -1,4 +1,4 @@ -package lolschedule +package lol import ( "fmt" @@ -86,7 +86,7 @@ func splitParts(trimmed string) ([]string, string) { } } -// ParseScheduleDate parses a /lolschedule date argument. Empty input → today. +// ParseScheduleDate parses a /lol date argument. Empty input → today. // Returns the start of the requested ICT day as a UTC instant. func ParseScheduleDate(input string, now time.Time) parseDateResult { trimmed := strings.TrimSpace(input) diff --git a/internal/modules/lolschedule/parse_date_test.go b/internal/modules/lol/parse_date_test.go similarity index 99% rename from internal/modules/lolschedule/parse_date_test.go rename to internal/modules/lol/parse_date_test.go index 501731b..77aca30 100644 --- a/internal/modules/lolschedule/parse_date_test.go +++ b/internal/modules/lol/parse_date_test.go @@ -1,4 +1,4 @@ -package lolschedule +package lol import ( "strings" diff --git a/internal/modules/lol/startup.go b/internal/modules/lol/startup.go new file mode 100644 index 0000000..cb29cef --- /dev/null +++ b/internal/modules/lol/startup.go @@ -0,0 +1,97 @@ +package lol + +import ( + "context" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo/options" + + "github.com/tiennm99/miti99bot/internal/storage" + "github.com/tiennm99/miti99bot/internal/systemstate" +) + +const ( + // CollectionName is the current MongoDB collection/module key. + CollectionName = "lol" + + legacyCollectionName = "lolschedule" + + collectionMigrationName = "lol-collection-v1" + collectionMigrationKey = "migration:" + collectionMigrationName +) + +// InitStore performs lol collection startup maintenance. It is safe to call on +// every boot: the collection rename migration is guarded by the system +// collection and only applies to Mongo-backed storage. +func InitStore(ctx context.Context, provider storage.Provider, systemColl storage.Collection) error { + oldColl, okOld := storage.MongoCollection(provider.Collection(legacyCollectionName)) + newColl, okNew := storage.MongoCollection(provider.Collection(CollectionName)) + if !okOld || !okNew { + return nil + } + + sys := systemstate.New(systemColl) + if rec, ok, err := sys.Get(ctx, collectionMigrationKey); err != nil { + return fmt.Errorf("lol collection migration marker get: %w", err) + } else if ok && rec.Status == "done" { + return nil + } + + count, err := oldColl.CountDocuments(ctx, bson.D{}) + if err != nil { + return fmt.Errorf("lol legacy collection count: %w", err) + } + if count == 0 { + if err := oldColl.Drop(ctx); err != nil { + return fmt.Errorf("lol legacy collection drop: %w", err) + } + return markCollectionMigrationDone(ctx, sys, 0) + } + + cur, err := oldColl.Find(ctx, bson.D{}) + if err != nil { + return fmt.Errorf("lol legacy collection find: %w", err) + } + defer func() { _ = cur.Close(ctx) }() + + copied := int64(0) + for cur.Next(ctx) { + var doc bson.M + if err := cur.Decode(&doc); err != nil { + return fmt.Errorf("lol legacy collection decode: %w", err) + } + id, ok := doc["_id"] + if !ok { + return fmt.Errorf("lol legacy collection document missing _id") + } + if _, err := newColl.ReplaceOne(ctx, bson.M{"_id": id}, doc, options.Replace().SetUpsert(true)); err != nil { + return fmt.Errorf("lol collection copy %v: %w", id, err) + } + copied++ + } + if err := cur.Err(); err != nil { + return fmt.Errorf("lol legacy collection cursor: %w", err) + } + + if err := oldColl.Drop(ctx); err != nil { + return fmt.Errorf("lol legacy collection drop: %w", err) + } + return markCollectionMigrationDone(ctx, sys, copied) +} + +func markCollectionMigrationDone(ctx context.Context, sys systemstate.Store, count int64) error { + now := time.Now().UnixMilli() + if err := sys.Put(ctx, collectionMigrationKey, systemstate.Record{ + Kind: "migration", + Name: collectionMigrationName, + Status: "done", + Count: count, + CompletedAt: now, + UpdatedAt: now, + }); err != nil { + return fmt.Errorf("lol collection migration marker put: %w", err) + } + return nil +} diff --git a/internal/modules/lol/startup_mongo_test.go b/internal/modules/lol/startup_mongo_test.go new file mode 100644 index 0000000..c26242f --- /dev/null +++ b/internal/modules/lol/startup_mongo_test.go @@ -0,0 +1,108 @@ +package lol + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "go.mongodb.org/mongo-driver/v2/bson" + + "github.com/tiennm99/miti99bot/internal/storage" + "github.com/tiennm99/miti99bot/internal/systemstate" +) + +func TestInitStore_MongoMigratesLegacyCollection(t *testing.T) { + uri := os.Getenv("MONGODB_TEST_URL") + if uri == "" { + t.Skip("MONGODB_TEST_URL not set; skipping MongoDB integration test") + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + client, err := storage.NewMongoClient(ctx, uri) + if err != nil { + t.Fatalf("NewMongoClient: %v", err) + } + dbName := fmt.Sprintf("miti99bot_lol_test_%d", time.Now().UnixNano()) + db := client.Database(dbName) + defer func() { + cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _ = db.Drop(cleanupCtx) + _ = client.Disconnect(cleanupCtx) + }() + + provider := storage.NewMongoProvider(db) + legacyColl := provider.Collection(legacyCollectionName) + systemColl := provider.Collection(systemstate.CollectionName) + legacySubscribers := storage.Typed[subscribersDoc](legacyColl) + legacyPushDate := storage.Typed[lastPushDoc](legacyColl) + legacyCache := storage.Typed[cacheRecord](legacyColl) + + if err := legacySubscribers.Put(ctx, subscribersKey, subscribersDoc{Subscribers: []Subscriber{{ChatID: 7}, {ChatID: 8, ThreadID: 3}}}); err != nil { + t.Fatalf("legacy subscribers: %v", err) + } + if err := legacyPushDate.Put(ctx, lastPushDateKey, lastPushDoc{Date: "2026-07-01"}); err != nil { + t.Fatalf("legacy push date: %v", err) + } + if err := legacyCache.Put(ctx, "matches:from:to", cacheRecord{ + Ts: 123, + Events: []ScheduleEvent{{StartTime: "2026-07-01T01:00:00Z", League: League{Slug: "lck"}}}, + }); err != nil { + t.Fatalf("legacy cache: %v", err) + } + + if err := InitStore(ctx, provider, systemColl); err != nil { + t.Fatalf("InitStore: %v", err) + } + + newColl := provider.Collection(CollectionName) + newSubscribers := storage.Typed[subscribersDoc](newColl) + newPushDate := storage.Typed[lastPushDoc](newColl) + newCache := storage.Typed[cacheRecord](newColl) + + subs, _, err := newSubscribers.Get(ctx, subscribersKey) + if err != nil { + t.Fatalf("new subscribers get: %v", err) + } + if len(subs.Subscribers) != 2 || subs.Subscribers[1] != (Subscriber{ChatID: 8, ThreadID: 3}) { + t.Fatalf("new subscribers = %+v", subs.Subscribers) + } + push, _, err := newPushDate.Get(ctx, lastPushDateKey) + if err != nil { + t.Fatalf("new push date get: %v", err) + } + if push.Date != "2026-07-01" { + t.Fatalf("new push date = %q", push.Date) + } + cached, _, err := newCache.Get(ctx, "matches:from:to") + if err != nil { + t.Fatalf("new cache get: %v", err) + } + if cached.Ts != 123 || len(cached.Events) != 1 || cached.Events[0].League.Slug != "lck" { + t.Fatalf("new cache = %+v", cached) + } + + names, err := db.ListCollectionNames(ctx, bson.M{"name": legacyCollectionName}) + if err != nil { + t.Fatalf("ListCollectionNames: %v", err) + } + if len(names) != 0 { + t.Fatalf("legacy collection still exists: %v", names) + } + + rec, ok, err := systemstate.New(systemColl).Get(ctx, collectionMigrationKey) + if err != nil { + t.Fatalf("migration marker get: %v", err) + } + if !ok || rec.Status != "done" || rec.Name != collectionMigrationName || rec.Count != 3 { + t.Fatalf("migration marker = %+v ok=%v", rec, ok) + } + + if err := InitStore(ctx, provider, systemColl); err != nil { + t.Fatalf("second InitStore: %v", err) + } +} diff --git a/internal/modules/lolschedule/subscribers.go b/internal/modules/lol/subscribers.go similarity index 93% rename from internal/modules/lolschedule/subscribers.go rename to internal/modules/lol/subscribers.go index d3028e0..3c9804f 100644 --- a/internal/modules/lolschedule/subscribers.go +++ b/internal/modules/lol/subscribers.go @@ -1,4 +1,4 @@ -package lolschedule +package lol import ( "context" @@ -51,7 +51,7 @@ func listSubscribers(ctx context.Context, store SubscriberStore) ([]Subscriber, case errors.Is(err, storage.ErrNotFound): return nil, nil case err != nil: - return nil, fmt.Errorf("lolschedule listSubscribers: %w", err) + return nil, fmt.Errorf("lol listSubscribers: %w", err) } if doc.Subscribers != nil { return doc.Subscribers, nil @@ -69,7 +69,7 @@ func listSubscribersLegacy(raw []byte) ([]Subscriber, error) { } var legacy []int64 if err := json.Unmarshal(raw, &legacy); err != nil { - return nil, fmt.Errorf("lolschedule listSubscribers decode: %w", err) + return nil, fmt.Errorf("lol listSubscribers decode: %w", err) } out := make([]Subscriber, len(legacy)) for i, id := range legacy { @@ -97,7 +97,7 @@ func addSubscriber(ctx context.Context, store SubscriberStore, chatID int64, thr } subs = append(subs, Subscriber{ChatID: chatID, ThreadID: threadID}) if err := store.Put(ctx, subscribersKey, subscribersDoc{Subscribers: subs}); err != nil { - return false, fmt.Errorf("lolschedule addSubscriber: %w", err) + return false, fmt.Errorf("lol addSubscriber: %w", err) } return true, nil } @@ -125,7 +125,7 @@ func removeSubscriber(ctx context.Context, store SubscriberStore, chatID int64, return false, nil } if err := store.Put(ctx, subscribersKey, subscribersDoc{Subscribers: out}); err != nil { - return false, fmt.Errorf("lolschedule removeSubscriber: %w", err) + return false, fmt.Errorf("lol removeSubscriber: %w", err) } return true, nil } @@ -155,7 +155,7 @@ func removeAllForChat(ctx context.Context, store SubscriberStore, chatID int64) return 0, nil } if err := store.Put(ctx, subscribersKey, subscribersDoc{Subscribers: out}); err != nil { - return 0, fmt.Errorf("lolschedule removeAllForChat: %w", err) + return 0, fmt.Errorf("lol removeAllForChat: %w", err) } return removed, nil } diff --git a/internal/modules/lolschedule/subscribers_test.go b/internal/modules/lol/subscribers_test.go similarity index 99% rename from internal/modules/lolschedule/subscribers_test.go rename to internal/modules/lol/subscribers_test.go index a35aa6d..7a1a486 100644 --- a/internal/modules/lolschedule/subscribers_test.go +++ b/internal/modules/lol/subscribers_test.go @@ -1,4 +1,4 @@ -package lolschedule +package lol import ( "context" @@ -9,7 +9,7 @@ import ( ) func newSubscriberStore() SubscriberStore { - return storage.Typed[subscribersDoc](storage.NewMemoryProvider().Collection("lolschedule")) + return storage.Typed[subscribersDoc](storage.NewMemoryProvider().Collection("lol")) } func TestSubscribers_AddRemoveListIdempotent(t *testing.T) { diff --git a/internal/modules/module.go b/internal/modules/module.go index cee0116..1c1e7c0 100644 --- a/internal/modules/module.go +++ b/internal/modules/module.go @@ -72,7 +72,7 @@ type Module struct { type Deps struct { Store storage.Collection // the module's own collection; build typed views with storage.Typed[T] Registry *Registry // populated by Build; safe to capture but read-only at module use - Bot *bot.Bot // nil-safe: only crons that fan-out (lolschedule daily push) need it + Bot *bot.Bot // nil-safe: only crons that fan-out (lol daily push) need it } // Factory constructs a Module from its Deps. Deps are passed directly (instead diff --git a/internal/storage/mongo_doc_store_test.go b/internal/storage/mongo_doc_store_test.go index 58947aa..c71c81a 100644 --- a/internal/storage/mongo_doc_store_test.go +++ b/internal/storage/mongo_doc_store_test.go @@ -162,7 +162,7 @@ func TestMongoDocStore_PutVersionedStaleConflict(t *testing.T) { } // wrappedScalar / wrappedArray prove non-object values become named root fields -// (the lolschedule pattern) rather than an envelope or fallback. +// (the lol pattern) rather than an envelope or fallback. type wrappedScalar struct { Date string `json:"date" bson:"date"` } @@ -176,20 +176,20 @@ func TestMongoDocStore_WrappedScalarAndArray(t *testing.T) { ctx := context.Background() p := NewMongoProvider(db) - scalar := Typed[wrappedScalar](p.Collection("lolschedule")) + scalar := Typed[wrappedScalar](p.Collection("lol")) if err := scalar.Put(ctx, "last_push_date", wrappedScalar{Date: "2026-06-28"}); err != nil { t.Fatalf("scalar Put: %v", err) } - doc := rawDoc(t, db.Collection("lolschedule"), "last_push_date") + doc := rawDoc(t, db.Collection("lol"), "last_push_date") if doc["date"] != "2026-06-28" { t.Errorf("scalar root field date = %v", doc["date"]) } - arr := Typed[wrappedArray](p.Collection("lolschedule")) + arr := Typed[wrappedArray](p.Collection("lol")) if err := arr.Put(ctx, "subscribers", wrappedArray{Subscribers: []int{1, 2, 3}}); err != nil { t.Fatalf("array Put: %v", err) } - doc = rawDoc(t, db.Collection("lolschedule"), "subscribers") + doc = rawDoc(t, db.Collection("lol"), "subscribers") if _, ok := doc["subscribers"].(bson.A); !ok { t.Errorf("array root field subscribers = %T, want bson.A", doc["subscribers"]) } diff --git a/telegram-commands.json b/telegram-commands.json index df0beef..89c4552 100644 --- a/telegram-commands.json +++ b/telegram-commands.json @@ -41,23 +41,19 @@ "description": "Show your loldle stats" }, { - "command": "lolschedule", + "command": "lol", "description": "LoL matches for a date" }, { - "command": "lolschedule_today", - "description": "Today's LoL esports matches" + "command": "lol_this_week", + "description": "LoL matches for this week" }, { - "command": "lolschedule_week", - "description": "LoL matches for the next 7 days" - }, - { - "command": "lolschedule_subscribe", + "command": "lol_subscribe", "description": "Get the daily LoL schedule digest" }, { - "command": "lolschedule_unsubscribe", + "command": "lol_unsubscribe", "description": "Stop the daily LoL schedule digest" }, {