Skip to content

Commit 4844f4e

Browse files
ernadoclaude
andcommitted
feat: add GetUserGifts, GetChatGifts, GetBusinessAccountGifts
Lists gifts owned by a user, a chat, or a managed business account via payments.getSavedStarGifts (the business variant wrapped in invokeWithBusinessConnection, peer inputPeerSelf). Shared GiftListOption set maps the exclude/sort/pagination filters onto the request. Adds the full OwnedGift union (OwnedGiftRegular / OwnedGiftUnique) and the UniqueGift type tree (model/symbol/backdrop with rarity), converted from the MTProto SavedStarGift / StarGiftUnique attributes. Each listed gift mints the owned_gift_id consumed by the convert/upgrade/transfer methods: the message form for user gifts, the chat form (channel id + access hash + saved id) for chat gifts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d451e59 commit 4844f4e

4 files changed

Lines changed: 776 additions & 0 deletions

File tree

owned_gift.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ func OwnedGiftFromSlug(slug string) string {
3535
return ownedGiftSlugPrefix + ":" + slug
3636
}
3737

38+
// ownedGiftFromChannel builds an owned_gift_id for a gift received by a channel
39+
// chat, from the channel's id and access hash and the gift's saved id.
40+
func ownedGiftFromChannel(channelID, accessHash, savedID int64) string {
41+
return ownedGiftChatPrefix + ":" +
42+
strconv.FormatInt(channelID, 10) + ":" +
43+
strconv.FormatInt(accessHash, 10) + ":" +
44+
strconv.FormatInt(savedID, 10)
45+
}
46+
3847
// ownedGiftToTg decodes an owned_gift_id into the MTProto inputSavedStarGift it
3948
// addresses.
4049
func ownedGiftToTg(id string) (tg.InputSavedStarGiftClass, error) {

owned_gifts.go

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"strconv"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
// giftListConfig holds the optional filters of the gift-listing methods.
11+
type giftListConfig struct {
12+
offset string
13+
limit int
14+
sortByPrice bool
15+
excludeUnsaved bool
16+
excludeSaved bool
17+
excludeUnlimited bool
18+
excludeUnique bool
19+
excludeUpgradable bool
20+
excludeNonUpgradable bool
21+
}
22+
23+
// GiftListOption customizes the gift-listing methods (GetUserGifts,
24+
// GetChatGifts, GetBusinessAccountGifts).
25+
type GiftListOption func(*giftListConfig)
26+
27+
// WithGiftListOffset sets the pagination offset, taken from a previous result's
28+
// NextOffset.
29+
func WithGiftListOffset(offset string) GiftListOption {
30+
return func(c *giftListConfig) { c.offset = offset }
31+
}
32+
33+
// WithGiftListLimit caps the number of gifts returned.
34+
func WithGiftListLimit(limit int) GiftListOption {
35+
return func(c *giftListConfig) { c.limit = limit }
36+
}
37+
38+
// WithGiftListSortByPrice sorts the gifts by price instead of by reception date.
39+
func WithGiftListSortByPrice() GiftListOption {
40+
return func(c *giftListConfig) { c.sortByPrice = true }
41+
}
42+
43+
// WithGiftListExcludeUnsaved excludes gifts not displayed on the profile.
44+
func WithGiftListExcludeUnsaved() GiftListOption {
45+
return func(c *giftListConfig) { c.excludeUnsaved = true }
46+
}
47+
48+
// WithGiftListExcludeSaved excludes gifts displayed on the profile.
49+
func WithGiftListExcludeSaved() GiftListOption {
50+
return func(c *giftListConfig) { c.excludeSaved = true }
51+
}
52+
53+
// WithGiftListExcludeUnlimited excludes gifts that can be bought unlimited times.
54+
func WithGiftListExcludeUnlimited() GiftListOption {
55+
return func(c *giftListConfig) { c.excludeUnlimited = true }
56+
}
57+
58+
// WithGiftListExcludeUnique excludes unique (collectible) gifts.
59+
func WithGiftListExcludeUnique() GiftListOption {
60+
return func(c *giftListConfig) { c.excludeUnique = true }
61+
}
62+
63+
// WithGiftListExcludeUpgradable excludes limited gifts that can be upgraded.
64+
func WithGiftListExcludeUpgradable() GiftListOption {
65+
return func(c *giftListConfig) { c.excludeUpgradable = true }
66+
}
67+
68+
// WithGiftListExcludeNonUpgradable excludes limited gifts that cannot be
69+
// upgraded.
70+
func WithGiftListExcludeNonUpgradable() GiftListOption {
71+
return func(c *giftListConfig) { c.excludeNonUpgradable = true }
72+
}
73+
74+
// request builds the MTProto request for the given peer from the filters.
75+
func (c giftListConfig) request(peer tg.InputPeerClass) *tg.PaymentsGetSavedStarGiftsRequest {
76+
return &tg.PaymentsGetSavedStarGiftsRequest{
77+
Peer: peer,
78+
Offset: c.offset,
79+
Limit: c.limit,
80+
SortByValue: c.sortByPrice,
81+
ExcludeUnsaved: c.excludeUnsaved,
82+
ExcludeSaved: c.excludeSaved,
83+
ExcludeUnlimited: c.excludeUnlimited,
84+
ExcludeUnique: c.excludeUnique,
85+
ExcludeUpgradable: c.excludeUpgradable,
86+
ExcludeUnupgradable: c.excludeNonUpgradable,
87+
}
88+
}
89+
90+
// GetUserGifts returns the gifts owned and hosted by a user.
91+
func (b *Bot) GetUserGifts(ctx context.Context, userID int64, opts ...GiftListOption) (OwnedGifts, error) {
92+
user, err := b.resolveInputUser(ctx, userID)
93+
if err != nil {
94+
return OwnedGifts{}, err
95+
}
96+
97+
peer := inputPeerFromUser(user)
98+
if peer == nil {
99+
return OwnedGifts{}, &Error{Code: 400, Description: "Bad Request: can't resolve user"}
100+
}
101+
102+
res, err := b.raw.PaymentsGetSavedStarGifts(ctx, giftListFrom(opts).request(peer))
103+
if err != nil {
104+
return OwnedGifts{}, asAPIError(err)
105+
}
106+
107+
return b.ownedGiftsFromTg(res, 0, 0), nil
108+
}
109+
110+
// GetChatGifts returns the gifts owned by a chat.
111+
func (b *Bot) GetChatGifts(ctx context.Context, chat ChatID, opts ...GiftListOption) (OwnedGifts, error) {
112+
peer, err := b.resolveInputPeer(ctx, chat)
113+
if err != nil {
114+
return OwnedGifts{}, err
115+
}
116+
117+
channel, ok := peer.(*tg.InputPeerChannel)
118+
if !ok {
119+
return OwnedGifts{}, &Error{Code: 400, Description: "Bad Request: chat is not a supergroup or channel"}
120+
}
121+
122+
res, err := b.raw.PaymentsGetSavedStarGifts(ctx, giftListFrom(opts).request(peer))
123+
if err != nil {
124+
return OwnedGifts{}, asAPIError(err)
125+
}
126+
127+
return b.ownedGiftsFromTg(res, channel.ChannelID, channel.AccessHash), nil
128+
}
129+
130+
// GetBusinessAccountGifts returns the gifts received and owned by a managed
131+
// business account. Requires the can_view_gifts_and_stars business bot right.
132+
func (b *Bot) GetBusinessAccountGifts(ctx context.Context, businessConnectionID string, opts ...GiftListOption) (OwnedGifts, error) {
133+
var res tg.PaymentsSavedStarGifts
134+
135+
req := giftListFrom(opts).request(&tg.InputPeerSelf{})
136+
if err := b.invokeBusiness(ctx, businessConnectionID, req, &res); err != nil {
137+
return OwnedGifts{}, asAPIError(err)
138+
}
139+
140+
return b.ownedGiftsFromTg(&res, 0, 0), nil
141+
}
142+
143+
// inputPeerFromUser adapts a resolved input user to an input peer for the
144+
// saved-gifts request.
145+
func inputPeerFromUser(u tg.InputUserClass) tg.InputPeerClass {
146+
switch u := u.(type) {
147+
case *tg.InputUser:
148+
return &tg.InputPeerUser{UserID: u.UserID, AccessHash: u.AccessHash}
149+
case *tg.InputUserSelf:
150+
return &tg.InputPeerSelf{}
151+
default:
152+
return nil
153+
}
154+
}
155+
156+
// giftListFrom collapses the options into a config.
157+
func giftListFrom(opts []GiftListOption) giftListConfig {
158+
var cfg giftListConfig
159+
160+
for _, opt := range opts {
161+
opt(&cfg)
162+
}
163+
164+
return cfg
165+
}
166+
167+
// ownedGiftsFromTg converts a saved-star-gifts response into the Bot API
168+
// OwnedGifts. ownerChannelID/ownerHash, when non-zero, are used to mint the
169+
// owned_gift_id of chat-owned gifts.
170+
func (b *Bot) ownedGiftsFromTg(res *tg.PaymentsSavedStarGifts, ownerChannelID, ownerHash int64) OwnedGifts {
171+
users := usersByID(res.Users)
172+
chats := chatsByID(res.Chats)
173+
174+
out := OwnedGifts{
175+
TotalCount: res.Count,
176+
NextOffset: res.NextOffset,
177+
Gifts: make([]OwnedGift, 0, len(res.Gifts)),
178+
}
179+
180+
for i := range res.Gifts {
181+
out.Gifts = append(out.Gifts, ownedGiftFromTg(res.Gifts[i], users, chats, ownerChannelID, ownerHash))
182+
}
183+
184+
return out
185+
}
186+
187+
// ownedGiftFromTg converts a single saved star gift into an OwnedGift.
188+
func ownedGiftFromTg(
189+
sg tg.SavedStarGift, users map[int64]*tg.User, chats map[int64]tg.ChatClass, ownerChannelID, ownerHash int64,
190+
) OwnedGift {
191+
id := mintOwnedGiftID(sg, ownerChannelID, ownerHash)
192+
sender := senderUser(sg, users)
193+
194+
switch gift := sg.Gift.(type) {
195+
case *tg.StarGiftUnique:
196+
owned := OwnedGiftUnique{
197+
Type: ownedGiftUnique,
198+
Gift: uniqueGiftFromTg(gift, chats),
199+
OwnedGiftID: id,
200+
SenderUser: sender,
201+
SendDate: sg.Date,
202+
IsSaved: !sg.Unsaved,
203+
}
204+
205+
if v, ok := sg.GetTransferStars(); ok {
206+
owned.TransferStarCount = int(v)
207+
}
208+
209+
if v, ok := sg.GetCanTransferAt(); ok {
210+
owned.CanBeTransferred = true
211+
owned.NextTransferDate = v
212+
}
213+
214+
return owned
215+
default:
216+
return regularOwnedGift(sg, id, sender, chats)
217+
}
218+
}
219+
220+
// regularOwnedGift builds an OwnedGiftRegular from a saved gift. A non-StarGift
221+
// payload yields an empty Gift.
222+
func regularOwnedGift(sg tg.SavedStarGift, id string, sender *User, chats map[int64]tg.ChatClass) OwnedGift {
223+
owned := OwnedGiftRegular{
224+
Type: ownedGiftRegular,
225+
OwnedGiftID: id,
226+
SenderUser: sender,
227+
SendDate: sg.Date,
228+
IsPrivate: sg.NameHidden,
229+
IsSaved: !sg.Unsaved,
230+
CanBeUpgraded: sg.CanUpgrade,
231+
WasRefunded: sg.Refunded,
232+
}
233+
234+
if g, ok := sg.Gift.(*tg.StarGift); ok {
235+
owned.Gift = giftFromTg(g, chats)
236+
}
237+
238+
if msg, ok := sg.GetMessage(); ok {
239+
owned.Text = msg.Text
240+
owned.Entities = entitiesFromTg(msg.Entities)
241+
}
242+
243+
if v, ok := sg.GetConvertStars(); ok {
244+
owned.ConvertStarCount = int(v)
245+
}
246+
247+
if v, ok := sg.GetUpgradeStars(); ok {
248+
owned.PrepaidUpgradeStarCount = int(v)
249+
}
250+
251+
return owned
252+
}
253+
254+
// mintOwnedGiftID derives the manageable owned_gift_id for a saved gift, or ""
255+
// when the gift cannot be addressed (e.g. a user gift without a message id).
256+
func mintOwnedGiftID(sg tg.SavedStarGift, ownerChannelID, ownerHash int64) string {
257+
if msgID, ok := sg.GetMsgID(); ok {
258+
return OwnedGiftFromMessage(msgID)
259+
}
260+
261+
if savedID, ok := sg.GetSavedID(); ok && ownerChannelID != 0 {
262+
return ownedGiftFromChannel(ownerChannelID, ownerHash, savedID)
263+
}
264+
265+
return ""
266+
}
267+
268+
// senderUser resolves the gift sender into a Bot API User.
269+
func senderUser(sg tg.SavedStarGift, users map[int64]*tg.User) *User {
270+
from, ok := sg.GetFromID()
271+
if !ok {
272+
return nil
273+
}
274+
275+
pu, ok := from.(*tg.PeerUser)
276+
if !ok {
277+
return nil
278+
}
279+
280+
var user User
281+
282+
if u, ok := users[pu.UserID]; ok {
283+
user = userFromTgUser(u)
284+
} else {
285+
user = User{ID: pu.UserID}
286+
}
287+
288+
return &user
289+
}
290+
291+
// uniqueGiftFromTg converts a collectible star gift into the Bot API UniqueGift.
292+
func uniqueGiftFromTg(g *tg.StarGiftUnique, chats map[int64]tg.ChatClass) UniqueGift {
293+
out := UniqueGift{
294+
GiftID: strconv.FormatInt(g.GiftID, 10),
295+
BaseName: g.Title,
296+
Name: g.Slug,
297+
Number: g.Num,
298+
IsPremium: g.RequirePremium,
299+
IsBurned: g.Burned,
300+
}
301+
302+
for _, attr := range g.Attributes {
303+
switch a := attr.(type) {
304+
case *tg.StarGiftAttributeModel:
305+
out.Model = giftModelFromTg(a)
306+
case *tg.StarGiftAttributePattern:
307+
out.Symbol = giftSymbolFromTg(a)
308+
case *tg.StarGiftAttributeBackdrop:
309+
out.Backdrop = giftBackdropFromTg(a)
310+
}
311+
}
312+
313+
if peer, ok := g.GetReleasedBy(); ok {
314+
if chat, ok := chatFromPublisher(peer, chats); ok {
315+
out.PublisherChat = &chat
316+
}
317+
}
318+
319+
return out
320+
}
321+
322+
// giftModelFromTg converts a model attribute, including its sticker and rarity.
323+
func giftModelFromTg(a *tg.StarGiftAttributeModel) UniqueGiftModel {
324+
perMille, rarity := rarityValues(a.Rarity)
325+
326+
m := UniqueGiftModel{Name: a.Name, RarityPerMille: perMille, Rarity: rarity}
327+
if doc, ok := a.Document.(*tg.Document); ok {
328+
m.Sticker = stickerFromDocument(doc, "", StickerRegular)
329+
}
330+
331+
return m
332+
}
333+
334+
// giftSymbolFromTg converts a pattern attribute into a unique gift symbol.
335+
func giftSymbolFromTg(a *tg.StarGiftAttributePattern) UniqueGiftSymbol {
336+
perMille, _ := rarityValues(a.Rarity)
337+
338+
s := UniqueGiftSymbol{Name: a.Name, RarityPerMille: perMille}
339+
if doc, ok := a.Document.(*tg.Document); ok {
340+
s.Sticker = stickerFromDocument(doc, "", StickerRegular)
341+
}
342+
343+
return s
344+
}
345+
346+
// giftBackdropFromTg converts a backdrop attribute into a unique gift backdrop.
347+
func giftBackdropFromTg(a *tg.StarGiftAttributeBackdrop) UniqueGiftBackdrop {
348+
perMille, _ := rarityValues(a.Rarity)
349+
350+
return UniqueGiftBackdrop{
351+
Name: a.Name,
352+
RarityPerMille: perMille,
353+
Colors: UniqueGiftBackdropColors{
354+
CenterColor: a.CenterColor,
355+
EdgeColor: a.EdgeColor,
356+
SymbolColor: a.PatternColor,
357+
TextColor: a.TextColor,
358+
},
359+
}
360+
}
361+
362+
// rarityValues extracts the per-mille count and rarity name from a gift rarity.
363+
// The per-mille variant carries the count; the named variants carry the name.
364+
func rarityValues(r tg.StarGiftAttributeRarityClass) (perMille int, name string) {
365+
switch r := r.(type) {
366+
case *tg.StarGiftAttributeRarity:
367+
return r.Permille, ""
368+
case *tg.StarGiftAttributeRarityUncommon:
369+
return 0, "uncommon"
370+
case *tg.StarGiftAttributeRarityRare:
371+
return 0, "rare"
372+
case *tg.StarGiftAttributeRarityEpic:
373+
return 0, "epic"
374+
case *tg.StarGiftAttributeRarityLegendary:
375+
return 0, "legendary"
376+
default:
377+
return 0, ""
378+
}
379+
}

0 commit comments

Comments
 (0)