mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 03:13:46 +00:00
Nine commands mirroring the names @Stickers uses: /newpack, /mypack, /addsticker, /delsticker, /editsticker, /ordersticker, /setpackicon, /renamepack and /delpack, plus a confirm callback for the destructive one. Sources are replied stickers, photos or image documents; photos are downloaded, resampled to 512px and re-uploaded. One pack per user, keyed by owner id. Creating a pack is the only operation here that makes a durable, publicly linkable object on a user's behalf, so it is built around proving ownership rather than assuming it: - A name is claimed globally and create-only before Telegram is called. A pending record alone proves only that a caller *asked* for a name, which is exactly what someone naming a victim's public slug also does. - Adopting an existing set additionally requires that the claim predates this invocation. The claim lives in our store and the pack lives at Telegram, so a wiped store would otherwise make every pack adoptable. - Names are released only on positive evidence that no pack stands behind them, never on a generic failure, so a transient error cannot hand a live name to the next caller. - Ownership refusals are byte-identical across failure modes, so they cannot be used to probe which sets exist. Error classification is positive-only throughout: "the set is gone" and "nothing was created" are each proven from a specific Telegram response, never inferred from an error. Post-action commits run on a context detached from the request so a shutdown mid-handler cannot lose the record of something Telegram already did. Enabled explicitly via MODULES rather than by default.
120 lines
3.9 KiB
Go
120 lines
3.9 KiB
Go
package sticker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/go-telegram/bot"
|
|
)
|
|
|
|
const (
|
|
// maxSetNameLen is Telegram's cap on a sticker set's short name.
|
|
maxSetNameLen = 64
|
|
// maxTitleLen is Telegram's cap on a set title.
|
|
maxTitleLen = 64
|
|
// minSlugLen / maxSlugLen keep the share link readable and leave room for
|
|
// the "_by_<botusername>" suffix inside maxSetNameLen.
|
|
minSlugLen = 3
|
|
maxSlugLen = 40
|
|
)
|
|
|
|
// slugRe is the user-chosen half of a set name: 3-40 chars, starting with a
|
|
// letter. Telegram additionally forbids consecutive underscores, which a
|
|
// character class cannot express, so validateSlug checks that separately.
|
|
var slugRe = regexp.MustCompile(`^[a-z][a-z0-9_]{2,39}$`)
|
|
|
|
// validateSlug reports why a slug is unusable, or nil when it is fine.
|
|
//
|
|
// The slug is the one irreversible choice in this module: it fixes
|
|
// t.me/addstickers/<slug>_by_<bot> forever, because Telegram has no
|
|
// rename-short-name method. Rejecting loudly here is much cheaper than a user
|
|
// discovering the typo is permanent.
|
|
func validateSlug(slug string) error {
|
|
if !slugRe.MatchString(slug) {
|
|
return refuse(fmt.Sprintf("Pack name must be %d-%d characters: lowercase letters, digits and underscores, starting with a letter.", minSlugLen, maxSlugLen))
|
|
}
|
|
if strings.Contains(slug, "__") {
|
|
return refuse("Pack name cannot contain two underscores in a row.")
|
|
}
|
|
if strings.HasSuffix(slug, "_") {
|
|
return refuse("Pack name cannot end with an underscore.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// makeSetName builds the Telegram set name for a new pack. It is used only at
|
|
// creation — never to resolve ownership, which compares the *stored* name (see
|
|
// ownsSet).
|
|
//
|
|
// The error reports the remaining budget rather than only refusing, because the
|
|
// only fix available to the user is a shorter slug and the limit depends on the
|
|
// bot's username length, which they cannot see.
|
|
func makeSetName(slug, botUsername string) (string, error) {
|
|
if botUsername == "" {
|
|
return "", errNoUsername
|
|
}
|
|
suffix := "_by_" + botUsername
|
|
if len(slug)+len(suffix) > maxSetNameLen {
|
|
budget := maxSetNameLen - len(suffix)
|
|
if budget > maxSlugLen {
|
|
budget = maxSlugLen
|
|
}
|
|
return "", refuse(fmt.Sprintf("Pack name is too long for this bot — use at most %d characters.", budget))
|
|
}
|
|
return slug + suffix, nil
|
|
}
|
|
|
|
// ownsSet reports whether setName is the caller's pack, comparing
|
|
// case-insensitively against the *stored* Pack.Name.
|
|
//
|
|
// It deliberately does not re-derive the name from the live bot username.
|
|
// Renaming the bot in BotFather is supported and leaves existing set names
|
|
// untouched, so a derived comparison would make every user's own pack refuse as
|
|
// "not yours" while /mypack still displayed it. Comparing the stored name also
|
|
// sidesteps casing: Telegram returns SetName with whatever casing the set was
|
|
// created with.
|
|
func ownsSet(pack Pack, setName string) bool {
|
|
if pack.Name == "" || setName == "" {
|
|
return false
|
|
}
|
|
return strings.EqualFold(pack.Name, setName)
|
|
}
|
|
|
|
// usernameResolver caches the bot's username for building new set names.
|
|
//
|
|
// The bot starts with bot.WithSkipGetMe(), so nothing populates a username
|
|
// until this asks. Failures are never cached: a transient GetMe error must not
|
|
// disable /newpack for the process's lifetime.
|
|
type usernameResolver struct {
|
|
mu sync.Mutex
|
|
username string
|
|
}
|
|
|
|
// resolve returns the bot's username, calling GetMe at most once per success.
|
|
// It takes the handler's *bot.Bot rather than Deps.Bot, which is documented
|
|
// nil-safe and is nil under BuildOptions{}.
|
|
func (r *usernameResolver) resolve(ctx context.Context, b *bot.Bot) (string, error) {
|
|
r.mu.Lock()
|
|
cached := r.username
|
|
r.mu.Unlock()
|
|
if cached != "" {
|
|
return cached, nil
|
|
}
|
|
|
|
me, err := b.GetMe(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if me == nil || me.Username == "" {
|
|
return "", errNoUsername
|
|
}
|
|
|
|
r.mu.Lock()
|
|
r.username = me.Username
|
|
r.mu.Unlock()
|
|
return me.Username, nil
|
|
}
|