Files
tiennm99bot/internal/modules/sticker/setpackicon.go
T
tiennm99 3751010b8e feat(sticker): add sticker pack module
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.
2026-08-25 15:54:28 +07:00

57 lines
1.6 KiB
Go

package sticker
import (
"bytes"
"context"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
)
// handleSetPackIcon sets the pack thumbnail from a sticker already in the pack.
//
// The sticker's own file_id cannot simply be handed to Telegram: a pack
// thumbnail must be exactly 100x100, which a 512px sticker is not. So the image
// is fetched, resized, and uploaded as a new file.
func (s *state) handleSetPackIcon(ctx context.Context, b *bot.Bot, update *models.Update) error {
ctx, cancel := handlerContext(ctx)
defer cancel()
msg := update.Message
ownerID, err := senderID(msg)
if err != nil {
return reply(ctx, b, msg, senderRefusal)
}
owned, err := s.resolveOwned(ctx, msg, ownerID)
if err != nil {
return replyErr(ctx, b, msg, "sticker_setpackicon_resolve", err)
}
// Reserve the reply tail before the slow leg. See mediaContext.
mediaCtx, cancelMedia := mediaContext(ctx)
defer cancelMedia()
raw, err := downloadFile(mediaCtx, b, owned.fileID)
if err != nil {
return replyErr(ctx, b, msg, "sticker_setpackicon_download", err)
}
thumb, err := toThumbnailPNG(raw)
if err != nil {
return replyErr(ctx, b, msg, "sticker_setpackicon_resize", err)
}
if _, err := b.SetStickerSetThumbnail(ctx, &bot.SetStickerSetThumbnailParams{
Name: owned.pack.Name,
UserID: ownerID,
Thumbnail: &models.InputFileUpload{Filename: "thumb.png", Data: bytes.NewReader(thumb)},
Format: stickerFormatStatic,
}); err != nil {
if isStickerSetMissing(err) {
s.dropPackRecord(ctx, ownerID)
}
return replyAPIError(ctx, b, msg, "sticker_setpackicon", err)
}
return reply(ctx, b, msg, "Pack icon updated.")
}