mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 12:28:54 +00:00
/alias <name> saves a replied message under a name and /insert <name> sends it back; /aliases lists every name and /unalias deletes one. Every Telegram format is supported — sticker, photo, GIF, video, video note, audio, voice, document, plain text — and each is kept as the file_id Telegram already issued, so nothing is downloaded and an alias survives redeploys. The namespace is global and the last assignment wins, matching the shared sticker pack; /unalias is open to anyone for the same reason. A saved name also works as its own command: /cheer rather than /insert cheer. This needs two new seams in the module contract. Module.Fallback handles a /command no module registered, and the dispatcher installs it after every Command — the bot library returns the first matching handler, so code always beats a name resolved at runtime, including an alias that shares a command added in a later build. /alias refuses a name already in the registry for the same reason, since such an alias would only reach /insert. An unknown command stays silent: the fallback sees every unrecognised /foo in every chat, so replying would make typos noisy and would confirm which names exist. Module.Inline answers inline-mode queries — "@botname <prefix>" from any chat, filtered by prefix and capped at Telegram's 50 results. Each result is a cached inline type carrying the stored file_id, so the picker renders real previews without an upload. Video notes are omitted because Telegram defines no InlineQueryResultCachedVideoNote and substituting a plain video would change what was saved. Inline mode must be enabled in BotFather before Telegram delivers these updates. Both slots are single-occupancy with conflict detection at Build. Auth.Permits learns the inline sender so a gated inline handler would not deny everyone. Build's command indexing and slot claiming move into addCommands/addSingletons, keeping it under the project's cyclomatic cap. Also restores the sticker module: /addsticker moves back out of util, which has no store, into internal/modules/sticker as its only command.
128 lines
4.4 KiB
Go
128 lines
4.4 KiB
Go
package sticker
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/go-telegram/bot/models"
|
|
)
|
|
|
|
// hasMovingSource picks which of the two conversion paths a reply takes, from
|
|
// the message alone. Getting it wrong sends a video at the image resampler, or
|
|
// an image at ffmpeg.
|
|
func TestHasMovingSource(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
replied *models.Message
|
|
want bool
|
|
}{
|
|
{name: "nil reply", replied: nil, want: false},
|
|
{name: "animation", replied: &models.Message{Animation: &models.Animation{FileID: "a"}}, want: true},
|
|
{name: "video", replied: &models.Message{Video: &models.Video{FileID: "v"}}, want: true},
|
|
{name: "video note", replied: &models.Message{VideoNote: &models.VideoNote{FileID: "n"}}, want: true},
|
|
{name: "gif document", replied: &models.Message{Document: &models.Document{MimeType: "image/gif"}}, want: true},
|
|
{name: "mp4 document", replied: &models.Message{Document: &models.Document{MimeType: "video/mp4"}}, want: true},
|
|
{name: "webm document", replied: &models.Message{Document: &models.Document{MimeType: "video/webm"}}, want: true},
|
|
{name: "mov document", replied: &models.Message{Document: &models.Document{MimeType: "video/quicktime"}}, want: true},
|
|
{name: "png document", replied: &models.Message{Document: &models.Document{MimeType: "image/png"}}, want: false},
|
|
{name: "photo", replied: &models.Message{Photo: []models.PhotoSize{{FileID: "p"}}}, want: false},
|
|
{name: "sticker", replied: &models.Message{Sticker: &models.Sticker{FileID: "s"}}, want: false},
|
|
{name: "pdf document", replied: &models.Message{Document: &models.Document{MimeType: "application/pdf"}}, want: false},
|
|
{name: "plain text", replied: &models.Message{Text: "hello"}, want: false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := hasMovingSource(tc.replied); got != tc.want {
|
|
t.Errorf("hasMovingSource = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Source selection happens before any download, so an oversized clip costs
|
|
// nothing.
|
|
func TestVideoFileID(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
replied *models.Message
|
|
want string
|
|
ok bool
|
|
}{
|
|
{
|
|
name: "animation",
|
|
replied: &models.Message{Animation: &models.Animation{FileID: "anim", FileSize: 1 << 20}},
|
|
want: "anim", ok: true,
|
|
},
|
|
{
|
|
name: "video",
|
|
replied: &models.Message{Video: &models.Video{FileID: "vid", FileSize: 1 << 20}},
|
|
want: "vid", ok: true,
|
|
},
|
|
{
|
|
name: "video note",
|
|
replied: &models.Message{VideoNote: &models.VideoNote{FileID: "note", FileSize: 1 << 20}},
|
|
want: "note", ok: true,
|
|
},
|
|
{
|
|
name: "document",
|
|
replied: &models.Message{Document: &models.Document{FileID: "doc", MimeType: "video/mp4"}},
|
|
want: "doc", ok: true,
|
|
},
|
|
{
|
|
// Animation is checked before Video: Telegram attaches both to a
|
|
// GIF-style message, and the animation is the one to convert.
|
|
name: "animation wins over video",
|
|
replied: &models.Message{
|
|
Animation: &models.Animation{FileID: "anim"},
|
|
Video: &models.Video{FileID: "vid"},
|
|
},
|
|
want: "anim", ok: true,
|
|
},
|
|
{
|
|
name: "oversized animation refused",
|
|
replied: &models.Message{Animation: &models.Animation{FileID: "anim", FileSize: maxVideoSourceBytes + 1}},
|
|
ok: false,
|
|
},
|
|
{
|
|
name: "oversized video refused",
|
|
replied: &models.Message{Video: &models.Video{FileID: "vid", FileSize: maxVideoSourceBytes + 1}},
|
|
ok: false,
|
|
},
|
|
{
|
|
name: "oversized document refused",
|
|
replied: &models.Message{Document: &models.Document{FileID: "doc", MimeType: "video/mp4", FileSize: maxVideoSourceBytes + 1}},
|
|
ok: false,
|
|
},
|
|
{
|
|
name: "nothing usable",
|
|
replied: &models.Message{Text: "hello"},
|
|
ok: false,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := videoFileID(tc.replied)
|
|
if tc.ok {
|
|
if err != nil {
|
|
t.Fatalf("videoFileID: %v", err)
|
|
}
|
|
if got != tc.want {
|
|
t.Errorf("file_id = %q, want %q", got, tc.want)
|
|
}
|
|
return
|
|
}
|
|
if err == nil {
|
|
t.Fatalf("videoFileID = %q, want a refusal", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The video cap is the one on the source, and it is an order of magnitude above
|
|
// the image cap on purpose — the 256 KB sticker limit applies to the transcode
|
|
// output, not the input.
|
|
func TestVideoSourceCapExceedsImageCap(t *testing.T) {
|
|
if maxVideoSourceBytes <= maxSourceBytes {
|
|
t.Errorf("maxVideoSourceBytes (%d) must exceed maxSourceBytes (%d)", maxVideoSourceBytes, maxSourceBytes)
|
|
}
|
|
}
|