Skip to content

Commit c74277c

Browse files
ernadoclaude
andcommitted
feat: add subscription invite link methods
CreateChatSubscriptionInviteLink and EditChatSubscriptionInviteLink over messages.exportChatInvite / editExportedChatInvite, with subscription pricing surfaced on ChatInviteLink. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0d7c11c commit c74277c

3 files changed

Lines changed: 196 additions & 0 deletions

File tree

chat_member.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ type ChatInviteLink struct {
9090
ExpireDate int `json:"expire_date,omitempty"`
9191
MemberLimit int `json:"member_limit,omitempty"`
9292
PendingJoinRequestCount int `json:"pending_join_request_count,omitempty"`
93+
// SubscriptionPeriod is the number of seconds the subscription is active
94+
// before the next payment, for subscription invite links.
95+
SubscriptionPeriod int `json:"subscription_period,omitempty"`
96+
// SubscriptionPrice is the amount of Telegram Stars a user must pay initially
97+
// and after each subsequent subscription period to join via the link.
98+
SubscriptionPrice int `json:"subscription_price,omitempty"`
9399
}
94100

95101
// ChatMemberUpdated represents a change in the status of a chat member.

subscription_links.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
6+
"github.com/gotd/td/telegram/peers"
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
// errUnexpectedInvite is returned when an invite RPC answers with an unexpected
11+
// constructor.
12+
func errUnexpectedInvite() *Error {
13+
return &Error{Code: 500, Description: "Internal Server Error: unexpected invite response"}
14+
}
15+
16+
// inviteLinkName resolves the optional name from invite-link options, ignoring
17+
// the other fields that subscription links do not support.
18+
func inviteLinkName(opts []InviteLinkOption) string {
19+
var o peers.ExportLinkOptions
20+
21+
for _, opt := range opts {
22+
opt(&o)
23+
}
24+
25+
return o.Title
26+
}
27+
28+
// inviteLinkFromExported converts an MTProto exported invite into the Bot API
29+
// type. The bot is always the creator of the links it makes.
30+
func (b *Bot) inviteLinkFromExported(inv *tg.ChatInviteExported) *ChatInviteLink {
31+
out := &ChatInviteLink{
32+
InviteLink: inv.Link,
33+
Creator: userFromTgUser(b.self),
34+
CreatesJoinRequest: inv.RequestNeeded,
35+
IsPrimary: inv.Permanent,
36+
IsRevoked: inv.Revoked,
37+
Name: inv.Title,
38+
ExpireDate: inv.ExpireDate,
39+
MemberLimit: inv.UsageLimit,
40+
PendingJoinRequestCount: inv.Requested,
41+
}
42+
43+
if pricing, ok := inv.GetSubscriptionPricing(); ok {
44+
out.SubscriptionPeriod = pricing.Period
45+
out.SubscriptionPrice = int(pricing.Amount)
46+
}
47+
48+
return out
49+
}
50+
51+
// CreateChatSubscriptionInviteLink creates a subscription invite link for a
52+
// channel chat. Users joining through the link pay subscriptionPrice Telegram
53+
// Stars every subscriptionPeriod seconds (currently only 2592000, 30 days, is
54+
// supported). The bot must have the can_invite_users administrator right.
55+
func (b *Bot) CreateChatSubscriptionInviteLink(
56+
ctx context.Context, chat ChatID, subscriptionPeriod, subscriptionPrice int, opts ...InviteLinkOption,
57+
) (*ChatInviteLink, error) {
58+
peer, err := b.resolveInputPeer(ctx, chat)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
req := &tg.MessagesExportChatInviteRequest{Peer: peer}
64+
req.SetSubscriptionPricing(tg.StarsSubscriptionPricing{
65+
Period: subscriptionPeriod,
66+
Amount: int64(subscriptionPrice),
67+
})
68+
69+
if name := inviteLinkName(opts); name != "" {
70+
req.SetTitle(name)
71+
}
72+
73+
res, err := b.raw.MessagesExportChatInvite(ctx, req)
74+
if err != nil {
75+
return nil, asAPIError(err)
76+
}
77+
78+
exported, ok := res.(*tg.ChatInviteExported)
79+
if !ok {
80+
return nil, errUnexpectedInvite()
81+
}
82+
83+
return b.inviteLinkFromExported(exported), nil
84+
}
85+
86+
// EditChatSubscriptionInviteLink edits the name of a subscription invite link
87+
// created by the bot. The price and period of a subscription link cannot be
88+
// changed.
89+
func (b *Bot) EditChatSubscriptionInviteLink(
90+
ctx context.Context, chat ChatID, inviteLink string, opts ...InviteLinkOption,
91+
) (*ChatInviteLink, error) {
92+
peer, err := b.resolveInputPeer(ctx, chat)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
req := &tg.MessagesEditExportedChatInviteRequest{Peer: peer, Link: inviteLink}
98+
req.SetTitle(inviteLinkName(opts))
99+
100+
res, err := b.raw.MessagesEditExportedChatInvite(ctx, req)
101+
if err != nil {
102+
return nil, asAPIError(err)
103+
}
104+
105+
edited, ok := res.(*tg.MessagesExportedChatInvite)
106+
if !ok {
107+
return nil, errUnexpectedInvite()
108+
}
109+
110+
exported, ok := edited.Invite.(*tg.ChatInviteExported)
111+
if !ok {
112+
return nil, errUnexpectedInvite()
113+
}
114+
115+
return b.inviteLinkFromExported(exported), nil
116+
}

subscription_links_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
func TestCreateChatSubscriptionInviteLink(t *testing.T) {
11+
inv := newMockInvoker()
12+
inv.reply(tg.MessagesExportChatInviteRequestTypeID, &tg.ChatInviteExported{
13+
Link: "https://t.me/+sub",
14+
Title: "Subs",
15+
SubscriptionPricing: tg.StarsSubscriptionPricing{
16+
Period: 2592000,
17+
Amount: 100,
18+
},
19+
})
20+
21+
b := newMockBot(inv)
22+
23+
link, err := b.CreateChatSubscriptionInviteLink(
24+
context.Background(), tdlibChannel(50), 2592000, 100, WithInviteLinkName("Subs"),
25+
)
26+
if err != nil {
27+
t.Fatalf("CreateChatSubscriptionInviteLink: %v", err)
28+
}
29+
30+
if link.SubscriptionPeriod != 2592000 || link.SubscriptionPrice != 100 || link.Name != "Subs" {
31+
t.Fatalf("link = %#v", link)
32+
}
33+
34+
var req tg.MessagesExportChatInviteRequest
35+
36+
inv.decode(t, tg.MessagesExportChatInviteRequestTypeID, &req)
37+
38+
pricing, ok := req.GetSubscriptionPricing()
39+
if !ok || pricing.Period != 2592000 || pricing.Amount != 100 {
40+
t.Fatalf("pricing = %#v, ok=%v", pricing, ok)
41+
}
42+
43+
if title, ok := req.GetTitle(); !ok || title != "Subs" {
44+
t.Fatalf("title = %q, ok=%v", title, ok)
45+
}
46+
}
47+
48+
func TestEditChatSubscriptionInviteLink(t *testing.T) {
49+
inv := newMockInvoker()
50+
inv.reply(tg.MessagesEditExportedChatInviteRequestTypeID, &tg.MessagesExportedChatInvite{
51+
Invite: &tg.ChatInviteExported{Link: "https://t.me/+sub", Title: "Renamed"},
52+
})
53+
54+
b := newMockBot(inv)
55+
56+
link, err := b.EditChatSubscriptionInviteLink(
57+
context.Background(), tdlibChannel(50), "https://t.me/+sub", WithInviteLinkName("Renamed"),
58+
)
59+
if err != nil {
60+
t.Fatalf("EditChatSubscriptionInviteLink: %v", err)
61+
}
62+
63+
if link.Name != "Renamed" {
64+
t.Fatalf("link = %#v", link)
65+
}
66+
67+
var req tg.MessagesEditExportedChatInviteRequest
68+
69+
inv.decode(t, tg.MessagesEditExportedChatInviteRequestTypeID, &req)
70+
71+
if req.Link != "https://t.me/+sub" {
72+
t.Fatalf("link = %q", req.Link)
73+
}
74+
}

0 commit comments

Comments
 (0)