Skip to content

Commit be88b45

Browse files
ernadoclaude
andcommitted
feat(stickers): implement GetStickerSet and SetStickerSetThumb
Add the StickerSet type and a tg document -> Bot API Sticker converter, GetStickerSet (messages.getStickerSet) and SetStickerSetThumb (stickers.setStickerSetThumb, file_id or upload). Drop both from the conformance deferred list; only payments and games remain deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f6abd98 commit be88b45

5 files changed

Lines changed: 199 additions & 6 deletions

File tree

conformance_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ var deferredMethods = map[string]string{
2828
"sendGame": "games — deferred",
2929
"setGameScore": "games — deferred",
3030
"getGameHighScores": "games — deferred",
31-
"getStickerSet": "stickers — deferred (needs Sticker[] conversion)",
32-
"setStickerSetThumb": "stickers — deferred",
3331
}
3432

3533
// notApplicableMethods lists published methods that do not apply to the

sticker.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,21 @@ const (
2020
StickerFormatVideo StickerFormat = "video"
2121
)
2222

23-
// mimeStickerStatic is the MIME type used for static (and unknown) stickers.
24-
const mimeStickerStatic = "image/png"
23+
// Sticker MIME types.
24+
const (
25+
mimeStickerStatic = "image/png"
26+
mimeStickerAnimated = "application/x-tgsticker"
27+
mimeStickerVideo = "video/webm"
28+
)
2529

2630
// mimeType reports the MTProto MIME type for the sticker format. Static (and
2731
// any unknown) formats default to PNG.
2832
func (f StickerFormat) mimeType() string {
2933
switch f {
3034
case StickerFormatAnimated:
31-
return "application/x-tgsticker"
35+
return mimeStickerAnimated
3236
case StickerFormatVideo:
33-
return "video/webm"
37+
return mimeStickerVideo
3438
case StickerFormatStatic:
3539
return mimeStickerStatic
3640
default:

sticker_set.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
6+
"github.com/gotd/td/fileid"
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
// GetStickerSet returns a sticker set by its short name.
11+
func (b *Bot) GetStickerSet(ctx context.Context, name string) (*StickerSet, error) {
12+
res, err := b.raw.MessagesGetStickerSet(ctx, &tg.MessagesGetStickerSetRequest{
13+
Stickerset: &tg.InputStickerSetShortName{ShortName: name},
14+
Hash: 0,
15+
})
16+
if err != nil {
17+
return nil, asAPIError(err)
18+
}
19+
set, ok := res.(*tg.MessagesStickerSet)
20+
if !ok {
21+
return nil, &Error{Code: 400, Description: "Bad Request: sticker set not found"}
22+
}
23+
24+
typ := stickerSetType(set.Set)
25+
out := &StickerSet{
26+
Name: set.Set.ShortName,
27+
Title: set.Set.Title,
28+
StickerType: typ,
29+
}
30+
out.Stickers = make([]Sticker, 0, len(set.Documents))
31+
for _, d := range set.Documents {
32+
if doc, ok := d.(*tg.Document); ok {
33+
out.Stickers = append(out.Stickers, stickerFromDocument(doc, set.Set.ShortName, typ))
34+
}
35+
}
36+
if thumbs := set.Set.Thumbs; len(thumbs) > 0 {
37+
if ps := photoSizeFromThumb(thumbs[0]); ps != nil {
38+
out.Thumbnail = ps
39+
}
40+
}
41+
return out, nil
42+
}
43+
44+
// SetStickerSetThumb sets the thumbnail of a sticker set owned by the bot. The
45+
// thumbnail is referenced by file_id or uploaded.
46+
func (b *Bot) SetStickerSetThumb(ctx context.Context, name string, thumb InputFile) error {
47+
var doc tg.InputDocumentClass
48+
switch f := thumb.(type) {
49+
case InputFileID:
50+
d, err := inputDocumentFromFileID(string(f))
51+
if err != nil {
52+
return err
53+
}
54+
doc = d
55+
case *InputFileUpload:
56+
uploaded, err := b.uploadStickerDocument(ctx, f, StickerFormatStatic)
57+
if err != nil {
58+
return err
59+
}
60+
doc = &tg.InputDocument{ID: uploaded.ID, AccessHash: uploaded.AccessHash, FileReference: uploaded.FileReference}
61+
default:
62+
return &Error{Code: 400, Description: "Bad Request: thumbnail must be a file_id or an uploaded file"}
63+
}
64+
65+
if _, err := b.raw.StickersSetStickerSetThumb(ctx, &tg.StickersSetStickerSetThumbRequest{
66+
Stickerset: &tg.InputStickerSetShortName{ShortName: name},
67+
Thumb: doc,
68+
}); err != nil {
69+
return asAPIError(err)
70+
}
71+
return nil
72+
}
73+
74+
// stickerSetType reports the Bot API sticker type of a set.
75+
func stickerSetType(set tg.StickerSet) StickerType {
76+
switch {
77+
case set.Masks:
78+
return StickerMask
79+
case set.Emojis:
80+
return StickerCustomEmoji
81+
default:
82+
return StickerRegular
83+
}
84+
}
85+
86+
// stickerFromDocument converts a sticker document into the Bot API Sticker. typ
87+
// is the type of the enclosing set.
88+
func stickerFromDocument(d *tg.Document, setName string, typ StickerType) Sticker {
89+
fileID, uniqueID := encodeFileID(fileid.FromDocument(d))
90+
s := Sticker{
91+
FileID: fileID,
92+
FileUniqueID: uniqueID,
93+
Type: typ,
94+
SetName: setName,
95+
FileSize: int(d.Size),
96+
IsAnimated: d.MimeType == mimeStickerAnimated,
97+
IsVideo: d.MimeType == mimeStickerVideo,
98+
}
99+
for _, attr := range d.Attributes {
100+
switch a := attr.(type) {
101+
case *tg.DocumentAttributeImageSize:
102+
s.Width, s.Height = a.W, a.H
103+
case *tg.DocumentAttributeVideo:
104+
s.Width, s.Height = a.W, a.H
105+
case *tg.DocumentAttributeSticker:
106+
s.Emoji = a.Alt
107+
}
108+
}
109+
if len(d.Thumbs) > 0 {
110+
s.Thumbnail = photoSizeFromThumb(d.Thumbs[0])
111+
}
112+
return s
113+
}
114+
115+
// photoSizeFromThumb converts a document/sticker thumbnail into a PhotoSize.
116+
// File ids for thumbnails are not derivable from the document alone, so only the
117+
// dimensions and size are filled.
118+
func photoSizeFromThumb(t tg.PhotoSizeClass) *PhotoSize {
119+
type sized interface {
120+
GetW() int
121+
GetH() int
122+
}
123+
s, ok := t.(sized)
124+
if !ok {
125+
return nil
126+
}
127+
ps := &PhotoSize{Width: s.GetW(), Height: s.GetH()}
128+
if base, ok := t.(*tg.PhotoSize); ok {
129+
ps.FileSize = base.Size
130+
}
131+
return ps
132+
}

sticker_set_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package botapi
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gotd/td/tg"
7+
)
8+
9+
func TestStickerFromDocument(t *testing.T) {
10+
d := &tg.Document{
11+
ID: 5,
12+
AccessHash: 6,
13+
FileReference: []byte{1},
14+
DCID: 2,
15+
Size: 4096,
16+
MimeType: mimeStickerVideo,
17+
Attributes: []tg.DocumentAttributeClass{
18+
&tg.DocumentAttributeImageSize{W: 512, H: 512},
19+
&tg.DocumentAttributeSticker{Alt: "🎉"},
20+
},
21+
}
22+
s := stickerFromDocument(d, "myset", StickerRegular)
23+
if s.FileID == "" || s.FileUniqueID == "" {
24+
t.Fatalf("file ids: %#v", s)
25+
}
26+
if s.Width != 512 || s.Height != 512 {
27+
t.Fatalf("dimensions: %#v", s)
28+
}
29+
if s.Emoji != "🎉" || s.SetName != "myset" || s.Type != StickerRegular {
30+
t.Fatalf("meta: %#v", s)
31+
}
32+
if !s.IsVideo || s.IsAnimated {
33+
t.Fatalf("video flags: %#v", s)
34+
}
35+
if s.FileSize != 4096 {
36+
t.Fatalf("size: %d", s.FileSize)
37+
}
38+
}
39+
40+
func TestStickerSetType(t *testing.T) {
41+
if stickerSetType(tg.StickerSet{Masks: true}) != StickerMask {
42+
t.Fatal("masks -> mask")
43+
}
44+
if stickerSetType(tg.StickerSet{Emojis: true}) != StickerCustomEmoji {
45+
t.Fatal("emojis -> custom_emoji")
46+
}
47+
if stickerSetType(tg.StickerSet{}) != StickerRegular {
48+
t.Fatal("default -> regular")
49+
}
50+
}

types_media.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,12 @@ type UserProfilePhotos struct {
105105
TotalCount int `json:"total_count"`
106106
Photos [][]PhotoSize `json:"photos"`
107107
}
108+
109+
// StickerSet represents a set of stickers.
110+
type StickerSet struct {
111+
Name string `json:"name"`
112+
Title string `json:"title"`
113+
StickerType StickerType `json:"sticker_type"`
114+
Stickers []Sticker `json:"stickers"`
115+
Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
116+
}

0 commit comments

Comments
 (0)