Skip to content

Commit 4f452a8

Browse files
ernadoclaude
andcommitted
feat: add SendGift
Sends a gift (by id from GetAvailableGifts) to a user or channel chat via the two-step Telegram Stars payment flow: payments.getPaymentForm with an inputInvoiceStarGift, then payments.sendStarsForm with the returned form id. Functional options cover the optional gift text (WithGiftText, with parse mode via WithGiftParseMode or explicit WithGiftEntities) and pay-for-upgrade (WithGiftPayForUpgrade). Text is resolved through the existing styledMessage / entitiesToTg helpers into a TextWithEntities. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 93eba34 commit 4f452a8

2 files changed

Lines changed: 191 additions & 0 deletions

File tree

send_gift.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+
"strconv"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
// giftConfig holds the optional parameters of SendGift.
11+
type giftConfig struct {
12+
text string
13+
parseMode ParseMode
14+
entities []MessageEntity
15+
payForUpgrade bool
16+
}
17+
18+
// GiftOption customizes SendGift.
19+
type GiftOption func(*giftConfig)
20+
21+
// WithGiftText attaches a text message to the gift.
22+
func WithGiftText(text string) GiftOption {
23+
return func(c *giftConfig) { c.text = text }
24+
}
25+
26+
// WithGiftParseMode sets the parse mode used for the gift text.
27+
func WithGiftParseMode(mode ParseMode) GiftOption {
28+
return func(c *giftConfig) { c.parseMode = mode }
29+
}
30+
31+
// WithGiftEntities sets explicit entities for the gift text, overriding the
32+
// parse mode.
33+
func WithGiftEntities(entities []MessageEntity) GiftOption {
34+
return func(c *giftConfig) { c.entities = entities }
35+
}
36+
37+
// WithGiftPayForUpgrade pays for the gift to be eligible for an upgrade to a
38+
// unique (collectible) gift by the receiver.
39+
func WithGiftPayForUpgrade() GiftOption {
40+
return func(c *giftConfig) { c.payForUpgrade = true }
41+
}
42+
43+
// SendGift sends a gift, identified by an id from GetAvailableGifts, to a user
44+
// or channel chat. The gift can't be converted to Telegram Stars by the
45+
// receiver.
46+
func (b *Bot) SendGift(ctx context.Context, target ChatID, giftID string, opts ...GiftOption) error {
47+
var cfg giftConfig
48+
49+
for _, opt := range opts {
50+
opt(&cfg)
51+
}
52+
53+
id, err := strconv.ParseInt(giftID, 10, 64)
54+
if err != nil {
55+
return &Error{Code: 400, Description: "Bad Request: invalid gift_id"}
56+
}
57+
58+
peer, err := b.resolveInputPeer(ctx, target)
59+
if err != nil {
60+
return err
61+
}
62+
63+
invoice := &tg.InputInvoiceStarGift{
64+
Peer: peer,
65+
GiftID: id,
66+
IncludeUpgrade: cfg.payForUpgrade,
67+
}
68+
69+
if cfg.text != "" {
70+
text, entities, err := b.giftMessage(ctx, cfg)
71+
if err != nil {
72+
return err
73+
}
74+
75+
invoice.SetMessage(tg.TextWithEntities{Text: text, Entities: entities})
76+
}
77+
78+
return b.payStarsForm(ctx, invoice)
79+
}
80+
81+
// giftMessage resolves the gift text into a (text, entities) pair: explicit
82+
// entities take precedence over the parse mode.
83+
func (b *Bot) giftMessage(ctx context.Context, cfg giftConfig) (string, []tg.MessageEntityClass, error) {
84+
if len(cfg.entities) > 0 {
85+
return cfg.text, entitiesToTg(cfg.entities), nil
86+
}
87+
88+
return b.styledMessage(ctx, cfg.text, cfg.parseMode)
89+
}
90+
91+
// payStarsForm runs the two-step Telegram Stars payment flow for an invoice:
92+
// fetch the payment form, then submit it.
93+
func (b *Bot) payStarsForm(ctx context.Context, invoice tg.InputInvoiceClass) error {
94+
form, err := b.raw.PaymentsGetPaymentForm(ctx, &tg.PaymentsGetPaymentFormRequest{
95+
Invoice: invoice,
96+
})
97+
if err != nil {
98+
return asAPIError(err)
99+
}
100+
101+
stars, ok := form.(*tg.PaymentsPaymentFormStarGift)
102+
if !ok {
103+
return &Error{Code: 500, Description: "Internal Server Error: unexpected payment form"}
104+
}
105+
106+
if _, err := b.raw.PaymentsSendStarsForm(ctx, &tg.PaymentsSendStarsFormRequest{
107+
FormID: stars.FormID,
108+
Invoice: invoice,
109+
}); err != nil {
110+
return asAPIError(err)
111+
}
112+
113+
return nil
114+
}

send_gift_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
func TestSendGift(t *testing.T) {
11+
inv := newMockInvoker()
12+
inv.reply(tg.PaymentsGetPaymentFormRequestTypeID, &tg.PaymentsPaymentFormStarGift{
13+
FormID: 999,
14+
Invoice: tg.Invoice{Currency: "XTR"},
15+
})
16+
inv.reply(tg.PaymentsSendStarsFormRequestTypeID, &tg.PaymentsPaymentResult{
17+
Updates: &tg.Updates{},
18+
})
19+
20+
bot := newMockBot(inv)
21+
22+
err := bot.SendGift(context.Background(), userRef(42, 7), "100",
23+
WithGiftText("thanks!"), WithGiftPayForUpgrade())
24+
if err != nil {
25+
t.Fatalf("SendGift: %v", err)
26+
}
27+
28+
var form tg.PaymentsGetPaymentFormRequest
29+
30+
inv.decode(t, tg.PaymentsGetPaymentFormRequestTypeID, &form)
31+
32+
invoice, ok := form.Invoice.(*tg.InputInvoiceStarGift)
33+
if !ok {
34+
t.Fatalf("invoice = %#v, want star gift", form.Invoice)
35+
}
36+
37+
if invoice.GiftID != 100 {
38+
t.Fatalf("gift id = %d, want 100", invoice.GiftID)
39+
}
40+
41+
if !invoice.IncludeUpgrade {
42+
t.Fatalf("include upgrade not set")
43+
}
44+
45+
if invoice.Message.Text != "thanks!" {
46+
t.Fatalf("message = %q", invoice.Message.Text)
47+
}
48+
49+
if _, ok := invoice.Peer.(*tg.InputPeerUser); !ok {
50+
t.Fatalf("peer = %#v, want user", invoice.Peer)
51+
}
52+
53+
var send tg.PaymentsSendStarsFormRequest
54+
55+
inv.decode(t, tg.PaymentsSendStarsFormRequestTypeID, &send)
56+
57+
if send.FormID != 999 {
58+
t.Fatalf("form id = %d, want 999", send.FormID)
59+
}
60+
61+
if _, ok := send.Invoice.(*tg.InputInvoiceStarGift); !ok {
62+
t.Fatalf("send invoice = %#v, want star gift", send.Invoice)
63+
}
64+
}
65+
66+
func TestSendGiftInvalidID(t *testing.T) {
67+
inv := newMockInvoker()
68+
69+
err := newMockBot(inv).SendGift(context.Background(), userRef(42, 7), "not-a-number")
70+
if err == nil {
71+
t.Fatal("expected error for invalid gift id")
72+
}
73+
74+
if inv.count() != 0 {
75+
t.Fatalf("made %d RPC calls, want 0", inv.count())
76+
}
77+
}

0 commit comments

Comments
 (0)