|
| 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