Skip to content

Commit f293a25

Browse files
ernadoclaude
andcommitted
feat(chat): add chat admin and invite link methods
Chat admin: SetChatTitle/SetChatDescription/LeaveChat (via the shared peers chat/channel ops), PinChatMessage/UnpinChatMessage/ UnpinAllChatMessages and SetChatPermissions over the raw messages.* API. Invite links: Export/Create/Edit/RevokeChatInviteLink over peers.InviteLinks, with a peers.InviteLink -> ChatInviteLink converter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 527be69 commit f293a25

2 files changed

Lines changed: 270 additions & 0 deletions

File tree

chat_admin.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
6+
"github.com/gotd/td/tg"
7+
)
8+
9+
// chatAdmin is the subset of peer operations shared by basic groups and
10+
// channels/supergroups. peers.Chat and peers.Channel both implement it; users
11+
// do not.
12+
type chatAdmin interface {
13+
SetTitle(ctx context.Context, title string) error
14+
SetDescription(ctx context.Context, about string) error
15+
Leave(ctx context.Context) error
16+
}
17+
18+
// resolveChatAdmin resolves a ChatID to the shared admin operations, rejecting
19+
// private chats.
20+
func (b *Bot) resolveChatAdmin(ctx context.Context, chat ChatID) (chatAdmin, error) {
21+
p, err := b.resolvePeer(ctx, chat)
22+
if err != nil {
23+
return nil, err
24+
}
25+
a, ok := p.(chatAdmin)
26+
if !ok {
27+
return nil, &Error{Code: 400, Description: "Bad Request: method is not available in private chats"}
28+
}
29+
return a, nil
30+
}
31+
32+
// SetChatTitle changes the title of a chat. The bot must be an administrator
33+
// with the appropriate rights.
34+
func (b *Bot) SetChatTitle(ctx context.Context, chat ChatID, title string) error {
35+
a, err := b.resolveChatAdmin(ctx, chat)
36+
if err != nil {
37+
return err
38+
}
39+
if err := a.SetTitle(ctx, title); err != nil {
40+
return asAPIError(err)
41+
}
42+
return nil
43+
}
44+
45+
// SetChatDescription changes the description of a chat.
46+
func (b *Bot) SetChatDescription(ctx context.Context, chat ChatID, description string) error {
47+
a, err := b.resolveChatAdmin(ctx, chat)
48+
if err != nil {
49+
return err
50+
}
51+
if err := a.SetDescription(ctx, description); err != nil {
52+
return asAPIError(err)
53+
}
54+
return nil
55+
}
56+
57+
// LeaveChat makes the bot leave a group, supergroup or channel.
58+
func (b *Bot) LeaveChat(ctx context.Context, chat ChatID) error {
59+
a, err := b.resolveChatAdmin(ctx, chat)
60+
if err != nil {
61+
return err
62+
}
63+
if err := a.Leave(ctx); err != nil {
64+
return asAPIError(err)
65+
}
66+
return nil
67+
}
68+
69+
// PinChatMessage pins a message in a chat. By default it notifies members; pass
70+
// Silent to pin quietly.
71+
func (b *Bot) PinChatMessage(ctx context.Context, chat ChatID, messageID int, opts ...SendOption) error {
72+
var cfg sendConfig
73+
for _, o := range opts {
74+
o(&cfg)
75+
}
76+
peer, err := b.resolveInputPeer(ctx, chat)
77+
if err != nil {
78+
return err
79+
}
80+
if _, err := b.raw.MessagesUpdatePinnedMessage(ctx, &tg.MessagesUpdatePinnedMessageRequest{
81+
Peer: peer,
82+
ID: messageID,
83+
Silent: cfg.silent,
84+
}); err != nil {
85+
return asAPIError(err)
86+
}
87+
return nil
88+
}
89+
90+
// UnpinChatMessage unpins the message with the given id in a chat.
91+
func (b *Bot) UnpinChatMessage(ctx context.Context, chat ChatID, messageID int) error {
92+
peer, err := b.resolveInputPeer(ctx, chat)
93+
if err != nil {
94+
return err
95+
}
96+
if _, err := b.raw.MessagesUpdatePinnedMessage(ctx, &tg.MessagesUpdatePinnedMessageRequest{
97+
Peer: peer,
98+
ID: messageID,
99+
Unpin: true,
100+
}); err != nil {
101+
return asAPIError(err)
102+
}
103+
return nil
104+
}
105+
106+
// UnpinAllChatMessages clears the list of pinned messages in a chat.
107+
func (b *Bot) UnpinAllChatMessages(ctx context.Context, chat ChatID) error {
108+
peer, err := b.resolveInputPeer(ctx, chat)
109+
if err != nil {
110+
return err
111+
}
112+
if _, err := b.raw.MessagesUnpinAllMessages(ctx, &tg.MessagesUnpinAllMessagesRequest{
113+
Peer: peer,
114+
}); err != nil {
115+
return asAPIError(err)
116+
}
117+
return nil
118+
}
119+
120+
// SetChatPermissions sets the default permissions for all non-administrator
121+
// members of a supergroup. permissions is an allow-list.
122+
func (b *Bot) SetChatPermissions(ctx context.Context, chat ChatID, permissions ChatPermissions) error {
123+
peer, err := b.resolveInputPeer(ctx, chat)
124+
if err != nil {
125+
return err
126+
}
127+
if _, err := b.raw.MessagesEditChatDefaultBannedRights(ctx, &tg.MessagesEditChatDefaultBannedRightsRequest{
128+
Peer: peer,
129+
BannedRights: permissions.toBannedRights(0),
130+
}); err != nil {
131+
return asAPIError(err)
132+
}
133+
return nil
134+
}

invite_links.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/gotd/td/telegram/peers"
8+
)
9+
10+
// resolveInviteLinks resolves a ChatID to its invite-link manager.
11+
func (b *Bot) resolveInviteLinks(ctx context.Context, chat ChatID) (peers.InviteLinks, error) {
12+
p, err := b.resolvePeer(ctx, chat)
13+
if err != nil {
14+
return peers.InviteLinks{}, err
15+
}
16+
type linkable interface {
17+
InviteLinks() peers.InviteLinks
18+
}
19+
l, ok := p.(linkable)
20+
if !ok {
21+
return peers.InviteLinks{}, &Error{Code: 400, Description: "Bad Request: method is not available in private chats"}
22+
}
23+
return l.InviteLinks(), nil
24+
}
25+
26+
// convertInviteLink converts a gotd invite link into the Bot API type. It
27+
// resolves the creator, which may require a network round trip.
28+
func (b *Bot) convertInviteLink(ctx context.Context, link peers.InviteLink) (*ChatInviteLink, error) {
29+
out := &ChatInviteLink{
30+
InviteLink: link.Link(),
31+
CreatesJoinRequest: link.RequestNeeded(),
32+
IsPrimary: link.Permanent(),
33+
IsRevoked: link.Revoked(),
34+
}
35+
if creator, err := link.Creator(ctx); err == nil {
36+
out.Creator = userFromTgUser(creator.Raw())
37+
}
38+
if name, ok := link.Title(); ok {
39+
out.Name = name
40+
}
41+
if expire, ok := link.ExpireDate(); ok {
42+
out.ExpireDate = int(expire.Unix())
43+
}
44+
if limit, ok := link.UsageLimit(); ok {
45+
out.MemberLimit = limit
46+
}
47+
if requested, ok := link.Requested(); ok {
48+
out.PendingJoinRequestCount = requested
49+
}
50+
return out, nil
51+
}
52+
53+
// InviteLinkOption configures a create/edit invite-link call.
54+
type InviteLinkOption func(*peers.ExportLinkOptions)
55+
56+
// WithInviteLinkName sets the invite link name (0-32 characters).
57+
func WithInviteLinkName(name string) InviteLinkOption {
58+
return func(o *peers.ExportLinkOptions) { o.Title = name }
59+
}
60+
61+
// WithInviteLinkExpire sets the Unix time when the link will expire.
62+
func WithInviteLinkExpire(unixTime int) InviteLinkOption {
63+
return func(o *peers.ExportLinkOptions) { o.ExpireDate = time.Unix(int64(unixTime), 0) }
64+
}
65+
66+
// WithInviteLinkMemberLimit caps how many users may join via this link.
67+
func WithInviteLinkMemberLimit(limit int) InviteLinkOption {
68+
return func(o *peers.ExportLinkOptions) { o.UsageLimit = limit }
69+
}
70+
71+
// WithInviteLinkJoinRequest requires administrators to approve users joining via
72+
// this link.
73+
func WithInviteLinkJoinRequest() InviteLinkOption {
74+
return func(o *peers.ExportLinkOptions) { o.RequestNeeded = true }
75+
}
76+
77+
// ExportChatInviteLink generates a new primary invite link for a chat, revoking
78+
// any previous one, and returns it.
79+
func (b *Bot) ExportChatInviteLink(ctx context.Context, chat ChatID) (string, error) {
80+
links, err := b.resolveInviteLinks(ctx, chat)
81+
if err != nil {
82+
return "", err
83+
}
84+
link, err := links.ExportNew(ctx, peers.ExportLinkOptions{})
85+
if err != nil {
86+
return "", asAPIError(err)
87+
}
88+
return link.Link(), nil
89+
}
90+
91+
// CreateChatInviteLink creates an additional invite link for a chat.
92+
func (b *Bot) CreateChatInviteLink(ctx context.Context, chat ChatID, opts ...InviteLinkOption) (*ChatInviteLink, error) {
93+
links, err := b.resolveInviteLinks(ctx, chat)
94+
if err != nil {
95+
return nil, err
96+
}
97+
var o peers.ExportLinkOptions
98+
for _, opt := range opts {
99+
opt(&o)
100+
}
101+
link, err := links.AddNew(ctx, o)
102+
if err != nil {
103+
return nil, asAPIError(err)
104+
}
105+
return b.convertInviteLink(ctx, link)
106+
}
107+
108+
// EditChatInviteLink edits a non-primary invite link created by the bot.
109+
func (b *Bot) EditChatInviteLink(ctx context.Context, chat ChatID, inviteLink string, opts ...InviteLinkOption) (*ChatInviteLink, error) {
110+
links, err := b.resolveInviteLinks(ctx, chat)
111+
if err != nil {
112+
return nil, err
113+
}
114+
var o peers.ExportLinkOptions
115+
for _, opt := range opts {
116+
opt(&o)
117+
}
118+
link, err := links.Edit(ctx, inviteLink, o)
119+
if err != nil {
120+
return nil, asAPIError(err)
121+
}
122+
return b.convertInviteLink(ctx, link)
123+
}
124+
125+
// RevokeChatInviteLink revokes an invite link created by the bot.
126+
func (b *Bot) RevokeChatInviteLink(ctx context.Context, chat ChatID, inviteLink string) (*ChatInviteLink, error) {
127+
links, err := b.resolveInviteLinks(ctx, chat)
128+
if err != nil {
129+
return nil, err
130+
}
131+
link, err := links.Revoke(ctx, inviteLink)
132+
if err != nil {
133+
return nil, asAPIError(err)
134+
}
135+
return b.convertInviteLink(ctx, link)
136+
}

0 commit comments

Comments
 (0)