mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-15 20:16:11 +00:00
refactor(modules): extract shared chathelper + champname packages
Phase 3 of the 2026-05-09 review remediation plan. Eliminates the helper drift (subjectFor / argAfterCommand / nowMillis / reply / replyHTML / winRate) that previously lived in 3-4 modules, plus the loldle-specific normalize / findChampion / findByExactName. - internal/modules/util/chathelper: SubjectFor, ArgAfterCommand, NowMillis, Reply, ReplyHTML, WinRate. Single canonical SubjectFor shape (group/supergroup -> chat ID, else user ID); WinRate uses math.Round to match JS Math.round (the truncation drift caught in Phase 5b/5c). - internal/champname: Normalize + generic Find[T] / FindByExactName[T] with name-extractor closure. Loldle and loldle-emoji both consume via Champion / EmojiChampion. Migrations: wordle, loldle, loldle-emoji, misc, util/info, util/stickerid. Module-local lookup.go + normalize.go in loldle and loldle-emoji deleted. go test -race -count=1 ./... clean across all 12 packages. Net ~290 lines removed across handler files.
This commit is contained in:
1 parent
939c5fedab
commit
80ef664473
16 files changed
+503
-546
No files matched your search
@@ -0,0 +1,83 @@
|
||||
// Package chathelper consolidates per-module Telegram helpers that diverged
|
||||
// across wordle/loldle/loldleemoji/misc. SubjectFor, ArgAfterCommand,
|
||||
// NowMillis, Reply, ReplyHTML, WinRate previously had near-identical copies
|
||||
// in each module — one drift incident (winRate truncation) already bit
|
||||
// Phase 5b/5c. Single source here; modules import.
|
||||
package chathelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
)
|
||||
|
||||
// SubjectFor mirrors JS getSubject: group/supergroup → chat ID (shared game
|
||||
// state), otherwise → user ID. Returns "" when no usable id is present
|
||||
// (caller should reply with a "cannot identify chat" error). Channels and
|
||||
// unknown chat types fall through to From.ID.
|
||||
//
|
||||
// Canonical shape: the wordle module previously had an explicit
|
||||
// ChatTypePrivate branch returning From.ID, which is identical to the
|
||||
// default branch — folded together here.
|
||||
func SubjectFor(msg *models.Message) string {
|
||||
if msg == nil {
|
||||
return ""
|
||||
}
|
||||
switch msg.Chat.Type {
|
||||
case models.ChatTypeGroup, models.ChatTypeSupergroup:
|
||||
return strconv.FormatInt(msg.Chat.ID, 10)
|
||||
default:
|
||||
if msg.From != nil {
|
||||
return strconv.FormatInt(msg.From.ID, 10)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ArgAfterCommand returns everything after the first space in text, trimmed.
|
||||
// Works for `/cmd arg`, `/cmd@bot arg`, etc. JS-parity.
|
||||
func ArgAfterCommand(text string) string {
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
idx := strings.IndexByte(text, ' ')
|
||||
if idx < 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(text[idx+1:])
|
||||
}
|
||||
|
||||
// NowMillis returns current UTC ms-since-epoch.
|
||||
func NowMillis() int64 { return time.Now().UTC().UnixMilli() }
|
||||
|
||||
// Reply sends a plain-text response to the given chat.
|
||||
func Reply(ctx context.Context, b *bot.Bot, chatID int64, text string) error {
|
||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: text})
|
||||
return err
|
||||
}
|
||||
|
||||
// ReplyHTML sends a Telegram HTML-formatted response to the given chat.
|
||||
func ReplyHTML(ctx context.Context, b *bot.Bot, chatID int64, text string) error {
|
||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: chatID,
|
||||
Text: text,
|
||||
ParseMode: models.ParseModeHTML,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// WinRate computes wins/played as a percentage rounded to nearest int.
|
||||
// math.Round matches JS Math.round (round half away from zero for positive
|
||||
// inputs); plain int(...) truncation would render 2/3 as 66% where JS shows
|
||||
// 67%. Returns 0 when played == 0 (avoids NaN).
|
||||
func WinRate(wins, played int) int {
|
||||
if played <= 0 {
|
||||
return 0
|
||||
}
|
||||
return int(math.Round(float64(wins) / float64(played) * 100))
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package chathelper
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-telegram/bot/models"
|
||||
)
|
||||
|
||||
func TestSubjectFor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
msg *models.Message
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "nil message",
|
||||
msg: nil,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "private chat with From",
|
||||
msg: &models.Message{
|
||||
Chat: models.Chat{ID: 999, Type: models.ChatTypePrivate},
|
||||
From: &models.User{ID: 42},
|
||||
},
|
||||
want: "42",
|
||||
},
|
||||
{
|
||||
name: "private chat without From",
|
||||
msg: &models.Message{
|
||||
Chat: models.Chat{ID: 999, Type: models.ChatTypePrivate},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "group chat → chat id (ignores From)",
|
||||
msg: &models.Message{
|
||||
Chat: models.Chat{ID: -100, Type: models.ChatTypeGroup},
|
||||
From: &models.User{ID: 42},
|
||||
},
|
||||
want: "-100",
|
||||
},
|
||||
{
|
||||
name: "supergroup → chat id",
|
||||
msg: &models.Message{
|
||||
Chat: models.Chat{ID: -1001, Type: models.ChatTypeSupergroup},
|
||||
From: &models.User{ID: 42},
|
||||
},
|
||||
want: "-1001",
|
||||
},
|
||||
{
|
||||
name: "channel falls through to From.ID",
|
||||
msg: &models.Message{
|
||||
Chat: models.Chat{ID: -200, Type: models.ChatTypeChannel},
|
||||
From: &models.User{ID: 7},
|
||||
},
|
||||
want: "7",
|
||||
},
|
||||
{
|
||||
name: "channel without From",
|
||||
msg: &models.Message{
|
||||
Chat: models.Chat{ID: -200, Type: models.ChatTypeChannel},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := SubjectFor(tt.msg); got != tt.want {
|
||||
t.Errorf("SubjectFor: got %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgAfterCommand(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"/cmd", ""},
|
||||
{"/cmd ", ""},
|
||||
{"/cmd ", ""},
|
||||
{"/cmd word", "word"},
|
||||
{"/cmd word ", "word"},
|
||||
{"/cmd@bot word", "word"},
|
||||
{"/cmd two words", "two words"},
|
||||
{"/cmd two words ", "two words"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.in, func(t *testing.T) {
|
||||
if got := ArgAfterCommand(tt.in); got != tt.want {
|
||||
t.Errorf("ArgAfterCommand(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNowMillis(t *testing.T) {
|
||||
a := NowMillis()
|
||||
b := NowMillis()
|
||||
if b < a {
|
||||
t.Errorf("NowMillis went backwards: %d → %d", a, b)
|
||||
}
|
||||
if a < 1700000000000 {
|
||||
t.Errorf("NowMillis too small (not ms-epoch?): %d", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWinRate(t *testing.T) {
|
||||
tests := []struct {
|
||||
wins, played, want int
|
||||
}{
|
||||
{0, 0, 0}, // no games
|
||||
{0, 5, 0}, // 0%
|
||||
{5, 5, 100}, // 100%
|
||||
{2, 3, 67}, // round-half-up: 66.67% → 67% (NOT 66%)
|
||||
{1, 3, 33}, // 33.33% → 33%
|
||||
{1, 6, 17}, // 16.67% → 17%
|
||||
{1, 2, 50}, // exact 50%
|
||||
{3, 4, 75}, // exact 75%
|
||||
// negative played guards against caller bugs.
|
||||
{1, -1, 0},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := WinRate(tt.wins, tt.played)
|
||||
if got != tt.want {
|
||||
t.Errorf("WinRate(%d,%d) = %d, want %d", tt.wins, tt.played, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util/chathelper"
|
||||
)
|
||||
|
||||
// infoCommand returns /info — replies plain text with chat / thread / sender
|
||||
@@ -36,11 +37,7 @@ func infoCommand() modules.Command {
|
||||
senderID = fmt.Sprintf("%d", msg.From.ID)
|
||||
}
|
||||
text := fmt.Sprintf("chat id: %s\nthread id: %s\nsender id: %s", chatID, threadID, senderID)
|
||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: msg.Chat.ID,
|
||||
Text: text,
|
||||
})
|
||||
return err
|
||||
return chathelper.Reply(ctx, b, msg.Chat.ID, text)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules"
|
||||
"github.com/tiennm99/miti99bot-go/internal/modules/util/chathelper"
|
||||
)
|
||||
|
||||
const stickerIDUsage = "Reply to a sticker message with /stickerid to get its file_id.\n" +
|
||||
@@ -31,11 +32,7 @@ func stickerIDCommand() modules.Command {
|
||||
|
||||
sticker := stickerFrom(msg)
|
||||
if sticker == nil {
|
||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: msg.Chat.ID,
|
||||
Text: stickerIDUsage,
|
||||
})
|
||||
return err
|
||||
return chathelper.Reply(ctx, b, msg.Chat.ID, stickerIDUsage)
|
||||
}
|
||||
|
||||
setName := sticker.SetName
|
||||
@@ -55,12 +52,7 @@ func stickerIDCommand() modules.Command {
|
||||
fmt.Fprintf(&sb, "set: %s · emoji: %s",
|
||||
html.EscapeString(setName), html.EscapeString(emoji))
|
||||
|
||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: msg.Chat.ID,
|
||||
Text: sb.String(),
|
||||
ParseMode: models.ParseModeHTML,
|
||||
})
|
||||
return err
|
||||
return chathelper.ReplyHTML(ctx, b, msg.Chat.ID, sb.String())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user