fix(lol): rename next week command

This commit is contained in:
tiennm99 committed 2026-07-06 20:46:10 +07:00
1 parent 00a5ff1c0b
commit 194caf17eb
9 files changed
+177 -13

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@ Atlas via long polling and an in-process cron scheduler.
| `misc` | `/ping`, `/ping_stats`, `/random`, `/wheelofnames`, `/wheelofnamesbeta`, `/the_answer`, `/trongtruonghop` + `/tth` disclaimer |
| `wordle` | Daily Wordle game |
| `loldle` | League-of-Legends "guess the champion" |
| `lol` | Pro-match schedule (`/lol`, `/lol_tomorrow`, `/lol_this_week`, `/lol_nextweek`) + daily push |
| `lol` | Pro-match schedule (`/lol`, `/lol_tomorrow`, `/lol_this_week`, `/lol_next_week`) + daily push |
| `wc` | World Cup schedule + silent daily push |
| `stock` | VN-stocks paper trading |
| `gold` | Gold paper trading (opt-in; VNAppMob SJC buy/sell VND/luong) |
+2 -1
View File
@@ -29,6 +29,7 @@ import (
"github.com/tiennm99/miti99bot/internal/modules/wordle"
"github.com/tiennm99/miti99bot/internal/server"
"github.com/tiennm99/miti99bot/internal/storage"
"github.com/tiennm99/miti99bot/internal/systemstate"
"github.com/tiennm99/miti99bot/internal/telegram"
)
@@ -94,7 +95,7 @@ func main() {
}
defer closeProvider()
if err := stats.InitStore(rootCtx, provider.Collection("stats")); err != nil {
if err := stats.InitStore(rootCtx, provider.Collection("stats"), provider.Collection(systemstate.CollectionName)); err != nil {
log.Fatal("stats storage init failed", "err", err)
}
if err := lol.InitStore(rootCtx, provider.Collection(lol.CollectionName)); err != nil {
+1 -1
View File
@@ -79,7 +79,7 @@ func (s *state) handleWeek(ctx context.Context, b *bot.Bot, update *models.Updat
"No major LoL matches this week.", "")
}
// handleNextWeek is /lol_nextweek — the next ICT calendar week
// handleNextWeek is /lol_next_week — the next ICT calendar week
// (Monday 00:00 ICT through the following Monday 00:00 ICT, exclusive).
func (s *state) handleNextWeek(ctx context.Context, b *bot.Bot, update *models.Update) error {
msg := update.Message
+3 -3
View File
@@ -41,7 +41,7 @@ func installSchedule(t *testing.T, bodyJSON string, nowMs int64) (*testutil.Reco
{Name: "lol", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleSchedule},
{Name: "lol_tomorrow", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleTomorrow},
{Name: "lol_this_week", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleWeek},
{Name: "lol_nextweek", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleNextWeek},
{Name: "lol_next_week", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleNextWeek},
{Name: "lol_subscribe", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleSubscribe},
{Name: "lol_unsubscribe", Visibility: modules.VisibilityPublic, Description: "x", Handler: s.handleUnsubscribe},
},
@@ -160,7 +160,7 @@ func TestHandleTomorrow_RendersTomorrow(t *testing.T) {
func TestHandleNextWeek_RendersNextWeek(t *testing.T) {
rb, _ := installSchedule(t, futureBody, fakeNowMs)
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(1, "/lol_nextweek"))
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(1, "/lol_next_week"))
got := rb.LastSent().Text()
for _, want := range []string{"Mon May 11", "Sun May 17", "<b>LPL</b>", "JDG vs BLG"} {
@@ -179,7 +179,7 @@ func TestNewRegistersScheduleShortcuts(t *testing.T) {
for _, cmd := range mod.Commands {
got[cmd.Name] = true
}
for _, want := range []string{"lol", "lol_tomorrow", "lol_this_week", "lol_nextweek"} {
for _, want := range []string{"lol", "lol_tomorrow", "lol_this_week", "lol_next_week"} {
if !got[want] {
t.Errorf("New() missing command %s", want)
}
+1 -1
View File
@@ -41,7 +41,7 @@ func New(deps modules.Deps) modules.Module {
Handler: s.handleWeek,
},
{
Name: "lol_nextweek",
Name: "lol_next_week",
Visibility: modules.VisibilityPublic,
Description: "LoL esports matches for next week (Mon–Sun, ICT)",
Handler: s.handleNextWeek,
+88 -3
View File
@@ -2,30 +2,115 @@ package stats
import (
"context"
"errors"
"fmt"
"strings"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"github.com/tiennm99/miti99bot/internal/storage"
"github.com/tiennm99/miti99bot/internal/systemstate"
)
const (
statsCommandUsersIndexName = "stats_cmd_n_user"
statsUserCommandsIndexName = "stats_uid_n_cmd"
statsUsernameLookupIndexName = "stats_user_uid"
renameLolNextWeekStatsKey = "stats:command-rename:lol_nextweek-to-lol_next_week"
oldLolNextWeekCommand = "lol_nextweek"
newLolNextWeekCommand = "lol_next_week"
)
// InitStore performs stats collection startup maintenance. It is safe to call
// every boot: MongoDB indexes are created idempotently and memory storage is a
// no-op.
func InitStore(ctx context.Context, statsColl storage.Collection) error {
// every boot: MongoDB indexes are created idempotently and one-time command
// rename migrations are guarded by the shared system collection.
func InitStore(ctx context.Context, statsColl, systemColl storage.Collection) error {
if mongoColl, ok := storage.MongoCollection(statsColl); ok {
if err := ensureUsageIndexes(ctx, mongoColl); err != nil {
return err
}
}
if err := migrateCommandRename(ctx, statsColl, systemColl, oldLolNextWeekCommand, newLolNextWeekCommand, renameLolNextWeekStatsKey); err != nil {
return err
}
return nil
}
func migrateCommandRename(ctx context.Context, statsColl, systemColl storage.Collection, oldCmd, newCmd, markerKey string) error {
state := systemstate.New(systemColl)
if rec, ok, err := state.Get(ctx, markerKey); err != nil {
return fmt.Errorf("stats command rename marker %s: %w", markerKey, err)
} else if ok && rec.Status == "complete" {
return nil
}
docs := storage.Typed[usageEntry](statsColl)
keys, err := docs.List(ctx, oldCmd)
if err != nil {
return fmt.Errorf("stats command rename list %s: %w", oldCmd, err)
}
var moved int64
for _, key := range keys {
if key != oldCmd && !strings.HasPrefix(key, oldCmd+":") {
continue
}
entry, _, err := docs.Get(ctx, key)
if err != nil {
return fmt.Errorf("stats command rename get %s: %w", key, err)
}
if entry.Cmd != oldCmd {
continue
}
targetKey := usageKey(newCmd, entry.UserID)
target, _, err := docs.Get(ctx, targetKey)
if err != nil && !errors.Is(err, storage.ErrNotFound) {
return fmt.Errorf("stats command rename get target %s: %w", targetKey, err)
}
missingTarget := errors.Is(err, storage.ErrNotFound)
if missingTarget {
target = usageEntry{}
}
target.Cmd = newCmd
target.UserID = entry.UserID
if entry.UserID == 0 {
target.Username = ""
} else if entry.Username != "" {
target.Username = entry.Username
}
target.N += entry.N
if missingTarget {
target.Deleted = entry.Deleted
}
if !entry.Deleted {
target.Deleted = false
}
if err := docs.Put(ctx, targetKey, target); err != nil {
return fmt.Errorf("stats command rename put target %s: %w", targetKey, err)
}
if err := docs.Delete(ctx, key); err != nil {
return fmt.Errorf("stats command rename delete old %s: %w", key, err)
}
moved += entry.N
}
now := time.Now().UTC().UnixMilli()
if err := state.Put(ctx, markerKey, systemstate.Record{
Kind: "migration",
Name: markerKey,
Status: "complete",
Count: moved,
CompletedAt: now,
UpdatedAt: now,
}); err != nil {
return fmt.Errorf("stats command rename marker put %s: %w", markerKey, err)
}
return nil
}
+4 -2
View File
@@ -8,6 +8,7 @@ import (
"time"
"github.com/tiennm99/miti99bot/internal/storage"
"github.com/tiennm99/miti99bot/internal/systemstate"
)
func TestInitStore_MongoCreatesIndexes(t *testing.T) {
@@ -34,16 +35,17 @@ func TestInitStore_MongoCreatesIndexes(t *testing.T) {
provider := storage.NewMongoProvider(db)
statsColl := provider.Collection("stats")
systemColl := provider.Collection(systemstate.CollectionName)
rawStatsColl, ok := storage.MongoCollection(statsColl)
if !ok {
t.Fatal("stats collection is not Mongo-backed")
}
if err := InitStore(ctx, statsColl); err != nil {
if err := InitStore(ctx, statsColl, systemColl); err != nil {
t.Fatalf("InitStore: %v", err)
}
if err := InitStore(ctx, statsColl); err != nil {
if err := InitStore(ctx, statsColl, systemColl); err != nil {
t.Fatalf("InitStore second run: %v", err)
}
+76
View File
@@ -0,0 +1,76 @@
package stats
import (
"context"
"errors"
"testing"
"github.com/tiennm99/miti99bot/internal/storage"
"github.com/tiennm99/miti99bot/internal/systemstate"
)
func TestInitStore_RenamesLolNextWeekStatsOnce(t *testing.T) {
ctx := context.Background()
provider := storage.NewMemoryProvider()
statsColl := provider.Collection("stats")
systemColl := provider.Collection(systemstate.CollectionName)
docs := storage.Typed[usageEntry](statsColl)
seeds := map[string]usageEntry{
usageKey(oldLolNextWeekCommand, 0): {Cmd: oldLolNextWeekCommand, N: 2},
usageKey(oldLolNextWeekCommand, 7): {
Cmd: oldLolNextWeekCommand,
UserID: 7,
Username: "alice",
N: 3,
},
usageKey(newLolNextWeekCommand, 7): {
Cmd: newLolNextWeekCommand,
UserID: 7,
Username: "alice",
N: 5,
},
}
for key, entry := range seeds {
if err := docs.Put(ctx, key, entry); err != nil {
t.Fatalf("seed %s: %v", key, err)
}
}
if err := InitStore(ctx, statsColl, systemColl); err != nil {
t.Fatalf("InitStore: %v", err)
}
if err := InitStore(ctx, statsColl, systemColl); err != nil {
t.Fatalf("InitStore second run: %v", err)
}
anon, _, err := docs.Get(ctx, usageKey(newLolNextWeekCommand, 0))
if err != nil {
t.Fatalf("new anonymous stats: %v", err)
}
if anon.Cmd != newLolNextWeekCommand || anon.N != 2 || anon.UserID != 0 {
t.Fatalf("new anonymous stats = %+v, want cmd %q count 2", anon, newLolNextWeekCommand)
}
user, _, err := docs.Get(ctx, usageKey(newLolNextWeekCommand, 7))
if err != nil {
t.Fatalf("new user stats: %v", err)
}
if user.Cmd != newLolNextWeekCommand || user.UserID != 7 || user.Username != "alice" || user.N != 8 {
t.Fatalf("new user stats = %+v, want merged count 8", user)
}
for _, key := range []string{usageKey(oldLolNextWeekCommand, 0), usageKey(oldLolNextWeekCommand, 7)} {
if _, _, err := docs.Get(ctx, key); !errors.Is(err, storage.ErrNotFound) {
t.Fatalf("old key %s err = %v, want ErrNotFound", key, err)
}
}
rec, ok, err := systemstate.New(systemColl).Get(ctx, renameLolNextWeekStatsKey)
if err != nil {
t.Fatalf("migration marker: %v", err)
}
if !ok || rec.Status != "complete" || rec.Count != 5 {
t.Fatalf("migration marker = %+v ok=%v, want complete count 5", rec, ok)
}
}
+1 -1
View File
@@ -69,7 +69,7 @@
"description": "LoL matches for this week"
},
{
"command": "lol_nextweek",
"command": "lol_next_week",
"description": "LoL matches for next week"
},
{