Skip to content

Commit d8e56f6

Browse files
ernadoclaude
andcommitted
feat: add sticker set/sticker editing methods
DeleteStickerSet, SetStickerSetTitle (stickers.renameStickerSet), SetStickerEmojiList/SetStickerKeywords/SetStickerMaskPosition (stickers.changeSticker), ReplaceStickerInSet (stickers.replaceSticker) and GetCustomEmojiStickers (messages.getCustomEmojiDocuments). Adds the MaskPosition type. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 50a4192 commit d8e56f6

2 files changed

Lines changed: 343 additions & 0 deletions

File tree

sticker_edit.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"strconv"
6+
"strings"
7+
8+
"github.com/gotd/td/tg"
9+
)
10+
11+
// MaskPosition describes the position on faces where a mask sticker is placed.
12+
type MaskPosition struct {
13+
// Point is the part of the face relative to which the mask is placed: one of
14+
// "forehead", "eyes", "mouth" or "chin".
15+
Point string `json:"point"`
16+
// XShift is the horizontal shift, in widths of the mask scaled to the face
17+
// size, left to right. Negative shifts the mask left.
18+
XShift float64 `json:"x_shift"`
19+
// YShift is the vertical shift, in heights of the mask scaled to the face
20+
// size, top to bottom. Negative shifts the mask up.
21+
YShift float64 `json:"y_shift"`
22+
// Scale is the mask scaling coefficient.
23+
Scale float64 `json:"scale"`
24+
}
25+
26+
// maskPoints maps a Bot API mask point to the MTProto MaskCoords.n index.
27+
var maskPoints = map[string]int{
28+
"forehead": 0,
29+
"eyes": 1,
30+
"mouth": 2,
31+
"chin": 3,
32+
}
33+
34+
// toMaskCoords converts the Bot API mask position to MTProto mask coordinates.
35+
func (m MaskPosition) toMaskCoords() (tg.MaskCoords, error) {
36+
n, ok := maskPoints[m.Point]
37+
if !ok {
38+
return tg.MaskCoords{}, &Error{Code: 400, Description: "Bad Request: invalid mask position point"}
39+
}
40+
41+
return tg.MaskCoords{N: n, X: m.XShift, Y: m.YShift, Zoom: m.Scale}, nil
42+
}
43+
44+
// DeleteStickerSet deletes a sticker set created by the bot, referenced by its
45+
// short name.
46+
func (b *Bot) DeleteStickerSet(ctx context.Context, name string) error {
47+
if _, err := b.raw.StickersDeleteStickerSet(ctx, &tg.InputStickerSetShortName{ShortName: name}); err != nil {
48+
return asAPIError(err)
49+
}
50+
51+
return nil
52+
}
53+
54+
// SetStickerSetTitle sets the title of a sticker set created by the bot.
55+
func (b *Bot) SetStickerSetTitle(ctx context.Context, name, title string) error {
56+
if _, err := b.raw.StickersRenameStickerSet(ctx, &tg.StickersRenameStickerSetRequest{
57+
Stickerset: &tg.InputStickerSetShortName{ShortName: name},
58+
Title: title,
59+
}); err != nil {
60+
return asAPIError(err)
61+
}
62+
63+
return nil
64+
}
65+
66+
// SetStickerEmojiList changes the list of emoji assigned to a sticker created by
67+
// the bot, referenced by file_id.
68+
func (b *Bot) SetStickerEmojiList(ctx context.Context, sticker string, emojiList []string) error {
69+
doc, err := inputDocumentFromFileID(sticker)
70+
if err != nil {
71+
return err
72+
}
73+
74+
req := &tg.StickersChangeStickerRequest{Sticker: doc}
75+
req.SetEmoji(strings.Join(emojiList, ""))
76+
77+
if _, err := b.raw.StickersChangeSticker(ctx, req); err != nil {
78+
return asAPIError(err)
79+
}
80+
81+
return nil
82+
}
83+
84+
// SetStickerKeywords changes the search keywords assigned to a sticker created
85+
// by the bot, referenced by file_id.
86+
func (b *Bot) SetStickerKeywords(ctx context.Context, sticker string, keywords []string) error {
87+
doc, err := inputDocumentFromFileID(sticker)
88+
if err != nil {
89+
return err
90+
}
91+
92+
req := &tg.StickersChangeStickerRequest{Sticker: doc}
93+
req.SetKeywords(strings.Join(keywords, ","))
94+
95+
if _, err := b.raw.StickersChangeSticker(ctx, req); err != nil {
96+
return asAPIError(err)
97+
}
98+
99+
return nil
100+
}
101+
102+
// SetStickerMaskPosition changes the mask position of a mask sticker created by
103+
// the bot, referenced by file_id. A nil position clears the mask position.
104+
func (b *Bot) SetStickerMaskPosition(ctx context.Context, sticker string, position *MaskPosition) error {
105+
doc, err := inputDocumentFromFileID(sticker)
106+
if err != nil {
107+
return err
108+
}
109+
110+
req := &tg.StickersChangeStickerRequest{Sticker: doc}
111+
112+
if position != nil {
113+
coords, err := position.toMaskCoords()
114+
if err != nil {
115+
return err
116+
}
117+
118+
req.SetMaskCoords(coords)
119+
}
120+
121+
if _, err := b.raw.StickersChangeSticker(ctx, req); err != nil {
122+
return asAPIError(err)
123+
}
124+
125+
return nil
126+
}
127+
128+
// ReplaceStickerInSet replaces an existing sticker (referenced by file_id) in a
129+
// set created by the bot with a new one, keeping its position.
130+
func (b *Bot) ReplaceStickerInSet(ctx context.Context, oldSticker string, sticker InputSticker) error {
131+
doc, err := inputDocumentFromFileID(oldSticker)
132+
if err != nil {
133+
return err
134+
}
135+
136+
item, err := b.resolveStickerItem(ctx, sticker)
137+
if err != nil {
138+
return err
139+
}
140+
141+
if _, err := b.raw.StickersReplaceSticker(ctx, &tg.StickersReplaceStickerRequest{
142+
Sticker: doc,
143+
NewSticker: item,
144+
}); err != nil {
145+
return asAPIError(err)
146+
}
147+
148+
return nil
149+
}
150+
151+
// GetCustomEmojiStickers returns information about custom emoji stickers by their
152+
// identifiers (at most 200).
153+
func (b *Bot) GetCustomEmojiStickers(ctx context.Context, customEmojiIDs []string) ([]Sticker, error) {
154+
if len(customEmojiIDs) == 0 {
155+
return nil, nil
156+
}
157+
158+
ids := make([]int64, 0, len(customEmojiIDs))
159+
160+
for _, s := range customEmojiIDs {
161+
id, err := strconv.ParseInt(s, 10, 64)
162+
if err != nil {
163+
return nil, &Error{Code: 400, Description: "Bad Request: invalid custom_emoji_id"}
164+
}
165+
166+
ids = append(ids, id)
167+
}
168+
169+
docs, err := b.raw.MessagesGetCustomEmojiDocuments(ctx, ids)
170+
if err != nil {
171+
return nil, asAPIError(err)
172+
}
173+
174+
out := make([]Sticker, 0, len(docs))
175+
176+
for _, d := range docs {
177+
if doc, ok := d.(*tg.Document); ok {
178+
out = append(out, stickerFromDocument(doc, "", StickerCustomEmoji))
179+
}
180+
}
181+
182+
return out, nil
183+
}

sticker_edit_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
func TestDeleteStickerSet(t *testing.T) {
11+
inv := newMockInvoker()
12+
inv.reply(tg.StickersDeleteStickerSetRequestTypeID, &tg.BoolTrue{})
13+
14+
if err := newMockBot(inv).DeleteStickerSet(context.Background(), "s"); err != nil {
15+
t.Fatalf("DeleteStickerSet: %v", err)
16+
}
17+
18+
if !inv.called(tg.StickersDeleteStickerSetRequestTypeID) {
19+
t.Fatal("delete not called")
20+
}
21+
}
22+
23+
func TestSetStickerSetTitle(t *testing.T) {
24+
inv := newMockInvoker()
25+
inv.reply(tg.StickersRenameStickerSetRequestTypeID, &tg.MessagesStickerSet{Set: tg.StickerSet{ShortName: "s"}})
26+
27+
if err := newMockBot(inv).SetStickerSetTitle(context.Background(), "s", "New Title"); err != nil {
28+
t.Fatalf("SetStickerSetTitle: %v", err)
29+
}
30+
31+
var req tg.StickersRenameStickerSetRequest
32+
33+
inv.decode(t, tg.StickersRenameStickerSetRequestTypeID, &req)
34+
35+
if req.Title != "New Title" {
36+
t.Fatalf("title = %q", req.Title)
37+
}
38+
}
39+
40+
func TestSetStickerEmojiList(t *testing.T) {
41+
fid := documentFileID(t, 0x55)
42+
43+
inv := newMockInvoker()
44+
inv.reply(tg.StickersChangeStickerRequestTypeID, &tg.MessagesStickerSet{Set: tg.StickerSet{ShortName: "s"}})
45+
46+
if err := newMockBot(inv).SetStickerEmojiList(context.Background(), fid, []string{"😀", "😎"}); err != nil {
47+
t.Fatalf("SetStickerEmojiList: %v", err)
48+
}
49+
50+
var req tg.StickersChangeStickerRequest
51+
52+
inv.decode(t, tg.StickersChangeStickerRequestTypeID, &req)
53+
54+
if got, ok := req.GetEmoji(); !ok || got != "😀😎" {
55+
t.Fatalf("emoji = %q, ok=%v", got, ok)
56+
}
57+
}
58+
59+
func TestSetStickerKeywords(t *testing.T) {
60+
fid := documentFileID(t, 0x56)
61+
62+
inv := newMockInvoker()
63+
inv.reply(tg.StickersChangeStickerRequestTypeID, &tg.MessagesStickerSet{Set: tg.StickerSet{ShortName: "s"}})
64+
65+
if err := newMockBot(inv).SetStickerKeywords(context.Background(), fid, []string{"a", "b"}); err != nil {
66+
t.Fatalf("SetStickerKeywords: %v", err)
67+
}
68+
69+
var req tg.StickersChangeStickerRequest
70+
71+
inv.decode(t, tg.StickersChangeStickerRequestTypeID, &req)
72+
73+
if got, ok := req.GetKeywords(); !ok || got != "a,b" {
74+
t.Fatalf("keywords = %q, ok=%v", got, ok)
75+
}
76+
}
77+
78+
func TestSetStickerMaskPosition(t *testing.T) {
79+
fid := documentFileID(t, 0x57)
80+
81+
inv := newMockInvoker()
82+
inv.reply(tg.StickersChangeStickerRequestTypeID, &tg.MessagesStickerSet{Set: tg.StickerSet{ShortName: "s"}})
83+
84+
pos := &MaskPosition{Point: "eyes", XShift: 0.1, YShift: -0.2, Scale: 2.0}
85+
if err := newMockBot(inv).SetStickerMaskPosition(context.Background(), fid, pos); err != nil {
86+
t.Fatalf("SetStickerMaskPosition: %v", err)
87+
}
88+
89+
var req tg.StickersChangeStickerRequest
90+
91+
inv.decode(t, tg.StickersChangeStickerRequestTypeID, &req)
92+
93+
coords, ok := req.GetMaskCoords()
94+
if !ok || coords.N != 1 || coords.Zoom != 2.0 {
95+
t.Fatalf("coords = %#v, ok=%v", coords, ok)
96+
}
97+
}
98+
99+
func TestSetStickerMaskPositionInvalidPoint(t *testing.T) {
100+
fid := documentFileID(t, 0x58)
101+
102+
err := newMockBot(newMockInvoker()).SetStickerMaskPosition(context.Background(), fid, &MaskPosition{Point: "nose"})
103+
if err == nil {
104+
t.Fatal("expected error for invalid point")
105+
}
106+
}
107+
108+
func TestReplaceStickerInSet(t *testing.T) {
109+
fid := documentFileID(t, 0x59)
110+
newFid := documentFileID(t, 0x5a)
111+
112+
inv := newMockInvoker()
113+
inv.reply(tg.StickersReplaceStickerRequestTypeID, &tg.MessagesStickerSet{Set: tg.StickerSet{ShortName: "s"}})
114+
115+
sticker := InputSticker{Sticker: FileID(newFid), Format: StickerFormatStatic, EmojiList: []string{"😀"}}
116+
if err := newMockBot(inv).ReplaceStickerInSet(context.Background(), fid, sticker); err != nil {
117+
t.Fatalf("ReplaceStickerInSet: %v", err)
118+
}
119+
120+
var req tg.StickersReplaceStickerRequest
121+
122+
inv.decode(t, tg.StickersReplaceStickerRequestTypeID, &req)
123+
124+
if req.NewSticker.Emoji != "😀" {
125+
t.Fatalf("new sticker = %#v", req.NewSticker)
126+
}
127+
}
128+
129+
func TestGetCustomEmojiStickers(t *testing.T) {
130+
inv := newMockInvoker()
131+
inv.reply(tg.MessagesGetCustomEmojiDocumentsRequestTypeID, &tg.DocumentClassVector{
132+
Elems: []tg.DocumentClass{
133+
&tg.Document{
134+
ID: 1,
135+
AccessHash: 2,
136+
MimeType: "image/webp",
137+
Attributes: []tg.DocumentAttributeClass{
138+
&tg.DocumentAttributeSticker{Alt: "😀", Stickerset: &tg.InputStickerSetEmpty{}},
139+
},
140+
},
141+
},
142+
})
143+
144+
stickers, err := newMockBot(inv).GetCustomEmojiStickers(context.Background(), []string{"12345"})
145+
if err != nil {
146+
t.Fatalf("GetCustomEmojiStickers: %v", err)
147+
}
148+
149+
if len(stickers) != 1 || stickers[0].Type != StickerCustomEmoji || stickers[0].Emoji != "😀" {
150+
t.Fatalf("stickers = %#v", stickers)
151+
}
152+
153+
var req tg.MessagesGetCustomEmojiDocumentsRequest
154+
155+
inv.decode(t, tg.MessagesGetCustomEmojiDocumentsRequestTypeID, &req)
156+
157+
if len(req.DocumentID) != 1 || req.DocumentID[0] != 12345 {
158+
t.Fatalf("ids = %v", req.DocumentID)
159+
}
160+
}

0 commit comments

Comments
 (0)