mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-14 02:16:31 +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.
124 lines
4.9 KiB
Go
124 lines
4.9 KiB
Go
package sticker
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/go-telegram/bot"
|
|
"github.com/go-telegram/bot/models"
|
|
|
|
"github.com/tiennm99/miti99bot/internal/log"
|
|
)
|
|
|
|
// userError carries text meant to be shown to the user verbatim.
|
|
//
|
|
// The module has two kinds of failure and must never confuse them: a refusal
|
|
// the user can act on ("that pack name is too long"), and an internal failure
|
|
// that must not reach a reply at all — plan rule 5 exists because a transport
|
|
// error's text can embed the bot token. Wrapping the first kind in a distinct
|
|
// type makes "is this safe to echo?" a type question instead of a judgement
|
|
// call at each call site.
|
|
type userError struct{ msg string }
|
|
|
|
func (e userError) Error() string { return e.msg }
|
|
|
|
// refuse builds a userError. Its text is replied verbatim, so write it as a
|
|
// sentence addressed to the user.
|
|
func refuse(msg string) error { return userError{msg: msg} }
|
|
|
|
// errNoUsername means the bot's own username is unavailable, so no new set name
|
|
// can be built. Internal, not user-facing: nothing the caller does fixes it.
|
|
var errNoUsername = errors.New("sticker: bot has no username")
|
|
|
|
// isStickerSetMissing reports whether err positively says the set does not
|
|
// exist on Telegram's side.
|
|
//
|
|
// Classification here is positive-only, and deliberately so: this is the one
|
|
// signal that authorises deleting a user's pack record. A network blip, a 429,
|
|
// or a context cancelled by SIGTERM must never be read as "the pack is gone" —
|
|
// under one pack per user that would destroy the only record of a live pack and
|
|
// block /newpack until the phantom cleared.
|
|
func isStickerSetMissing(err error) bool {
|
|
return errors.Is(err, bot.ErrorBadRequest) &&
|
|
strings.Contains(err.Error(), "STICKERSET_INVALID")
|
|
}
|
|
|
|
// apiRefusal maps a Telegram API error to user-facing text, or returns ok=false
|
|
// when the error has no specific meaning and should be treated as a failure.
|
|
//
|
|
// Matching is on MTProto code substrings rather than prose. The Bot API server
|
|
// rewrites only three of these into English (PACK_SHORT_NAME_OCCUPIED,
|
|
// PACK_SHORT_NAME_INVALID, STICKER_EMOJI_INVALID); the rest arrive as
|
|
// "Bad Request: <CODE>", and the prose for the three could change without
|
|
// notice. Both forms are matched where they differ.
|
|
func apiRefusal(err error) (string, bool) {
|
|
if err == nil {
|
|
return "", false
|
|
}
|
|
text := err.Error()
|
|
switch {
|
|
case contains(text, "PACK_SHORT_NAME_OCCUPIED", "already occupied"):
|
|
return "That pack name is taken. Pick a different one.", true
|
|
case contains(text, "PACK_SHORT_NAME_INVALID", "invalid sticker set name"):
|
|
return "Telegram rejected that pack name. Use lowercase letters, digits and single underscores.", true
|
|
case contains(text, "PACK_TITLE_INVALID"):
|
|
return "Telegram rejected that title. Try a shorter, simpler one.", true
|
|
case contains(text, "STICKERSET_INVALID"):
|
|
return "Your pack no longer exists on Telegram. Use /newpack to create a new one.", true
|
|
case contains(text, "STICKERS_TOO_MUCH"):
|
|
return "Your pack is full (120 stickers).", true
|
|
case contains(text, "STICKER_EMOJI_INVALID", "invalid sticker emojis"):
|
|
return "Telegram rejected those emoji. Try different ones.", true
|
|
case contains(text, "too many emoji specified"):
|
|
return "At most 20 emoji per sticker.", true
|
|
case contains(text, "STICKER_PNG_DIMENSIONS", "STICKER_DIMENSIONS_INVALID"):
|
|
return "Telegram rejected that image's dimensions.", true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// replyAPIError converts a Telegram API error into a reply. Errors with no
|
|
// specific mapping are logged and answered generically — the raw error never
|
|
// reaches the user.
|
|
func replyAPIError(ctx context.Context, b *bot.Bot, msg *models.Message, op string, err error) error {
|
|
if text, ok := apiRefusal(err); ok {
|
|
return reply(ctx, b, msg, text)
|
|
}
|
|
log.Error(op, "err", err)
|
|
return reply(ctx, b, msg, genericFailure)
|
|
}
|
|
|
|
func contains(text string, needles ...string) bool {
|
|
for _, n := range needles {
|
|
if strings.Contains(text, n) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// createRefused reports whether err proves CreateNewStickerSet created nothing.
|
|
//
|
|
// Deliberately separate from apiRefusal even though today their code lists
|
|
// overlap. apiRefusal's job is "map an error to user-facing text"; this one's is
|
|
// "prove no set exists", which is what authorises releasing a name reservation
|
|
// and dropping a write-ahead intent. Reusing apiRefusal for both would mean the
|
|
// next person adding a code there for wording reasons silently converts it into
|
|
// a strand-the-slug bug.
|
|
//
|
|
// Every code here is a request-validation refusal: Telegram rejected the call
|
|
// before creating anything.
|
|
func createRefused(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
return contains(err.Error(),
|
|
"PACK_SHORT_NAME_OCCUPIED", "already occupied",
|
|
"PACK_SHORT_NAME_INVALID", "invalid sticker set name",
|
|
"PACK_TITLE_INVALID",
|
|
"STICKER_EMOJI_INVALID", "invalid sticker emojis",
|
|
"too many emoji specified",
|
|
)
|
|
}
|