mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-15 12:15:05 +00:00
fix(lol): rename next week command
This commit is contained in:
1 parent
00a5ff1c0b
commit
194caf17eb
9 files changed
+177
-13
No files matched your search
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@
|
||||
"description": "LoL matches for this week"
|
||||
},
|
||||
{
|
||||
"command": "lol_nextweek",
|
||||
"command": "lol_next_week",
|
||||
"description": "LoL matches for next week"
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user