mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-07-25 02:21:01 +00:00
feat: rename lol schedule module
This commit is contained in:
@@ -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 ------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
+15
-12
@@ -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)
|
||||
|
||||
@@ -65,10 +65,14 @@ overrides are not supported in runtime env; modules use coded defaults.
|
||||
> document — `{ _id: <user key>, ...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,
|
||||
|
||||
@@ -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")
|
||||
+2
-2
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package lolschedule
|
||||
package lol
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -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 {
|
||||
+24
-25
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package lolschedule
|
||||
package lol
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+2
-2
@@ -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) {
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user