Skip to content

Commit 527be69

Browse files
ernadoclaude
andcommitted
feat(members): implement chat member management
Add ban/unban/restrict/promote and GetChatMember(s)/GetChatAdministrators/ GetChatMemberCount over the MTProto channels.* API for supergroups and channels. Introduce ChatPermissions and ChatAdminRights with mapping to MTProto banned/admin rights, and a participant -> ChatMember converter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9d56456 commit 527be69

5 files changed

Lines changed: 540 additions & 3 deletions

File tree

chat_member_methods.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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+
// resolveChannel resolves a ChatID to its MTProto input channel. Member
11+
// management is only available in supergroups and channels.
12+
func (b *Bot) resolveChannel(ctx context.Context, chat ChatID) (tg.InputChannelClass, error) {
13+
p, err := b.resolvePeer(ctx, chat)
14+
if err != nil {
15+
return nil, err
16+
}
17+
ch, ok := p.(peers.Channel)
18+
if !ok {
19+
return nil, &Error{Code: 400, Description: "Bad Request: method is available only for supergroups and channels"}
20+
}
21+
return ch.InputChannel(), nil
22+
}
23+
24+
// BanChatMemberOption configures a BanChatMember call.
25+
type BanChatMemberOption func(*banConfig)
26+
27+
type banConfig struct {
28+
untilDate int
29+
revokeHistory bool
30+
}
31+
32+
// WithBanUntil sets the Unix time when the user will be unbanned (0 or far in
33+
// the future means forever).
34+
func WithBanUntil(untilDate int) BanChatMemberOption {
35+
return func(c *banConfig) { c.untilDate = untilDate }
36+
}
37+
38+
// WithRevokeMessages deletes all messages from the user being banned.
39+
func WithRevokeMessages() BanChatMemberOption {
40+
return func(c *banConfig) { c.revokeHistory = true }
41+
}
42+
43+
// BanChatMember bans a user from a supergroup or channel. The bot must be an
44+
// administrator with the can_restrict_members right.
45+
func (b *Bot) BanChatMember(ctx context.Context, chat ChatID, userID int64, opts ...BanChatMemberOption) error {
46+
var cfg banConfig
47+
for _, o := range opts {
48+
o(&cfg)
49+
}
50+
51+
channel, err := b.resolveChannel(ctx, chat)
52+
if err != nil {
53+
return err
54+
}
55+
user, err := b.resolveInputUser(ctx, userID)
56+
if err != nil {
57+
return err
58+
}
59+
60+
if _, err := b.raw.ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{
61+
Channel: channel,
62+
Participant: userToInputPeer(user),
63+
BannedRights: tg.ChatBannedRights{
64+
ViewMessages: true,
65+
SendMessages: true,
66+
SendMedia: true,
67+
SendStickers: true,
68+
SendGifs: true,
69+
SendGames: true,
70+
SendInline: true,
71+
EmbedLinks: true,
72+
UntilDate: cfg.untilDate,
73+
},
74+
}); err != nil {
75+
return asAPIError(err)
76+
}
77+
if cfg.revokeHistory {
78+
if _, err := b.raw.ChannelsDeleteParticipantHistory(ctx, &tg.ChannelsDeleteParticipantHistoryRequest{
79+
Channel: channel,
80+
Participant: userToInputPeer(user),
81+
}); err != nil {
82+
return asAPIError(err)
83+
}
84+
}
85+
return nil
86+
}
87+
88+
// UnbanChatMember removes a user's ban from a supergroup or channel, clearing
89+
// all restrictions. It does not re-add the user to the chat.
90+
func (b *Bot) UnbanChatMember(ctx context.Context, chat ChatID, userID int64) error {
91+
channel, err := b.resolveChannel(ctx, chat)
92+
if err != nil {
93+
return err
94+
}
95+
user, err := b.resolveInputUser(ctx, userID)
96+
if err != nil {
97+
return err
98+
}
99+
100+
if _, err := b.raw.ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{
101+
Channel: channel,
102+
Participant: userToInputPeer(user),
103+
BannedRights: tg.ChatBannedRights{}, // empty rights clear the ban
104+
}); err != nil {
105+
return asAPIError(err)
106+
}
107+
return nil
108+
}
109+
110+
// RestrictChatMember restricts a user in a supergroup. permissions is an
111+
// allow-list; denied actions are turned into MTProto banned rights. untilDate is
112+
// a Unix time (0 means forever).
113+
func (b *Bot) RestrictChatMember(ctx context.Context, chat ChatID, userID int64, permissions ChatPermissions, untilDate int) error {
114+
channel, err := b.resolveChannel(ctx, chat)
115+
if err != nil {
116+
return err
117+
}
118+
user, err := b.resolveInputUser(ctx, userID)
119+
if err != nil {
120+
return err
121+
}
122+
123+
if _, err := b.raw.ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{
124+
Channel: channel,
125+
Participant: userToInputPeer(user),
126+
BannedRights: permissions.toBannedRights(untilDate),
127+
}); err != nil {
128+
return asAPIError(err)
129+
}
130+
return nil
131+
}
132+
133+
// PromoteChatMember promotes or demotes a user in a supergroup or channel. The
134+
// granted rights are the true fields of rights; pass a zero value to demote.
135+
func (b *Bot) PromoteChatMember(ctx context.Context, chat ChatID, userID int64, rights ChatAdminRights) error {
136+
channel, err := b.resolveChannel(ctx, chat)
137+
if err != nil {
138+
return err
139+
}
140+
user, err := b.resolveInputUser(ctx, userID)
141+
if err != nil {
142+
return err
143+
}
144+
145+
if _, err := b.raw.ChannelsEditAdmin(ctx, &tg.ChannelsEditAdminRequest{
146+
Channel: channel,
147+
UserID: user,
148+
AdminRights: rights.toTg(),
149+
Rank: rights.CustomTitle,
150+
}); err != nil {
151+
return asAPIError(err)
152+
}
153+
return nil
154+
}
155+
156+
// GetChatMember returns information about a member of a supergroup or channel.
157+
func (b *Bot) GetChatMember(ctx context.Context, chat ChatID, userID int64) (ChatMember, error) {
158+
channel, err := b.resolveChannel(ctx, chat)
159+
if err != nil {
160+
return nil, err
161+
}
162+
user, err := b.resolveInputUser(ctx, userID)
163+
if err != nil {
164+
return nil, err
165+
}
166+
167+
res, err := b.raw.ChannelsGetParticipant(ctx, &tg.ChannelsGetParticipantRequest{
168+
Channel: channel,
169+
Participant: userToInputPeer(user),
170+
})
171+
if err != nil {
172+
return nil, asAPIError(err)
173+
}
174+
return chatMemberFromParticipant(res.Participant, usersByID(res.Users)), nil
175+
}
176+
177+
// GetChatAdministrators returns the administrators of a supergroup or channel,
178+
// excluding other bots.
179+
func (b *Bot) GetChatAdministrators(ctx context.Context, chat ChatID) ([]ChatMember, error) {
180+
channel, err := b.resolveChannel(ctx, chat)
181+
if err != nil {
182+
return nil, err
183+
}
184+
185+
res, err := b.raw.ChannelsGetParticipants(ctx, &tg.ChannelsGetParticipantsRequest{
186+
Channel: channel,
187+
Filter: &tg.ChannelParticipantsAdmins{},
188+
Limit: 200,
189+
})
190+
if err != nil {
191+
return nil, asAPIError(err)
192+
}
193+
participants, ok := res.AsModified()
194+
if !ok {
195+
return nil, &Error{Code: 500, Description: "Internal Server Error: unexpected participants response"}
196+
}
197+
198+
users := usersByID(participants.Users)
199+
out := make([]ChatMember, 0, len(participants.Participants))
200+
for _, p := range participants.Participants {
201+
out = append(out, chatMemberFromParticipant(p, users))
202+
}
203+
return out, nil
204+
}
205+
206+
// GetChatMemberCount returns the number of members in a supergroup or channel.
207+
func (b *Bot) GetChatMemberCount(ctx context.Context, chat ChatID) (int, error) {
208+
channel, err := b.resolveChannel(ctx, chat)
209+
if err != nil {
210+
return 0, err
211+
}
212+
213+
res, err := b.raw.ChannelsGetParticipants(ctx, &tg.ChannelsGetParticipantsRequest{
214+
Channel: channel,
215+
Filter: &tg.ChannelParticipantsRecent{},
216+
Limit: 1,
217+
})
218+
if err != nil {
219+
return 0, asAPIError(err)
220+
}
221+
participants, ok := res.AsModified()
222+
if !ok {
223+
return 0, &Error{Code: 500, Description: "Internal Server Error: unexpected participants response"}
224+
}
225+
return participants.Count, nil
226+
}
227+
228+
// userToInputPeer wraps an input user as the input peer the participant-editing
229+
// methods expect.
230+
func userToInputPeer(u tg.InputUserClass) tg.InputPeerClass {
231+
switch v := u.(type) {
232+
case *tg.InputUser:
233+
return &tg.InputPeerUser{UserID: v.UserID, AccessHash: v.AccessHash}
234+
case *tg.InputUserFromMessage:
235+
return &tg.InputPeerUserFromMessage{Peer: v.Peer, MsgID: v.MsgID, UserID: v.UserID}
236+
case *tg.InputUserSelf:
237+
return &tg.InputPeerSelf{}
238+
default:
239+
return &tg.InputPeerEmpty{}
240+
}
241+
}

chat_permissions.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package botapi
2+
3+
import "github.com/gotd/td/tg"
4+
5+
// ChatPermissions describes the actions a non-administrator user is allowed to
6+
// take in a chat. A nil/false field denies the action.
7+
type ChatPermissions struct {
8+
CanSendMessages bool `json:"can_send_messages,omitempty"`
9+
CanSendAudios bool `json:"can_send_audios,omitempty"`
10+
CanSendDocuments bool `json:"can_send_documents,omitempty"`
11+
CanSendPhotos bool `json:"can_send_photos,omitempty"`
12+
CanSendVideos bool `json:"can_send_videos,omitempty"`
13+
CanSendVideoNotes bool `json:"can_send_video_notes,omitempty"`
14+
CanSendVoiceNotes bool `json:"can_send_voice_notes,omitempty"`
15+
CanSendPolls bool `json:"can_send_polls,omitempty"`
16+
CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"`
17+
CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"`
18+
CanChangeInfo bool `json:"can_change_info,omitempty"`
19+
CanInviteUsers bool `json:"can_invite_users,omitempty"`
20+
CanPinMessages bool `json:"can_pin_messages,omitempty"`
21+
CanManageTopics bool `json:"can_manage_topics,omitempty"`
22+
}
23+
24+
// ChatAdminRights describes the administrator privileges granted to a user in a
25+
// supergroup or channel. It mirrors the Bot API promoteChatMember parameters.
26+
type ChatAdminRights struct {
27+
IsAnonymous bool `json:"is_anonymous,omitempty"`
28+
CanManageChat bool `json:"can_manage_chat,omitempty"`
29+
CanDeleteMessages bool `json:"can_delete_messages,omitempty"`
30+
CanManageVideoChats bool `json:"can_manage_video_chats,omitempty"`
31+
CanRestrictMembers bool `json:"can_restrict_members,omitempty"`
32+
CanPromoteMembers bool `json:"can_promote_members,omitempty"`
33+
CanChangeInfo bool `json:"can_change_info,omitempty"`
34+
CanInviteUsers bool `json:"can_invite_users,omitempty"`
35+
CanPostMessages bool `json:"can_post_messages,omitempty"`
36+
CanEditMessages bool `json:"can_edit_messages,omitempty"`
37+
CanPinMessages bool `json:"can_pin_messages,omitempty"`
38+
CanManageTopics bool `json:"can_manage_topics,omitempty"`
39+
// CustomTitle is the administrator's rank (custom title), if any.
40+
CustomTitle string `json:"custom_title,omitempty"`
41+
}
42+
43+
// toTg converts the Bot API admin rights to the MTProto representation.
44+
func (r ChatAdminRights) toTg() tg.ChatAdminRights {
45+
return tg.ChatAdminRights{
46+
ChangeInfo: r.CanChangeInfo,
47+
PostMessages: r.CanPostMessages,
48+
EditMessages: r.CanEditMessages,
49+
DeleteMessages: r.CanDeleteMessages,
50+
BanUsers: r.CanRestrictMembers,
51+
InviteUsers: r.CanInviteUsers,
52+
PinMessages: r.CanPinMessages,
53+
AddAdmins: r.CanPromoteMembers,
54+
Anonymous: r.IsAnonymous,
55+
ManageCall: r.CanManageVideoChats,
56+
Other: r.CanManageChat,
57+
ManageTopics: r.CanManageTopics,
58+
}
59+
}
60+
61+
// toBannedRights turns the allow-list permissions into MTProto banned rights,
62+
// where a set bit denies the action. untilDate is a Unix time (0 = forever).
63+
func (p ChatPermissions) toBannedRights(untilDate int) tg.ChatBannedRights {
64+
canSendAnyMedia := p.CanSendAudios || p.CanSendDocuments || p.CanSendPhotos ||
65+
p.CanSendVideos || p.CanSendVideoNotes || p.CanSendVoiceNotes
66+
return tg.ChatBannedRights{
67+
SendMessages: !p.CanSendMessages,
68+
SendMedia: !canSendAnyMedia,
69+
SendAudios: !p.CanSendAudios,
70+
SendDocs: !p.CanSendDocuments,
71+
SendPhotos: !p.CanSendPhotos,
72+
SendVideos: !p.CanSendVideos,
73+
SendRoundvideos: !p.CanSendVideoNotes,
74+
SendVoices: !p.CanSendVoiceNotes,
75+
SendPolls: !p.CanSendPolls,
76+
SendStickers: !p.CanSendOtherMessages,
77+
SendGifs: !p.CanSendOtherMessages,
78+
SendGames: !p.CanSendOtherMessages,
79+
SendInline: !p.CanSendOtherMessages,
80+
EmbedLinks: !p.CanAddWebPagePreviews,
81+
ChangeInfo: !p.CanChangeInfo,
82+
InviteUsers: !p.CanInviteUsers,
83+
PinMessages: !p.CanPinMessages,
84+
ManageTopics: !p.CanManageTopics,
85+
UntilDate: untilDate,
86+
}
87+
}

0 commit comments

Comments
 (0)