mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-09-14 06:20:27 +00:00
/addsticker becomes a single stateless command in util, writing to one env-configured set (STICKER_PACK_NAME, default miti99_by_miti99bot). AddStickerToSet takes the set owner's user ID rather than the caller's, so nothing is per-user any more: the sticker module's pack records, slug reservations, pending deletes, per-user locks and its eight other commands are removed with it. The pack creates itself on first use. A positive STICKERSET_INVALID from the add triggers createNewStickerSet owned by OWNER_ID, seeded with the sticker that triggered it and titled with the slug half of the name; a name that is occupied but unwritable is reported instead of taken over. The mandatory "_by_<bot_username>" suffix is Telegram's own proof of authorship, so a misconfigured pack name is refused offline before any API call. StickerSet exposes no owner ID, so ownership is only provable when Telegram refuses. Video, GIF, animation and video-note sources are transcoded to WEBM/VP9 with ffmpeg: long edge scaled to exactly 512 in either direction, cut to 3s, capped at 30fps, audio dropped, retried down a CRF ladder until under 256KB. Animated and video stickers are copied by file_id with no conversion. Sticker format is per-sticker since Bot API 7.2, so one pack holds all three. ffmpeg cannot ship in distroless/static and Go has no VP9 encoder, so the runtime base becomes alpine with apk add ffmpeg. The image grows from roughly 20MB to 213MB, and the transcode holds the single dispatcher worker — bounded at 20s per encode and a 45s handler deadline for moving sources, against 10s for stills.
198 lines
6.9 KiB
Go
198 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"unicode/utf8"
|
|
|
|
"github.com/go-telegram/bot/models"
|
|
|
|
"github.com/tiennm99/miti99bot/internal/modules"
|
|
"github.com/tiennm99/miti99bot/internal/modules/stock"
|
|
moduleutil "github.com/tiennm99/miti99bot/internal/modules/util"
|
|
"github.com/tiennm99/miti99bot/internal/storage"
|
|
"github.com/tiennm99/miti99bot/internal/testutil"
|
|
)
|
|
|
|
func TestBotCommandMenu_UsesLoadedPublicCommandsInModuleOrder(t *testing.T) {
|
|
reg := &modules.Registry{
|
|
Modules: []modules.Module{
|
|
{
|
|
Name: "beta",
|
|
Commands: []modules.Command{
|
|
{Name: "beta_public", Description: "Beta public", Parameters: "<value>", Visibility: modules.VisibilityPublic},
|
|
{Name: "beta_private", Description: "Beta private", Visibility: modules.VisibilityPrivate},
|
|
},
|
|
},
|
|
{
|
|
Name: "alpha",
|
|
Commands: []modules.Command{
|
|
{Name: "alpha_public", Description: "Alpha public", Visibility: modules.VisibilityPublic},
|
|
{Name: "alpha_protected", Description: "Alpha protected", Visibility: modules.VisibilityProtected},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
got := botCommandMenu(reg)
|
|
want := []models.BotCommand{
|
|
{Command: "beta_public", Description: "<value>. Beta public."},
|
|
{Command: "alpha_public", Description: "Alpha public."},
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("commands = %v, want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("commands[%d] = %+v, want %+v", i, got[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommandDiscovery_AllPublicCommandsHaveSafeMetadata(t *testing.T) {
|
|
reg, err := modules.Build(nil, factories(), storage.NewMemoryProvider(), modules.BuildOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Build: %v", err)
|
|
}
|
|
|
|
expectedParameters := map[string]string{
|
|
"amlich": "[date]",
|
|
"duonglich": "<date> [nhuan]",
|
|
"coin_price": "<coin>",
|
|
"coin_topup": "<usd_amount>",
|
|
"coin_buy": "<coin> <usd_to_spend>",
|
|
"coin_sell": "<coin> <usd_to_receive>",
|
|
"gold_topup": "<vnd_amount>",
|
|
"gold_buy": "<luong>",
|
|
"gold_sell": "<luong>",
|
|
"lol": "[date]",
|
|
"loldle": "[champion]",
|
|
"monkeyd_crawl": "<url> [font_size]",
|
|
"monkeyd_tags": "<url>",
|
|
"random": "<option,...>",
|
|
"addsticker": "[emoji...]",
|
|
"stats": "[users | user <username> | cmd <command_name>]",
|
|
"stock_events": "<ticker> [days]",
|
|
"stock_info": "<ticker>",
|
|
"stock_price": "<ticker>",
|
|
"stock_topup": "<vnd_amount>",
|
|
"stock_buy": "<quantity> <ticker>",
|
|
"stock_sell": "<quantity> <ticker>",
|
|
"stock_cash_dividend": "<vnd_per_share> <ticker>",
|
|
"stock_share_dividend": "<ratio(owned:new)> <ticker>",
|
|
"trongtruonghop": "[target...]",
|
|
"tth": "[target...]",
|
|
"wheelofnames": "<option,...>",
|
|
"wordle": "[word]",
|
|
}
|
|
|
|
menu := botCommandMenu(reg)
|
|
if len(menu) != len(reg.PublicCommands()) {
|
|
t.Fatalf("menu commands = %d, public commands = %d", len(menu), len(reg.PublicCommands()))
|
|
}
|
|
// This map lists the commands whose parameter strings are pinned; commands
|
|
// absent from it are not asserted (the lookup yields "" for them).
|
|
seen := map[string]bool{}
|
|
for _, command := range reg.PublicCommands() {
|
|
seen[command.Name] = true
|
|
if got := command.Parameters; got != expectedParameters[command.Name] {
|
|
t.Errorf("/%s parameters = %q, want %q", command.Name, got, expectedParameters[command.Name])
|
|
}
|
|
description := command.TelegramMenuDescription()
|
|
if strings.Contains(description, "Eg:") {
|
|
t.Errorf("/%s native menu description contains an example: %q", command.Name, description)
|
|
}
|
|
if strings.ContainsAny(description, "\r\n") {
|
|
t.Errorf("/%s native menu description is multiline: %q", command.Name, description)
|
|
}
|
|
if utf8.RuneCountInString(description) > telegramCommandDescriptionMaxRunesForTest {
|
|
t.Errorf("/%s native menu description exceeds Telegram limit: %d", command.Name, utf8.RuneCountInString(description))
|
|
}
|
|
}
|
|
|
|
// Every expectation must correspond to a registered command.
|
|
//
|
|
// Without this, an entry whose command stopped being registered simply
|
|
// stopped being checked: the forward loop only visits commands that exist,
|
|
// so removing a whole module from factories() left the suite green. That is
|
|
// also what makes the parameterless entries above load-bearing rather than
|
|
// decorative — "" == "" asserts nothing on its own, but the command having
|
|
// to exist at all does.
|
|
for name := range expectedParameters {
|
|
if !seen[name] {
|
|
t.Errorf("/%s is expected but not registered — a module dropped out of factories()", name)
|
|
}
|
|
}
|
|
|
|
help := moduleutil.RenderHelp(reg)
|
|
if utf8.RuneCountInString(help) > telegramMessageMaxRunesForTest {
|
|
t.Fatalf("/help source is %d characters, exceeds conservative Telegram limit %d", utf8.RuneCountInString(help), telegramMessageMaxRunesForTest)
|
|
}
|
|
}
|
|
|
|
const (
|
|
telegramCommandDescriptionMaxRunesForTest = 256
|
|
telegramMessageMaxRunesForTest = 4096
|
|
)
|
|
|
|
func TestBotCommandMenu_StockDividendContracts(t *testing.T) {
|
|
mod := stock.New(modules.Deps{Store: storage.NewMemoryProvider().Collection("stock")})
|
|
mod.Name = "stock"
|
|
got := botCommandMenu(&modules.Registry{Modules: []modules.Module{mod}})
|
|
|
|
commands := make(map[string]string, len(got))
|
|
for _, command := range got {
|
|
commands[command.Command] = command.Description
|
|
if len(command.Description) > 256 {
|
|
t.Fatalf("description for %s exceeds Telegram limit", command.Command)
|
|
}
|
|
}
|
|
for _, name := range []string{"stock_cash_dividend", "stock_share_dividend"} {
|
|
if commands[name] == "" {
|
|
t.Fatalf("stock menu missing %s: %v", name, commands)
|
|
}
|
|
}
|
|
if _, exists := commands["stock_dividend"]; exists {
|
|
t.Fatalf("retired stock_dividend remains in public menu: %v", commands)
|
|
}
|
|
if _, exists := commands["stock_bonus"]; exists {
|
|
t.Fatalf("stock_bonus remains in public menu: %v", commands)
|
|
}
|
|
}
|
|
|
|
func TestRegisterCommandMenu_CallsTelegramSetMyCommands(t *testing.T) {
|
|
reg := &modules.Registry{
|
|
Modules: []modules.Module{{
|
|
Name: "demo",
|
|
Commands: []modules.Command{{
|
|
Name: "demo",
|
|
Description: "Demo command",
|
|
Visibility: modules.VisibilityPublic,
|
|
}},
|
|
}},
|
|
}
|
|
rb := testutil.NewRecordingBot(t)
|
|
|
|
n, err := registerCommandMenu(context.Background(), rb.Bot, reg)
|
|
if err != nil {
|
|
t.Fatalf("registerCommandMenu: %v", err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("registered count = %d, want 1", n)
|
|
}
|
|
|
|
call := rb.LastSent()
|
|
if call.Method != "setMyCommands" {
|
|
t.Fatalf("method = %q, want setMyCommands", call.Method)
|
|
}
|
|
var cmds []models.BotCommand
|
|
if err := json.Unmarshal([]byte(call.Form["commands"]), &cmds); err != nil {
|
|
t.Fatalf("decode commands form field: %v; raw=%q", err, call.Form["commands"])
|
|
}
|
|
if len(cmds) != 1 || cmds[0].Command != "demo" || cmds[0].Description != "Demo command." {
|
|
t.Fatalf("commands payload = %+v, want demo command", cmds)
|
|
}
|
|
}
|