Skip to content

Commit 93eba34

Browse files
ernadoclaude
andcommitted
feat: add GetAvailableGifts
Returns the list of gifts that can be sent by the bot via payments.getStarGifts (no cache hash). Each *tg.StarGift maps to a Bot API Gift; upgraded collectible (StarGiftUnique) entries are skipped since the Bot API surface only describes regular, sendable gifts. Maps star_count, upgrade_star_count, total/remaining counts, per-user limits, unique-variant count, the default GiftBackground palette, and the publisher chat (resolved from the response's chats via chatFromRaw). The gift sticker is built with the existing stickerFromDocument helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 916deea commit 93eba34

2 files changed

Lines changed: 269 additions & 0 deletions

File tree

gifts.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"strconv"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
// GiftBackground describes the background palette of a gift.
11+
type GiftBackground struct {
12+
// CenterColor is the center color of the background, in RGB format.
13+
CenterColor int `json:"center_color"`
14+
// EdgeColor is the edge color of the background, in RGB format.
15+
EdgeColor int `json:"edge_color"`
16+
// TextColor is the color to use for text on the background, in RGB format.
17+
TextColor int `json:"text_color"`
18+
}
19+
20+
// Gift describes a gift that can be sent by the bot.
21+
type Gift struct {
22+
// ID is the unique identifier of the gift.
23+
ID string `json:"id"`
24+
// Sticker is the sticker that represents the gift.
25+
Sticker Sticker `json:"sticker"`
26+
// StarCount is the number of Telegram Stars that must be paid to send the
27+
// sticker.
28+
StarCount int `json:"star_count"`
29+
// UpgradeStarCount is the number of Telegram Stars that must be paid to
30+
// upgrade the gift to a unique one.
31+
UpgradeStarCount int `json:"upgrade_star_count,omitempty"`
32+
// IsPremium reports whether the gift can be purchased only by Telegram
33+
// Premium subscribers.
34+
IsPremium bool `json:"is_premium,omitempty"`
35+
// HasColors reports whether the gift can be used to generate a message color
36+
// palette.
37+
HasColors bool `json:"has_colors,omitempty"`
38+
// TotalCount is the total number of the gifts of this type that can be sent;
39+
// only for limited gifts.
40+
TotalCount int `json:"total_count,omitempty"`
41+
// RemainingCount is the number of remaining gifts of this type that can be
42+
// sent; only for limited gifts.
43+
RemainingCount int `json:"remaining_count,omitempty"`
44+
// PersonalTotalCount is the total number of the gifts of this type that can
45+
// be owned by a single user; only for limited-per-user gifts.
46+
PersonalTotalCount int `json:"personal_total_count,omitempty"`
47+
// PersonalRemainingCount is the number of remaining gifts of this type that
48+
// can be owned by the current user; only for limited-per-user gifts.
49+
PersonalRemainingCount int `json:"personal_remaining_count,omitempty"`
50+
// UniqueGiftVariantCount is the number of possible unique variants the gift
51+
// can be upgraded to.
52+
UniqueGiftVariantCount int `json:"unique_gift_variant_count,omitempty"`
53+
// Background is the default background of the gift.
54+
Background *GiftBackground `json:"background,omitempty"`
55+
// PublisherChat is the chat that published the gift, if any.
56+
PublisherChat *Chat `json:"publisher_chat,omitempty"`
57+
}
58+
59+
// GetAvailableGifts returns the list of gifts that can be sent by the bot.
60+
func (b *Bot) GetAvailableGifts(ctx context.Context) ([]Gift, error) {
61+
res, err := b.raw.PaymentsGetStarGifts(ctx, 0)
62+
if err != nil {
63+
return nil, asAPIError(err)
64+
}
65+
66+
gifts, ok := res.(*tg.PaymentsStarGifts)
67+
if !ok {
68+
// PaymentsStarGiftsNotModified only appears when a cache hash is sent,
69+
// which this method never does.
70+
return nil, &Error{Code: 500, Description: "Internal Server Error: unexpected gifts response"}
71+
}
72+
73+
chats := chatsByID(gifts.Chats)
74+
75+
out := make([]Gift, 0, len(gifts.Gifts))
76+
for _, g := range gifts.Gifts {
77+
// getStarGifts also returns upgraded (unique) collectibles; the Bot API
78+
// surface only describes regular, sendable gifts.
79+
raw, ok := g.(*tg.StarGift)
80+
if !ok {
81+
continue
82+
}
83+
84+
out = append(out, giftFromTg(raw, chats))
85+
}
86+
87+
return out, nil
88+
}
89+
90+
// giftFromTg converts a raw MTProto star gift into the Bot API Gift. chats
91+
// resolves the publisher peer.
92+
func giftFromTg(g *tg.StarGift, chats map[int64]tg.ChatClass) Gift {
93+
gift := Gift{
94+
ID: strconv.FormatInt(g.ID, 10),
95+
StarCount: int(g.Stars),
96+
IsPremium: g.RequirePremium,
97+
HasColors: g.PeerColorAvailable,
98+
}
99+
100+
if doc, ok := g.Sticker.(*tg.Document); ok {
101+
// Gift stickers carry no enclosing set; they are plain regular stickers.
102+
gift.Sticker = stickerFromDocument(doc, "", StickerRegular)
103+
}
104+
105+
if v, ok := g.GetUpgradeStars(); ok {
106+
gift.UpgradeStarCount = int(v)
107+
}
108+
109+
if v, ok := g.GetAvailabilityTotal(); ok {
110+
gift.TotalCount = v
111+
}
112+
113+
if v, ok := g.GetAvailabilityRemains(); ok {
114+
gift.RemainingCount = v
115+
}
116+
117+
if v, ok := g.GetPerUserTotal(); ok {
118+
gift.PersonalTotalCount = v
119+
}
120+
121+
if v, ok := g.GetPerUserRemains(); ok {
122+
gift.PersonalRemainingCount = v
123+
}
124+
125+
if v, ok := g.GetUpgradeVariants(); ok {
126+
gift.UniqueGiftVariantCount = v
127+
}
128+
129+
if bg, ok := g.GetBackground(); ok {
130+
gift.Background = &GiftBackground{
131+
CenterColor: bg.CenterColor,
132+
EdgeColor: bg.EdgeColor,
133+
TextColor: bg.TextColor,
134+
}
135+
}
136+
137+
if peer, ok := g.GetReleasedBy(); ok {
138+
if chat, ok := chatFromPublisher(peer, chats); ok {
139+
gift.PublisherChat = &chat
140+
}
141+
}
142+
143+
return gift
144+
}
145+
146+
// chatFromPublisher resolves a gift publisher peer into a Bot API Chat. Only
147+
// chat/channel publishers map onto publisher_chat.
148+
func chatFromPublisher(p tg.PeerClass, chats map[int64]tg.ChatClass) (Chat, bool) {
149+
switch p.(type) {
150+
case *tg.PeerChat, *tg.PeerChannel:
151+
return chatFromRaw(p, chats), true
152+
default:
153+
return Chat{}, false
154+
}
155+
}

gifts_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
func TestGetAvailableGifts(t *testing.T) {
11+
doc := &tg.Document{
12+
ID: 5,
13+
AccessHash: 6,
14+
FileReference: []byte{1},
15+
DCID: 2,
16+
Size: 2048,
17+
MimeType: mimeStickerAnimated,
18+
Attributes: []tg.DocumentAttributeClass{
19+
&tg.DocumentAttributeImageSize{W: 512, H: 512},
20+
&tg.DocumentAttributeSticker{Alt: "🎁", Stickerset: &tg.InputStickerSetEmpty{}},
21+
},
22+
}
23+
24+
limited := tg.StarGift{
25+
ID: 100,
26+
Sticker: doc,
27+
Stars: 50,
28+
Limited: true,
29+
RequirePremium: true,
30+
}
31+
limited.SetUpgradeStars(25)
32+
limited.SetAvailabilityTotal(1000)
33+
limited.SetAvailabilityRemains(900)
34+
limited.SetPerUserTotal(5)
35+
limited.SetPerUserRemains(3)
36+
limited.SetUpgradeVariants(7)
37+
limited.SetBackground(tg.StarGiftBackground{CenterColor: 0x112233, EdgeColor: 0x445566, TextColor: 0xffffff})
38+
limited.SetReleasedBy(&tg.PeerChannel{ChannelID: 777})
39+
40+
plain := tg.StarGift{
41+
ID: 200,
42+
Sticker: doc,
43+
Stars: 10,
44+
}
45+
46+
resp := &tg.PaymentsStarGifts{
47+
Gifts: []tg.StarGiftClass{
48+
&limited,
49+
&tg.StarGiftUnique{ID: 300}, // collectible, must be skipped
50+
&plain,
51+
},
52+
Chats: []tg.ChatClass{
53+
&tg.Channel{ID: 777, Title: "Publisher", Username: "pub", Broadcast: true, Photo: &tg.ChatPhotoEmpty{}},
54+
},
55+
}
56+
57+
inv := newMockInvoker()
58+
inv.reply(tg.PaymentsGetStarGiftsRequestTypeID, resp)
59+
60+
got, err := newMockBot(inv).GetAvailableGifts(context.Background())
61+
if err != nil {
62+
t.Fatalf("GetAvailableGifts: %v", err)
63+
}
64+
65+
if len(got) != 2 {
66+
t.Fatalf("len = %d, want 2 (unique gift skipped)", len(got))
67+
}
68+
69+
g := got[0]
70+
if g.ID != "100" || g.StarCount != 50 || g.UpgradeStarCount != 25 {
71+
t.Fatalf("counts: %#v", g)
72+
}
73+
74+
if !g.IsPremium {
75+
t.Fatalf("is_premium not set: %#v", g)
76+
}
77+
78+
if g.TotalCount != 1000 || g.RemainingCount != 900 {
79+
t.Fatalf("availability: %#v", g)
80+
}
81+
82+
if g.PersonalTotalCount != 5 || g.PersonalRemainingCount != 3 {
83+
t.Fatalf("per-user: %#v", g)
84+
}
85+
86+
if g.UniqueGiftVariantCount != 7 {
87+
t.Fatalf("variants: %#v", g)
88+
}
89+
90+
if g.Sticker.FileID == "" || g.Sticker.Emoji != "🎁" || !g.Sticker.IsAnimated {
91+
t.Fatalf("sticker: %#v", g.Sticker)
92+
}
93+
94+
if g.Background == nil || g.Background.CenterColor != 0x112233 || g.Background.TextColor != 0xffffff {
95+
t.Fatalf("background: %#v", g.Background)
96+
}
97+
98+
if g.PublisherChat == nil || g.PublisherChat.Title != "Publisher" || g.PublisherChat.Type != ChatTypeChannel {
99+
t.Fatalf("publisher: %#v", g.PublisherChat)
100+
}
101+
102+
plainGift := got[1]
103+
if plainGift.ID != "200" || plainGift.UpgradeStarCount != 0 || plainGift.Background != nil || plainGift.PublisherChat != nil {
104+
t.Fatalf("plain gift: %#v", plainGift)
105+
}
106+
107+
var req tg.PaymentsGetStarGiftsRequest
108+
109+
inv.decode(t, tg.PaymentsGetStarGiftsRequestTypeID, &req)
110+
111+
if req.Hash != 0 {
112+
t.Fatalf("hash = %d, want 0", req.Hash)
113+
}
114+
}

0 commit comments

Comments
 (0)