feat(misc): add wheelofnames command

This commit is contained in:
tiennm99 committed 2026-07-03 11:12:34 +07:00
1 parent beebddf1d8
commit d375029214
5 files changed
+83 -2

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@ Atlas via long polling and an in-process cron scheduler.
| Module | What it does |
|---|---|
| `util` | `/help`, `/info`, `/stickerid` |
| `misc` | `/ping`, `/ping_stats`, `/the_answer`, `/trongtruonghop` + `/tth` disclaimer |
| `misc` | `/ping`, `/ping_stats`, `/wheelofnames`, `/the_answer`, `/trongtruonghop` + `/tth` disclaimer |
| `wordle` | Daily Wordle game |
| `loldle` | League-of-Legends "guess the champion" |
| `lol` | Pro-match schedule + daily push |
+42
View File
@@ -89,6 +89,48 @@ func TestPingStats_DeniedToNonAdmin(t *testing.T) {
}
}
func TestWheelOfNames_UsageWhenMissingOptions(t *testing.T) {
for _, text := range []string{"/wheelofnames", "/wheelofnames , ,"} {
t.Run(text, func(t *testing.T) {
rb, _ := installMisc(t, 999)
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, text))
if got := rb.LastSent().Text(); got != wheelOfNamesUsage {
t.Errorf("wheelofnames reply = %q, want usage %q", got, wheelOfNamesUsage)
}
})
}
}
func TestWheelOfNames_SingleOption(t *testing.T) {
rb, _ := installMisc(t, 999)
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/wheelofnames Alice"))
if got := rb.LastSent().Text(); got != "Alice" {
t.Errorf("wheelofnames reply = %q, want Alice", got)
}
}
func TestWheelOfNames_PicksFromTrimmedOptions(t *testing.T) {
rb, _ := installMisc(t, 999)
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/wheelofnames Alice, Bob, Carol"))
got := rb.LastSent().Text()
if got != "Alice" && got != "Bob" && got != "Carol" {
t.Errorf("wheelofnames reply = %q, want one of Alice/Bob/Carol", got)
}
}
func TestWheelOfNames_IgnoresEmptySegments(t *testing.T) {
rb, _ := installMisc(t, 999)
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/wheelofnames , Alice , , Bob ,"))
got := rb.LastSent().Text()
if got != "Alice" && got != "Bob" {
t.Errorf("wheelofnames reply = %q, want Alice or Bob", got)
}
}
// trongTruongHopUpdate is the inline counterpart of testutil.NewPrivateMessage
// for cases that need control over From (username, names). The dispatcher
// requires a bot_command entity, so we lift that from the helper API by reusing
+35 -1
View File
@@ -1,6 +1,7 @@
// Package misc is a small stub module that proves the framework end-to-end:
// /ping (public, exercises KV write), /ping_stats (protected, exercises KV
// read), /the_answer (private easter egg).
// read), /wheelofnames (public random picker), /the_answer (private easter
// egg).
package misc
import (
@@ -8,6 +9,7 @@ import (
"errors"
"fmt"
"html"
"math/rand/v2"
"strings"
"time"
@@ -31,6 +33,8 @@ const defaultTarget = "VNG"
// %s slots: target (escaped), sender mention, sender mention.
const trongTruongHopTemplate = "Trong trường hợp nhóm này bị điều tra bởi %s, %s khẳng định không liên quan tới nhóm hoặc những cá nhân khác trong nhóm này. %s không rõ tại sao lại có mặt ở đây vào thời điểm này, có lẽ tài khoản đã được thêm bởi một bên thứ ba."
const wheelOfNamesUsage = "Usage: /wheelofnames <option1>, <option2>, ..."
// lastPing is the value stored at the `last_ping` key: { at: <ms-since-epoch> }.
// int64 ms-epoch (not time.Time → RFC3339) keeps the on-disk shape compact
// and consistent with every other timestamp field in the bot's KV.
@@ -46,6 +50,7 @@ func New(deps modules.Deps) modules.Module {
Commands: []modules.Command{
pingCommand(store),
pingStatsCommand(store),
wheelOfNamesCommand(),
theAnswerCommand(),
trongTruongHopCommand("trongtruonghop"),
trongTruongHopCommand("tth"),
@@ -99,6 +104,35 @@ func pingStatsCommand(store storage.DocStore[lastPing]) modules.Command {
}
}
func splitWheelOptions(arg string) []string {
parts := strings.Split(arg, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
if option := strings.TrimSpace(part); option != "" {
out = append(out, option)
}
}
return out
}
func wheelOfNamesCommand() modules.Command {
return modules.Command{
Name: "wheelofnames",
Visibility: modules.VisibilityPublic,
Description: "Pick one random comma-separated option",
Handler: func(ctx context.Context, b *bot.Bot, update *models.Update) error {
if update.Message == nil {
return nil
}
options := splitWheelOptions(chathelper.ArgAfterCommand(update.Message.Text))
if len(options) == 0 {
return chathelper.Reply(ctx, b, update.Message, wheelOfNamesUsage)
}
return chathelper.Reply(ctx, b, update.Message, options[rand.N(len(options))])
},
}
}
// senderMention renders the mention used inside the trongtruonghop template.
// Prefer @username (Telegram resolves it server-side and enforces a safe
// charset). Fall back to a tg://user?id link with the user's display name when
+1
View File
@@ -24,6 +24,7 @@ func TestNew_RegistersExpectedCommands(t *testing.T) {
want := map[string]modules.Visibility{
"ping": modules.VisibilityPublic,
"ping_stats": modules.VisibilityProtected,
"wheelofnames": modules.VisibilityPublic,
"the_answer": modules.VisibilityPrivate,
"trongtruonghop": modules.VisibilityPublic,
"tth": modules.VisibilityPublic,
+4
View File
@@ -8,6 +8,10 @@
"command": "ping",
"description": "Health check; replies pong"
},
{
"command": "wheelofnames",
"description": "Pick one random comma-separated option"
},
{
"command": "trongtruonghop",
"description": "Phát biểu disclaimer cho thành viên hiện tại"