From 4052cfa6877efc479f53ae2e7f337e0312846e82 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Sun, 30 Jan 2022 20:43:30 +0300 Subject: [PATCH 01/10] feat(peers): add IsBroadcast and IsSupergroup methods --- telegram/peers/channel.go | 10 ++++++++++ telegram/peers/chat.go | 10 ++++++++++ telegram/peers/multichat_test.go | 4 +++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/telegram/peers/channel.go b/telegram/peers/channel.go index c21df4c4fb..e759086208 100644 --- a/telegram/peers/channel.go +++ b/telegram/peers/channel.go @@ -138,6 +138,11 @@ func (c Channel) ToBroadcast() (Broadcast, bool) { }, true } +// IsBroadcast whether this Channel is Broadcast. +func (c Channel) IsBroadcast() bool { + return c.raw.Broadcast +} + // ToSupergroup tries to convert this Channel to Supergroup. func (c Channel) ToSupergroup() (Supergroup, bool) { if !c.raw.Megagroup { @@ -148,6 +153,11 @@ func (c Channel) ToSupergroup() (Supergroup, bool) { }, true } +// IsSupergroup whether this Channel is Supergroup. +func (c Channel) IsSupergroup() bool { + return c.raw.Megagroup +} + // InviteLinks returns InviteLinks for this peer. func (c Channel) InviteLinks() InviteLinks { return InviteLinks{ diff --git a/telegram/peers/chat.go b/telegram/peers/chat.go index eccc1125b5..98c5110d61 100644 --- a/telegram/peers/chat.go +++ b/telegram/peers/chat.go @@ -142,11 +142,21 @@ func (c Chat) ToBroadcast() (Broadcast, bool) { return Broadcast{}, false } +// IsBroadcast whether this Chat is Broadcast. +func (c Chat) IsBroadcast() bool { + return false +} + // ToSupergroup tries to convert this Chat to Supergroup. func (c Chat) ToSupergroup() (Supergroup, bool) { return Supergroup{}, false } +// IsSupergroup whether this Chat is Supergroup. +func (c Chat) IsSupergroup() bool { + return false +} + // Creator whether the current user is the creator of this group. func (c Chat) Creator() bool { return c.raw.GetCreator() diff --git a/telegram/peers/multichat_test.go b/telegram/peers/multichat_test.go index 5a7294cc28..789914ea04 100644 --- a/telegram/peers/multichat_test.go +++ b/telegram/peers/multichat_test.go @@ -26,8 +26,10 @@ type multiChat interface { SetDescription(ctx context.Context, about string) error InviteLinks() InviteLinks - ToSupergroup() (Supergroup, bool) ToBroadcast() (Broadcast, bool) + IsBroadcast() bool + ToSupergroup() (Supergroup, bool) + IsSupergroup() bool SetReactions(ctx context.Context, r ...string) error DisableReactions(ctx context.Context) error From 2caa1ac1ed3ebbcedb5a945f585f61675a9687a7 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Sun, 30 Jan 2022 20:43:44 +0300 Subject: [PATCH 02/10] feat(peers): initial support for admin methods --- telegram/peers/channel.go | 45 ++++++++++++++++++++++++ telegram/peers/chat.go | 47 ++++++++++++++++--------- telegram/peers/multichat_test.go | 2 ++ telegram/peers/query.go | 20 +++++++++++ telegram/peers/resolve.go | 8 ++--- telegram/peers/rights.go | 60 ++++++++++++++++++++++++++++++++ 6 files changed, 161 insertions(+), 21 deletions(-) create mode 100644 telegram/peers/rights.go diff --git a/telegram/peers/channel.go b/telegram/peers/channel.go index e759086208..fc9b2896aa 100644 --- a/telegram/peers/channel.go +++ b/telegram/peers/channel.go @@ -305,4 +305,49 @@ func (c Channel) setReactions(ctx context.Context, reactions ...string) error { return nil } +// KickUser kicks user member. +// +// Needed for parity with Chat to define common interface. +// +// If revokeHistory is set, will delete all messages from this member. +func (c Channel) KickUser(ctx context.Context, member tg.InputUserClass, revokeHistory bool) error { + p := convertInputUserToInputPeer(member) + if revokeHistory { + if _, err := c.m.api.ChannelsDeleteParticipantHistory(ctx, &tg.ChannelsDeleteParticipantHistoryRequest{ + Channel: c.InputChannel(), + Participant: p, + }); err != nil { + return errors.Wrap(err, "revoke history") + } + } + return c.KickParticipant(ctx, p) +} + +// KickParticipant kicks participant. +func (c Channel) KickParticipant(ctx context.Context, participant tg.InputPeerClass) error { + return c.editParticipantRights(ctx, participant, ParticipantRights{ + ViewMessages: true, + }) +} + +// EditParticipantRights edits participant rights in this channel. +func (c Channel) EditParticipantRights( + ctx context.Context, + participant tg.InputPeerClass, + options ParticipantRights, +) error { + return c.editParticipantRights(ctx, participant, options) +} + +func (c Channel) editParticipantRights(ctx context.Context, member tg.InputPeerClass, options ParticipantRights) error { + if _, err := c.m.api.ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{ + Channel: c.InputChannel(), + Participant: member, + BannedRights: options.IntoChatBannedRights(), + }); err != nil { + return errors.Wrap(err, "edit member rights") + } + return nil +} + // TODO(tdakkota): add more getters, helpers and convertors diff --git a/telegram/peers/chat.go b/telegram/peers/chat.go index 98c5110d61..1464f7f713 100644 --- a/telegram/peers/chat.go +++ b/telegram/peers/chat.go @@ -259,22 +259,6 @@ func (c Chat) SetDescription(ctx context.Context, about string) error { return nil } -// LeaveAndDelete leaves this chat and removes the entire chat history of this user in this chat. -func (c Chat) LeaveAndDelete(ctx context.Context) error { - return c.deleteMe(ctx, true) -} - -func (c Chat) deleteMe(ctx context.Context, revokeHistory bool) error { - if _, err := c.m.api.MessagesDeleteChatUser(ctx, &tg.MessagesDeleteChatUserRequest{ - RevokeHistory: revokeHistory, - ChatID: c.raw.GetID(), - UserID: &tg.InputUserSelf{}, - }); err != nil { - return errors.Wrapf(err, "leave (revoke: %v)", revokeHistory) - } - return nil -} - // SetReactions sets list of available reactions. // // Empty list disables reactions at all. @@ -298,4 +282,35 @@ func (c Chat) setReactions(ctx context.Context, reactions ...string) error { return nil } +// LeaveAndDelete leaves this chat and removes the entire chat history of this user in this chat. +func (c Chat) LeaveAndDelete(ctx context.Context) error { + return c.deleteMe(ctx, true) +} + +func (c Chat) deleteMe(ctx context.Context, revokeHistory bool) error { + return c.deleteUser(ctx, &tg.InputUserSelf{}, revokeHistory) +} + +func (c Chat) deleteUser(ctx context.Context, user tg.InputUserClass, revokeHistory bool) error { + if _, err := c.m.api.MessagesDeleteChatUser(ctx, &tg.MessagesDeleteChatUserRequest{ + RevokeHistory: revokeHistory, + ChatID: c.raw.GetID(), + UserID: user, + }); err != nil { + _, self := user.(*tg.InputUserSelf) + if self { + return errors.Wrapf(err, "leave (revoke: %v)", revokeHistory) + } + return errors.Wrapf(err, "delete user (revoke: %v)", revokeHistory) + } + return nil +} + +// KickUser kicks member. +// +// If revokeHistory is set, will delete all messages from this member. +func (c Chat) KickUser(ctx context.Context, member tg.InputUserClass, revokeHistory bool) error { + return c.deleteUser(ctx, member, revokeHistory) +} + // TODO(tdakkota): add more getters, helpers and convertors diff --git a/telegram/peers/multichat_test.go b/telegram/peers/multichat_test.go index 789914ea04..a950061bdb 100644 --- a/telegram/peers/multichat_test.go +++ b/telegram/peers/multichat_test.go @@ -33,6 +33,8 @@ type multiChat interface { SetReactions(ctx context.Context, r ...string) error DisableReactions(ctx context.Context) error + + KickUser(ctx context.Context, member tg.InputUserClass, revokeHistory bool) error } var _ = []multiChat{ diff --git a/telegram/peers/query.go b/telegram/peers/query.go index 4fd8b91786..e1eaafe130 100644 --- a/telegram/peers/query.go +++ b/telegram/peers/query.go @@ -9,6 +9,26 @@ import ( "github.com/gotd/td/tg" ) +func convertInputUserToInputPeer(p tg.InputUserClass) tg.InputPeerClass { + switch p := p.(type) { + case *tg.InputUserSelf: + return &tg.InputPeerSelf{} + case *tg.InputUser: + return &tg.InputPeerUser{ + UserID: p.UserID, + AccessHash: p.AccessHash, + } + case *tg.InputUserFromMessage: + return &tg.InputPeerUserFromMessage{ + Peer: p.Peer, + MsgID: p.MsgID, + UserID: p.UserID, + } + default: + return nil + } +} + func (m *Manager) getIDFromInputUser(p tg.InputUserClass) (int64, bool) { switch p := p.(type) { case *tg.InputUserSelf: diff --git a/telegram/peers/resolve.go b/telegram/peers/resolve.go index 87d7e6544f..adc13c5f15 100644 --- a/telegram/peers/resolve.go +++ b/telegram/peers/resolve.go @@ -12,7 +12,7 @@ import ( ) // Resolve uses given string to create new peer promise. -// It resolves peer of message using given Resolver. +// // Input examples: // // @telegram @@ -72,7 +72,6 @@ func cleanupPhone(phone string) string { // ResolvePhone uses given phone to resolve User. // -// It resolves peer of message using given Resolver. // Input example: // // +13115552368 @@ -152,7 +151,7 @@ func (m *Manager) findPeerClass(p tg.PeerClass, users []tg.UserClass, chats []tg } // ResolveDomain uses given domain to create new peer promise. -// It resolves peer of message using given Resolver. +// // May be prefixed with @ or not. // // Input examples: @@ -199,8 +198,7 @@ func (m *Manager) ResolveDomain(ctx context.Context, domain string) (Peer, error } // ResolveDeeplink uses given deeplink to create new peer promise. -// Deeplink is a URL like https://t.me/telegram. -// It resolves peer of message using given Resolver. +// // Input examples: // // t.me/telegram diff --git a/telegram/peers/rights.go b/telegram/peers/rights.go new file mode 100644 index 0000000000..dc500e317d --- /dev/null +++ b/telegram/peers/rights.go @@ -0,0 +1,60 @@ +package peers + +import ( + "time" + + "github.com/gotd/td/tg" +) + +// ParticipantRights is options for Channel.EditParticipantRights. +type ParticipantRights struct { + // If set, does not allow a user to view messages in a supergroup/channel/chat + ViewMessages bool + // If set, does not allow a user to send messages in a supergroup/chat + SendMessages bool + // If set, does not allow a user to send any media in a supergroup/chat + SendMedia bool + // If set, does not allow a user to send stickers in a supergroup/chat + SendStickers bool + // If set, does not allow a user to send gifs in a supergroup/chat + SendGifs bool + // If set, does not allow a user to send games in a supergroup/chat + SendGames bool + // If set, does not allow a user to use inline bots in a supergroup/chat + SendInline bool + // If set, does not allow a user to embed links in the messages of a supergroup/chat + EmbedLinks bool + // If set, does not allow a user to send polls in a supergroup/chat + SendPolls bool + // If set, does not allow any user to change the description of a supergroup/chat + ChangeInfo bool + // If set, does not allow any user to invite users in a supergroup/chat + InviteUsers bool + // If set, does not allow any user to pin messages in a supergroup/chat + PinMessages bool + // Validity of said permissions (it is considered forever any value less than 30 seconds or more than 366 days). + UntilDate time.Time +} + +// IntoChatBannedRights converts ParticipantRights into tg.ChatBannedRights. +func (b ParticipantRights) IntoChatBannedRights() (r tg.ChatBannedRights) { + r = tg.ChatBannedRights{ + ViewMessages: b.ViewMessages, + SendMessages: b.SendMessages, + SendMedia: b.SendMedia, + SendStickers: b.SendStickers, + SendGifs: b.SendGifs, + SendGames: b.SendGames, + SendInline: b.SendInline, + EmbedLinks: b.EmbedLinks, + SendPolls: b.SendPolls, + ChangeInfo: b.ChangeInfo, + InviteUsers: b.InviteUsers, + PinMessages: b.PinMessages, + } + if !b.UntilDate.IsZero() { + r.UntilDate = int(b.UntilDate.Unix()) + } + r.SetFlags() + return r +} From 94aaa3ee690f0d2fc1ce4c6ccab8e172552d9fda Mon Sep 17 00:00:00 2001 From: tdakkota Date: Sat, 5 Feb 2022 17:21:15 +0300 Subject: [PATCH 03/10] feat(peers): add methods for editing rights of admins and all participants --- telegram/peers/channel.go | 62 +++++++++++++----------- telegram/peers/chat.go | 50 +++++++++---------- telegram/peers/multichat.go | 42 ++++++++++++++++ telegram/peers/multichat_test.go | 1 + telegram/peers/rights.go | 82 +++++++++++++++++++++++++++----- 5 files changed, 173 insertions(+), 64 deletions(-) create mode 100644 telegram/peers/multichat.go diff --git a/telegram/peers/channel.go b/telegram/peers/channel.go index fc9b2896aa..7d55710e51 100644 --- a/telegram/peers/channel.go +++ b/telegram/peers/channel.go @@ -213,6 +213,7 @@ func (c Channel) NoForwards() bool { // // See https://core.telegram.org/api/rights. func (c Channel) AdminRights() (tg.ChatAdminRights, bool) { + // TODO(tdakkota): add wrapper for raw object? return c.raw.GetAdminRights() } @@ -220,6 +221,7 @@ func (c Channel) AdminRights() (tg.ChatAdminRights, bool) { // // See https://core.telegram.org/api/rights. func (c Channel) BannedRights() (tg.ChatBannedRights, bool) { + // TODO(tdakkota): add wrapper for raw object? return c.raw.GetBannedRights() } @@ -227,6 +229,7 @@ func (c Channel) BannedRights() (tg.ChatBannedRights, bool) { // // See https://core.telegram.org/api/rights. func (c Channel) DefaultBannedRights() (tg.ChatBannedRights, bool) { + // TODO(tdakkota): add wrapper for raw object? return c.raw.GetDefaultBannedRights() } @@ -273,45 +276,28 @@ func (c Channel) SetTitle(ctx context.Context, title string) error { // SetDescription sets new description for this Chat. func (c Channel) SetDescription(ctx context.Context, about string) error { - if _, err := c.m.api.MessagesEditChatAbout(ctx, &tg.MessagesEditChatAboutRequest{ - Peer: c.InputPeer(), - About: about, - }); err != nil { - return errors.Wrap(err, "edit channel about") - } - return nil + return c.m.editAbout(ctx, c.InputPeer(), about) } // SetReactions sets list of available reactions. // // Empty list disables reactions at all. func (c Channel) SetReactions(ctx context.Context, reactions ...string) error { - return c.setReactions(ctx, reactions...) + return c.m.editReactions(ctx, c.InputPeer(), reactions...) } // DisableReactions disables reactions. func (c Channel) DisableReactions(ctx context.Context) error { - return c.setReactions(ctx) -} - -func (c Channel) setReactions(ctx context.Context, reactions ...string) error { - if _, err := c.m.api.MessagesSetChatAvailableReactions(ctx, &tg.MessagesSetChatAvailableReactionsRequest{ - Peer: c.InputPeer(), - AvailableReactions: reactions, - }); err != nil { - return errors.Wrap(err, "set reactions") - } - - return nil + return c.m.editReactions(ctx, c.InputPeer()) } -// KickUser kicks user member. +// KickUser kicks user participant. // // Needed for parity with Chat to define common interface. // -// If revokeHistory is set, will delete all messages from this member. -func (c Channel) KickUser(ctx context.Context, member tg.InputUserClass, revokeHistory bool) error { - p := convertInputUserToInputPeer(member) +// If revokeHistory is set, will delete all messages from this participant. +func (c Channel) KickUser(ctx context.Context, participant tg.InputUserClass, revokeHistory bool) error { + p := convertInputUserToInputPeer(participant) if revokeHistory { if _, err := c.m.api.ChannelsDeleteParticipantHistory(ctx, &tg.ChannelsDeleteParticipantHistoryRequest{ Channel: c.InputChannel(), @@ -339,13 +325,35 @@ func (c Channel) EditParticipantRights( return c.editParticipantRights(ctx, participant, options) } -func (c Channel) editParticipantRights(ctx context.Context, member tg.InputPeerClass, options ParticipantRights) error { +func (c Channel) editParticipantRights(ctx context.Context, p tg.InputPeerClass, options ParticipantRights) error { if _, err := c.m.api.ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{ Channel: c.InputChannel(), - Participant: member, + Participant: p, BannedRights: options.IntoChatBannedRights(), }); err != nil { - return errors.Wrap(err, "edit member rights") + return errors.Wrap(err, "edit participant rights") + } + return nil +} + +// EditRights edits rights of all participants in this channel. +func (c Channel) EditRights(ctx context.Context, options ParticipantRights) error { + return c.m.editDefaultRights(ctx, c.InputPeer(), options) +} + +// EditAdminRights edits admin rights in this channel. +func (c Channel) EditAdminRights( + ctx context.Context, + admin tg.InputUserClass, + options AdminRights, +) error { + if _, err := c.m.api.ChannelsEditAdmin(ctx, &tg.ChannelsEditAdminRequest{ + Channel: c.InputChannel(), + UserID: admin, + AdminRights: options.IntoChatAdminRights(), + Rank: options.Rank, + }); err != nil { + return errors.Wrap(err, "edit admin rights") } return nil } diff --git a/telegram/peers/chat.go b/telegram/peers/chat.go index 1464f7f713..11541e31b9 100644 --- a/telegram/peers/chat.go +++ b/telegram/peers/chat.go @@ -206,6 +206,7 @@ func (c Chat) ParticipantsCount() int { // // See https://core.telegram.org/api/rights. func (c Chat) AdminRights() (tg.ChatAdminRights, bool) { + // TODO(tdakkota): add wrapper for raw object? return c.raw.GetAdminRights() } @@ -213,6 +214,7 @@ func (c Chat) AdminRights() (tg.ChatAdminRights, bool) { // // See https://core.telegram.org/api/rights. func (c Chat) DefaultBannedRights() (tg.ChatBannedRights, bool) { + // TODO(tdakkota): add wrapper for raw object? return c.raw.GetDefaultBannedRights() } @@ -250,36 +252,19 @@ func (c Chat) SetTitle(ctx context.Context, title string) error { // SetDescription sets new description for this Chat. func (c Chat) SetDescription(ctx context.Context, about string) error { - if _, err := c.m.api.MessagesEditChatAbout(ctx, &tg.MessagesEditChatAboutRequest{ - Peer: c.InputPeer(), - About: about, - }); err != nil { - return errors.Wrap(err, "edit chat about") - } - return nil + return c.m.editAbout(ctx, c.InputPeer(), about) } // SetReactions sets list of available reactions. // // Empty list disables reactions at all. func (c Chat) SetReactions(ctx context.Context, reactions ...string) error { - return c.setReactions(ctx, reactions...) + return c.m.editReactions(ctx, c.InputPeer(), reactions...) } // DisableReactions disables reactions. func (c Chat) DisableReactions(ctx context.Context) error { - return c.setReactions(ctx) -} - -func (c Chat) setReactions(ctx context.Context, reactions ...string) error { - if _, err := c.m.api.MessagesSetChatAvailableReactions(ctx, &tg.MessagesSetChatAvailableReactionsRequest{ - Peer: c.InputPeer(), - AvailableReactions: reactions, - }); err != nil { - return errors.Wrap(err, "set reactions") - } - - return nil + return c.m.editReactions(ctx, c.InputPeer()) } // LeaveAndDelete leaves this chat and removes the entire chat history of this user in this chat. @@ -306,11 +291,28 @@ func (c Chat) deleteUser(ctx context.Context, user tg.InputUserClass, revokeHist return nil } -// KickUser kicks member. +// KickUser kicks user participant. // -// If revokeHistory is set, will delete all messages from this member. -func (c Chat) KickUser(ctx context.Context, member tg.InputUserClass, revokeHistory bool) error { - return c.deleteUser(ctx, member, revokeHistory) +// If revokeHistory is set, will delete all messages from this participant. +func (c Chat) KickUser(ctx context.Context, participant tg.InputUserClass, revokeHistory bool) error { + return c.deleteUser(ctx, participant, revokeHistory) +} + +// EditRights edits rights of all participants in this channel. +func (c Chat) EditRights(ctx context.Context, options ParticipantRights) error { + return c.m.editDefaultRights(ctx, c.InputPeer(), options) +} + +// EditAdmin edits admin rights for given user. +func (c Chat) EditAdmin(ctx context.Context, user tg.InputUserClass, isAdmin bool) error { + if _, err := c.m.api.MessagesEditChatAdmin(ctx, &tg.MessagesEditChatAdminRequest{ + ChatID: c.ID(), + UserID: user, + IsAdmin: isAdmin, + }); err != nil { + return errors.Wrap(err, "edit admin") + } + return nil } // TODO(tdakkota): add more getters, helpers and convertors diff --git a/telegram/peers/multichat.go b/telegram/peers/multichat.go new file mode 100644 index 0000000000..f376d32e7b --- /dev/null +++ b/telegram/peers/multichat.go @@ -0,0 +1,42 @@ +package peers + +import ( + "context" + + "github.com/go-faster/errors" + + "github.com/gotd/td/tg" +) + +func (m *Manager) editAbout(ctx context.Context, p tg.InputPeerClass, about string) error { + if _, err := m.api.MessagesEditChatAbout(ctx, &tg.MessagesEditChatAboutRequest{ + Peer: p, + About: about, + }); err != nil { + if _, ok := p.(*tg.InputPeerChat); ok { + return errors.Wrap(err, "edit chat about") + } + return errors.Wrap(err, "edit channel about") + } + return nil +} + +func (m *Manager) editReactions(ctx context.Context, p tg.InputPeerClass, reactions ...string) error { + if _, err := m.api.MessagesSetChatAvailableReactions(ctx, &tg.MessagesSetChatAvailableReactionsRequest{ + Peer: p, + AvailableReactions: reactions, + }); err != nil { + return errors.Wrap(err, "set reactions") + } + return nil +} + +func (m *Manager) editDefaultRights(ctx context.Context, p tg.InputPeerClass, rights ParticipantRights) error { + if _, err := m.api.MessagesEditChatDefaultBannedRights(ctx, &tg.MessagesEditChatDefaultBannedRightsRequest{ + Peer: p, + BannedRights: rights.IntoChatBannedRights(), + }); err != nil { + return errors.Wrap(err, "edit default rights") + } + return nil +} diff --git a/telegram/peers/multichat_test.go b/telegram/peers/multichat_test.go index a950061bdb..95f9f5b20e 100644 --- a/telegram/peers/multichat_test.go +++ b/telegram/peers/multichat_test.go @@ -35,6 +35,7 @@ type multiChat interface { DisableReactions(ctx context.Context) error KickUser(ctx context.Context, member tg.InputUserClass, revokeHistory bool) error + EditRights(ctx context.Context, options ParticipantRights) error } var _ = []multiChat{ diff --git a/telegram/peers/rights.go b/telegram/peers/rights.go index dc500e317d..caad2b42a5 100644 --- a/telegram/peers/rights.go +++ b/telegram/peers/rights.go @@ -6,36 +6,92 @@ import ( "github.com/gotd/td/tg" ) -// ParticipantRights is options for Channel.EditParticipantRights. +// AdminRights represents admin right settings. +type AdminRights struct { + // Indicates the role (rank) of the admin in the group: just an arbitrary string. + // + // If empty, will not be used. + Rank string + // If set, allows the admin to modify the description of the channel/supergroup. + ChangeInfo bool + // If set, allows the admin to post messages in the channel. + PostMessages bool + // If set, allows the admin to also edit messages from other admins in the channel. + EditMessages bool + // If set, allows the admin to also delete messages from other admins in the channel. + DeleteMessages bool + // If set, allows the admin to ban users from the channel/supergroup. + BanUsers bool + // If set, allows the admin to invite users in the channel/supergroup. + InviteUsers bool + // If set, allows the admin to pin messages in the channel/supergroup. + PinMessages bool + // If set, allows the admin to add other admins with the same (or more limited) + // permissions in the channel/supergroup. + AddAdmins bool + // Whether this admin is anonymous. + Anonymous bool + // If set, allows the admin to change group call/livestream settings. + ManageCall bool + // Set this flag if none of the other flags are set, but you still want the user to be an + // admin. + Other bool +} + +// IntoChatAdminRights converts AdminRights into tg.ChatAdminRights. +func (b AdminRights) IntoChatAdminRights() (r tg.ChatAdminRights) { + r.ChangeInfo = b.ChangeInfo + r.PostMessages = b.PostMessages + r.EditMessages = b.EditMessages + r.DeleteMessages = b.DeleteMessages + r.BanUsers = b.BanUsers + r.InviteUsers = b.InviteUsers + r.PinMessages = b.PinMessages + r.AddAdmins = b.AddAdmins + r.Anonymous = b.Anonymous + r.ManageCall = b.ManageCall + r.Other = b.Other + r.SetFlags() + return r +} + +// ParticipantRights represents participant right settings. type ParticipantRights struct { - // If set, does not allow a user to view messages in a supergroup/channel/chat + // If set, does not allow a user to view messages in a supergroup/channel/chat. ViewMessages bool - // If set, does not allow a user to send messages in a supergroup/chat + // If set, does not allow a user to send messages in a supergroup/chat. SendMessages bool - // If set, does not allow a user to send any media in a supergroup/chat + // If set, does not allow a user to send any media in a supergroup/chat. SendMedia bool - // If set, does not allow a user to send stickers in a supergroup/chat + // If set, does not allow a user to send stickers in a supergroup/chat. SendStickers bool - // If set, does not allow a user to send gifs in a supergroup/chat + // If set, does not allow a user to send gifs in a supergroup/chat. SendGifs bool - // If set, does not allow a user to send games in a supergroup/chat + // If set, does not allow a user to send games in a supergroup/chat. SendGames bool - // If set, does not allow a user to use inline bots in a supergroup/chat + // If set, does not allow a user to use inline bots in a supergroup/chat. SendInline bool - // If set, does not allow a user to embed links in the messages of a supergroup/chat + // If set, does not allow a user to embed links in the messages of a supergroup/chat. EmbedLinks bool - // If set, does not allow a user to send polls in a supergroup/chat + // If set, does not allow a user to send polls in a supergroup/chat. SendPolls bool - // If set, does not allow any user to change the description of a supergroup/chat + // If set, does not allow any user to change the description of a supergroup/chat. ChangeInfo bool - // If set, does not allow any user to invite users in a supergroup/chat + // If set, does not allow any user to invite users in a supergroup/chat. InviteUsers bool - // If set, does not allow any user to pin messages in a supergroup/chat + // If set, does not allow any user to pin messages in a supergroup/chat. PinMessages bool // Validity of said permissions (it is considered forever any value less than 30 seconds or more than 366 days). + // + // If value is zero, value will not be used. UntilDate time.Time } +// SetUntil sets duration of validity of set rights. +func (b *ParticipantRights) SetUntil(d time.Duration) { + b.UntilDate = time.Now().Add(d) +} + // IntoChatBannedRights converts ParticipantRights into tg.ChatBannedRights. func (b ParticipantRights) IntoChatBannedRights() (r tg.ChatBannedRights) { r = tg.ChatBannedRights{ From a1a0b6b13bcbfa1183cb20751ea34997438635a7 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Sat, 5 Feb 2022 20:24:35 +0300 Subject: [PATCH 04/10] test(peers): improve test coverage --- telegram/peers/channel.go | 10 +-- telegram/peers/channel_test.go | 116 +++++++++++++++++++++++++++++++ telegram/peers/chat.go | 4 +- telegram/peers/multichat_test.go | 26 +++++++ telegram/peers/rights.go | 54 +++++++------- telegram/peers/rights_test.go | 57 +++++++++++++++ telegram/peers/user_test.go | 61 ++++++++++++++++ 7 files changed, 295 insertions(+), 33 deletions(-) create mode 100644 telegram/peers/rights_test.go diff --git a/telegram/peers/channel.go b/telegram/peers/channel.go index 7d55710e51..6310c3584c 100644 --- a/telegram/peers/channel.go +++ b/telegram/peers/channel.go @@ -130,7 +130,7 @@ func (c Channel) FullRaw(ctx context.Context) (*tg.ChannelFull, error) { // ToBroadcast tries to convert this Channel to Broadcast. func (c Channel) ToBroadcast() (Broadcast, bool) { - if !c.raw.Broadcast { + if !c.IsBroadcast() { return Broadcast{}, false } return Broadcast{ @@ -145,7 +145,7 @@ func (c Channel) IsBroadcast() bool { // ToSupergroup tries to convert this Channel to Supergroup. func (c Channel) ToSupergroup() (Supergroup, bool) { - if !c.raw.Megagroup { + if !c.IsSupergroup() { return Supergroup{}, false } return Supergroup{ @@ -311,8 +311,8 @@ func (c Channel) KickUser(ctx context.Context, participant tg.InputUserClass, re // KickParticipant kicks participant. func (c Channel) KickParticipant(ctx context.Context, participant tg.InputPeerClass) error { - return c.editParticipantRights(ctx, participant, ParticipantRights{ - ViewMessages: true, + return c.EditParticipantRights(ctx, participant, ParticipantRights{ + DenyViewMessages: true, }) } @@ -341,7 +341,7 @@ func (c Channel) EditRights(ctx context.Context, options ParticipantRights) erro return c.m.editDefaultRights(ctx, c.InputPeer(), options) } -// EditAdminRights edits admin rights in this channel. +// EditAdminRights edits admin rights of given user in this channel. func (c Channel) EditAdminRights( ctx context.Context, admin tg.InputUserClass, diff --git a/telegram/peers/channel_test.go b/telegram/peers/channel_test.go index 6f2b2e97af..7d3e11a42c 100644 --- a/telegram/peers/channel_test.go +++ b/telegram/peers/channel_test.go @@ -73,6 +73,24 @@ func TestChannelGetters(t *testing.T) { a.Equal(b.raw.Broadcast, ok) a.Equal(b.raw.Signatures, b.Signatures()) } + { + v, ok := u.AdminRights() + v2, ok2 := u.raw.GetAdminRights() + a.Equal(ok, ok2) + a.Equal(v2, v) + } + { + v, ok := u.BannedRights() + v2, ok2 := u.raw.GetBannedRights() + a.Equal(ok, ok2) + a.Equal(v2, v) + } + { + v, ok := u.DefaultBannedRights() + v2, ok2 := u.raw.GetDefaultBannedRights() + a.Equal(ok2, ok) + a.Equal(v2, v) + } } func TestChannel_Leave(t *testing.T) { @@ -134,3 +152,101 @@ func TestChannel_SetDescription(t *testing.T) { }).ThenTrue() a.NoError(ch.SetDescription(ctx, about)) } + +func TestChannel_Join(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + ch := m.Channel(getTestChannel()) + + mock.ExpectCall(&tg.ChannelsJoinChannelRequest{ + Channel: ch.InputChannel(), + }).ThenRPCErr(getTestError()) + a.Error(ch.Join(ctx)) + + mock.ExpectCall(&tg.ChannelsJoinChannelRequest{ + Channel: ch.InputChannel(), + }).ThenResult(&tg.Updates{}) + a.NoError(ch.Join(ctx)) +} + +func TestChannel_Delete(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + ch := m.Channel(getTestChannel()) + + mock.ExpectCall(&tg.ChannelsDeleteChannelRequest{ + Channel: ch.InputChannel(), + }).ThenRPCErr(getTestError()) + a.Error(ch.Delete(ctx)) + + mock.ExpectCall(&tg.ChannelsDeleteChannelRequest{ + Channel: ch.InputChannel(), + }).ThenResult(&tg.Updates{}) + a.NoError(ch.Delete(ctx)) +} + +func TestChannel_KickUser(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + u := m.User(getTestUser()) + ch := m.Channel(getTestChannel()) + rights := tg.ChatBannedRights{ + ViewMessages: true, + } + rights.SetFlags() + + mock.ExpectCall(&tg.ChannelsEditBannedRequest{ + Channel: ch.InputChannel(), + Participant: u.InputPeer(), + BannedRights: rights, + }).ThenRPCErr(getTestError()) + a.Error(ch.KickUser(ctx, u.InputUser(), false)) + + mock.ExpectCall(&tg.ChannelsEditBannedRequest{ + Channel: ch.InputChannel(), + Participant: u.InputPeer(), + BannedRights: rights, + }).ThenResult(&tg.Updates{}) + a.NoError(ch.KickUser(ctx, u.InputUser(), false)) +} + +func TestChannel_EditAdminRights(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + u := m.User(getTestUser()) + ch := m.Channel(getTestChannel()) + rights := tg.ChatAdminRights{ + AddAdmins: true, + } + rights.SetFlags() + + mock.ExpectCall(&tg.ChannelsEditAdminRequest{ + Channel: ch.InputChannel(), + UserID: u.InputUser(), + AdminRights: rights, + Rank: "rank", + }).ThenRPCErr(getTestError()) + a.Error(ch.EditAdminRights(ctx, u.InputUser(), AdminRights{ + Rank: "rank", + AddAdmins: true, + })) + + mock.ExpectCall(&tg.ChannelsEditAdminRequest{ + Channel: ch.InputChannel(), + UserID: u.InputUser(), + AdminRights: rights, + Rank: "rank", + }).ThenResult(&tg.Updates{}) + a.NoError(ch.EditAdminRights(ctx, u.InputUser(), AdminRights{ + Rank: "rank", + AddAdmins: true, + })) +} diff --git a/telegram/peers/chat.go b/telegram/peers/chat.go index 11541e31b9..a53d2ebed3 100644 --- a/telegram/peers/chat.go +++ b/telegram/peers/chat.go @@ -139,7 +139,7 @@ func (c Chat) InviteLinks() InviteLinks { // ToBroadcast tries to convert this Chat to Broadcast. func (c Chat) ToBroadcast() (Broadcast, bool) { - return Broadcast{}, false + return Broadcast{}, c.IsBroadcast() } // IsBroadcast whether this Chat is Broadcast. @@ -149,7 +149,7 @@ func (c Chat) IsBroadcast() bool { // ToSupergroup tries to convert this Chat to Supergroup. func (c Chat) ToSupergroup() (Supergroup, bool) { - return Supergroup{}, false + return Supergroup{}, c.IsSupergroup() } // IsSupergroup whether this Chat is Supergroup. diff --git a/telegram/peers/multichat_test.go b/telegram/peers/multichat_test.go index 95f9f5b20e..ad10b67d07 100644 --- a/telegram/peers/multichat_test.go +++ b/telegram/peers/multichat_test.go @@ -70,3 +70,29 @@ func TestReactions(t *testing.T) { a.NoError(p.DisableReactions(ctx)) } } + +func TestEditRights(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + rights := tg.ChatBannedRights{ + SendInline: true, + } + rights.SetFlags() + req := func(p Peer) *tgmock.RequestBuilder { + return mock.ExpectCall(&tg.MessagesEditChatDefaultBannedRightsRequest{ + Peer: p.InputPeer(), + BannedRights: rights, + }) + } + for _, p := range []multiChat{ + m.Chat(getTestChat()), + m.Channel(getTestChannel()), + } { + req(p).ThenRPCErr(getTestError()) + a.Error(p.EditRights(ctx, ParticipantRights{DenySendInline: true})) + req(p).ThenResult(&tg.Updates{}) + a.NoError(p.EditRights(ctx, ParticipantRights{DenySendInline: true})) + } +} diff --git a/telegram/peers/rights.go b/telegram/peers/rights.go index caad2b42a5..d45f010997 100644 --- a/telegram/peers/rights.go +++ b/telegram/peers/rights.go @@ -58,55 +58,57 @@ func (b AdminRights) IntoChatAdminRights() (r tg.ChatAdminRights) { // ParticipantRights represents participant right settings. type ParticipantRights struct { // If set, does not allow a user to view messages in a supergroup/channel/chat. - ViewMessages bool + // + // In fact, user will be kicked. + DenyViewMessages bool // If set, does not allow a user to send messages in a supergroup/chat. - SendMessages bool + DenySendMessages bool // If set, does not allow a user to send any media in a supergroup/chat. - SendMedia bool + DenySendMedia bool // If set, does not allow a user to send stickers in a supergroup/chat. - SendStickers bool + DenySendStickers bool // If set, does not allow a user to send gifs in a supergroup/chat. - SendGifs bool + DenySendGifs bool // If set, does not allow a user to send games in a supergroup/chat. - SendGames bool + DenySendGames bool // If set, does not allow a user to use inline bots in a supergroup/chat. - SendInline bool + DenySendInline bool // If set, does not allow a user to embed links in the messages of a supergroup/chat. - EmbedLinks bool + DenyEmbedLinks bool // If set, does not allow a user to send polls in a supergroup/chat. - SendPolls bool + DenySendPolls bool // If set, does not allow any user to change the description of a supergroup/chat. - ChangeInfo bool + DenyChangeInfo bool // If set, does not allow any user to invite users in a supergroup/chat. - InviteUsers bool + DenyInviteUsers bool // If set, does not allow any user to pin messages in a supergroup/chat. - PinMessages bool + DenyPinMessages bool // Validity of said permissions (it is considered forever any value less than 30 seconds or more than 366 days). // // If value is zero, value will not be used. UntilDate time.Time } -// SetUntil sets duration of validity of set rights. -func (b *ParticipantRights) SetUntil(d time.Duration) { +// ApplyFor sets duration of validity of set rights. +func (b *ParticipantRights) ApplyFor(d time.Duration) { b.UntilDate = time.Now().Add(d) } // IntoChatBannedRights converts ParticipantRights into tg.ChatBannedRights. func (b ParticipantRights) IntoChatBannedRights() (r tg.ChatBannedRights) { r = tg.ChatBannedRights{ - ViewMessages: b.ViewMessages, - SendMessages: b.SendMessages, - SendMedia: b.SendMedia, - SendStickers: b.SendStickers, - SendGifs: b.SendGifs, - SendGames: b.SendGames, - SendInline: b.SendInline, - EmbedLinks: b.EmbedLinks, - SendPolls: b.SendPolls, - ChangeInfo: b.ChangeInfo, - InviteUsers: b.InviteUsers, - PinMessages: b.PinMessages, + ViewMessages: b.DenyViewMessages, + SendMessages: b.DenySendMessages, + SendMedia: b.DenySendMedia, + SendStickers: b.DenySendStickers, + SendGifs: b.DenySendGifs, + SendGames: b.DenySendGames, + SendInline: b.DenySendInline, + EmbedLinks: b.DenyEmbedLinks, + SendPolls: b.DenySendPolls, + ChangeInfo: b.DenyChangeInfo, + InviteUsers: b.DenyInviteUsers, + PinMessages: b.DenyPinMessages, } if !b.UntilDate.IsZero() { r.UntilDate = int(b.UntilDate.Unix()) diff --git a/telegram/peers/rights_test.go b/telegram/peers/rights_test.go new file mode 100644 index 0000000000..38fa007db3 --- /dev/null +++ b/telegram/peers/rights_test.go @@ -0,0 +1,57 @@ +package peers + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/gotd/td/tg" +) + +func TestParticipantRights_ApplyFor(t *testing.T) { + var r ParticipantRights + r.ApplyFor(time.Second) + require.False(t, r.UntilDate.IsZero()) +} + +func TestParticipantRights_IntoChatBannedRights(t *testing.T) { + r := ParticipantRights{ + DenyViewMessages: true, + DenySendMessages: true, + DenySendMedia: true, + DenySendStickers: true, + DenySendGifs: true, + DenySendGames: true, + DenySendInline: true, + DenyEmbedLinks: true, + DenySendPolls: true, + DenyChangeInfo: true, + DenyInviteUsers: true, + DenyPinMessages: true, + UntilDate: time.Time{}, + } + + rights := r.IntoChatBannedRights() + expected := tg.ChatBannedRights{ + ViewMessages: true, + SendMessages: true, + SendMedia: true, + SendStickers: true, + SendGifs: true, + SendGames: true, + SendInline: true, + EmbedLinks: true, + SendPolls: true, + ChangeInfo: true, + InviteUsers: true, + PinMessages: true, + UntilDate: 0, + } + expected.SetFlags() + require.Equal(t, expected, rights) + + r.ApplyFor(time.Second) + rights = r.IntoChatBannedRights() + require.NotZero(t, rights.UntilDate) +} diff --git a/telegram/peers/user_test.go b/telegram/peers/user_test.go index 4a576713b5..9727290144 100644 --- a/telegram/peers/user_test.go +++ b/telegram/peers/user_test.go @@ -1,6 +1,7 @@ package peers import ( + "context" "testing" "github.com/stretchr/testify/require" @@ -92,6 +93,18 @@ func TestUserGetters(t *testing.T) { a.Equal(ok, ok2) a.Equal(v, v2) } + { + v, ok := u.raw.GetStatus() + v2, ok2 := u.Status() + a.Equal(ok, ok2) + a.Equal(v, v2) + } + { + v, ok := u.raw.GetLangCode() + v2, ok2 := u.LangCode() + a.Equal(ok, ok2) + a.Equal(v, v2) + } b, ok := u.ToBot() a.True(ok) @@ -131,3 +144,51 @@ func TestUser_VisibleName(t *testing.T) { LastName: "LastName", }}.VisibleName()) } + +func TestUser_ReportSpam(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + u := m.User(getTestUser()) + + mock.ExpectCall(&tg.MessagesReportSpamRequest{Peer: u.InputPeer()}). + ThenRPCErr(getTestError()) + a.Error(u.ReportSpam(ctx)) + + mock.ExpectCall(&tg.MessagesReportSpamRequest{Peer: u.InputPeer()}). + ThenTrue() + a.NoError(u.ReportSpam(ctx)) +} + +func TestUser_Block(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + u := m.User(getTestUser()) + + mock.ExpectCall(&tg.ContactsBlockRequest{ID: u.InputPeer()}). + ThenRPCErr(getTestError()) + a.Error(u.Block(ctx)) + + mock.ExpectCall(&tg.ContactsBlockRequest{ID: u.InputPeer()}). + ThenTrue() + a.NoError(u.Block(ctx)) +} + +func TestUser_Unblock(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + u := m.User(getTestUser()) + + mock.ExpectCall(&tg.ContactsUnblockRequest{ID: u.InputPeer()}). + ThenRPCErr(getTestError()) + a.Error(u.Unblock(ctx)) + + mock.ExpectCall(&tg.ContactsUnblockRequest{ID: u.InputPeer()}). + ThenTrue() + a.NoError(u.Unblock(ctx)) +} From dc25996fce001e46e4a48b053ba6567ae948d19a Mon Sep 17 00:00:00 2001 From: tdakkota Date: Mon, 7 Mar 2022 00:33:06 +0300 Subject: [PATCH 05/10] feat(peers): add id resolvers --- telegram/peers/id.go | 74 +++++++++++++++++++++++++++++++++++++++++ telegram/peers/peers.go | 55 ++---------------------------- 2 files changed, 77 insertions(+), 52 deletions(-) create mode 100644 telegram/peers/id.go diff --git a/telegram/peers/id.go b/telegram/peers/id.go new file mode 100644 index 0000000000..d5afd5b3f7 --- /dev/null +++ b/telegram/peers/id.go @@ -0,0 +1,74 @@ +package peers + +import ( + "context" + + "github.com/go-faster/errors" + + "github.com/gotd/td/constant" + "github.com/gotd/td/tg" + "github.com/gotd/td/tgerr" +) + +// ResolveTDLibID creates Peer using given constant.TDLibPeerID. +func (m *Manager) ResolveTDLibID(ctx context.Context, peerID constant.TDLibPeerID) (p Peer, err error) { + switch { + case peerID.IsUser(): + p, err = m.ResolveUserID(ctx, peerID.ToPlain()) + case peerID.IsChat(): + p, err = m.ResolveChatID(ctx, peerID.ToPlain()) + case peerID.IsChannel(): + p, err = m.ResolveChannelID(ctx, peerID.ToPlain()) + default: + return nil, errors.Errorf("invalid ID %d", peerID) + } + return p, err +} + +// ResolveUserID creates User using given id. +func (m *Manager) ResolveUserID(ctx context.Context, id int64) (User, error) { + v, ok, err := m.storage.Find(ctx, Key{ + Prefix: usersPrefix, + ID: id, + }) + if err != nil { + return User{}, err + } + u, err := m.GetUser(ctx, &tg.InputUser{ + UserID: id, + AccessHash: v.AccessHash, + }) + if !ok && tgerr.Is(err, tg.ErrUserIDInvalid) { + return User{}, &PeerNotFoundError{ + Peer: &tg.PeerUser{UserID: id}, + } + } + return u, err +} + +// ResolveChatID creates Chat using given id. +func (m *Manager) ResolveChatID(ctx context.Context, id int64) (Chat, error) { + c, err := m.GetChat(ctx, id) + return c, err +} + +// ResolveChannelID creates Channel using given id. +func (m *Manager) ResolveChannelID(ctx context.Context, id int64) (Channel, error) { + v, ok, err := m.storage.Find(ctx, Key{ + Prefix: channelPrefix, + ID: id, + }) + if err != nil { + return Channel{}, err + } + c, err := m.GetChannel(ctx, &tg.InputChannel{ + ChannelID: id, + AccessHash: v.AccessHash, + }) + if !ok && tgerr.Is(err, tg.ErrChannelInvalid) { + return Channel{}, &PeerNotFoundError{ + Peer: &tg.PeerChannel{ChannelID: id}, + } + } + return c, err +} diff --git a/telegram/peers/peers.go b/telegram/peers/peers.go index f32f2c2092..b233bfa138 100644 --- a/telegram/peers/peers.go +++ b/telegram/peers/peers.go @@ -54,64 +54,15 @@ var _ = []Peer{ Channel{}, } -// ResolveTDLibID creates Peer using given constant.TDLibPeerID. -func (m *Manager) ResolveTDLibID(ctx context.Context, peerID constant.TDLibPeerID) (Peer, error) { - var p tg.PeerClass - switch { - case peerID.IsUser(): - p = &tg.PeerUser{UserID: peerID.ToPlain()} - case peerID.IsChat(): - p = &tg.PeerChat{ChatID: peerID.ToPlain()} - case peerID.IsChannel(): - p = &tg.PeerChannel{ChannelID: peerID.ToPlain()} - default: - return nil, errors.Errorf("invalid ID %d", peerID) - } - return m.ResolvePeer(ctx, p) -} - // ResolvePeer creates Peer using given tg.PeerClass. func (m *Manager) ResolvePeer(ctx context.Context, p tg.PeerClass) (Peer, error) { switch p := p.(type) { case *tg.PeerUser: - v, ok, err := m.storage.Find(ctx, Key{ - Prefix: usersPrefix, - ID: p.UserID, - }) - if err != nil { - return nil, err - } - u, err := m.GetUser(ctx, &tg.InputUser{ - UserID: p.UserID, - AccessHash: v.AccessHash, - }) - if !ok && tgerr.Is(err, tg.ErrUserIDInvalid) { - return nil, &PeerNotFoundError{ - Peer: p, - } - } - return u, err + return m.ResolveUserID(ctx, p.UserID) case *tg.PeerChat: - c, err := m.GetChat(ctx, p.ChatID) - return c, err + return m.ResolveChatID(ctx, p.ChatID) case *tg.PeerChannel: - v, ok, err := m.storage.Find(ctx, Key{ - Prefix: channelPrefix, - ID: p.ChannelID, - }) - if err != nil { - return nil, err - } - c, err := m.GetChannel(ctx, &tg.InputChannel{ - ChannelID: p.ChannelID, - AccessHash: v.AccessHash, - }) - if !ok && tgerr.Is(err, tg.ErrChannelInvalid) { - return nil, &PeerNotFoundError{ - Peer: p, - } - } - return c, err + return m.ResolveChannelID(ctx, p.ChannelID) default: return nil, errors.Errorf("unexpected type %T", p) } From 4a9ab7bfa3314bc8e3dde38893dcdd2d59a9ae2f Mon Sep 17 00:00:00 2001 From: tdakkota Date: Mon, 7 Mar 2022 00:33:58 +0300 Subject: [PATCH 06/10] feat(peers): add invoker and manager getters for Peer objects --- telegram/peers/channel.go | 5 +++++ telegram/peers/chat.go | 5 +++++ telegram/peers/manager.go | 5 +++++ telegram/peers/peers.go | 3 ++- telegram/peers/user.go | 5 +++++ 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/telegram/peers/channel.go b/telegram/peers/channel.go index 6310c3584c..c551d2ae5d 100644 --- a/telegram/peers/channel.go +++ b/telegram/peers/channel.go @@ -100,6 +100,11 @@ func (c Channel) Sync(ctx context.Context) error { return nil } +// Manager returns attached Manager. +func (c Channel) Manager() *Manager { + return c.m +} + // Report reports a peer for violation of telegram's Terms of Service. func (c Channel) Report(ctx context.Context, reason tg.ReportReasonClass, message string) error { if _, err := c.m.api.AccountReportPeer(ctx, &tg.AccountReportPeerRequest{ diff --git a/telegram/peers/chat.go b/telegram/peers/chat.go index a53d2ebed3..2182e625e7 100644 --- a/telegram/peers/chat.go +++ b/telegram/peers/chat.go @@ -96,6 +96,11 @@ func (c Chat) Sync(ctx context.Context) error { return nil } +// Manager returns attached Manager. +func (c Chat) Manager() *Manager { + return c.m +} + // Report reports a peer for violation of telegram's Terms of Service. func (c Chat) Report(ctx context.Context, reason tg.ReportReasonClass, message string) error { if _, err := c.m.api.AccountReportPeer(ctx, &tg.AccountReportPeerRequest{ diff --git a/telegram/peers/manager.go b/telegram/peers/manager.go index 33b73eb648..e3c5bd6b79 100644 --- a/telegram/peers/manager.go +++ b/telegram/peers/manager.go @@ -39,3 +39,8 @@ func (m *Manager) Init(ctx context.Context) error { } return nil } + +// API returns used Client. +func (m *Manager) API() *tg.Client { + return m.api +} diff --git a/telegram/peers/peers.go b/telegram/peers/peers.go index b233bfa138..5b30dc7ec5 100644 --- a/telegram/peers/peers.go +++ b/telegram/peers/peers.go @@ -11,7 +11,6 @@ import ( "github.com/gotd/td/constant" "github.com/gotd/td/tg" - "github.com/gotd/td/tgerr" ) // Peer represents generic peer. @@ -41,6 +40,8 @@ type Peer interface { InputPeer() tg.InputPeerClass // Sync updates current object. Sync(ctx context.Context) error + // Manager returns attached Manager. + Manager() *Manager // Report reports a peer for violation of telegram's Terms of Service. Report(ctx context.Context, reason tg.ReportReasonClass, message string) error diff --git a/telegram/peers/user.go b/telegram/peers/user.go index acae79a532..02c7064caa 100644 --- a/telegram/peers/user.go +++ b/telegram/peers/user.go @@ -109,6 +109,11 @@ func (u User) Sync(ctx context.Context) error { return nil } +// Manager returns attached Manager. +func (u User) Manager() *Manager { + return u.m +} + // Report reports a peer for violation of telegram's Terms of Service. func (u User) Report(ctx context.Context, reason tg.ReportReasonClass, message string) error { if _, err := u.m.api.AccountReportPeer(ctx, &tg.AccountReportPeerRequest{ From 48073e86f2bb0f9da87d86d905ca29b09be281c1 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 8 Mar 2022 04:52:57 +0300 Subject: [PATCH 07/10] feat(members): add members package --- telegram/peers/members/channel.go | 238 ++++++++++++++++++++++++ telegram/peers/members/channel_test.go | 51 +++++ telegram/peers/members/chat.go | 144 ++++++++++++++ telegram/peers/members/errors.go | 24 +++ telegram/peers/members/members.go | 42 +++++ telegram/peers/members/members_test.go | 38 ++++ telegram/peers/members/status.go | 19 ++ telegram/peers/members/status_string.go | 27 +++ 8 files changed, 583 insertions(+) create mode 100644 telegram/peers/members/channel.go create mode 100644 telegram/peers/members/channel_test.go create mode 100644 telegram/peers/members/chat.go create mode 100644 telegram/peers/members/errors.go create mode 100644 telegram/peers/members/members.go create mode 100644 telegram/peers/members/members_test.go create mode 100644 telegram/peers/members/status.go create mode 100644 telegram/peers/members/status_string.go diff --git a/telegram/peers/members/channel.go b/telegram/peers/members/channel.go new file mode 100644 index 0000000000..8a1d216c4a --- /dev/null +++ b/telegram/peers/members/channel.go @@ -0,0 +1,238 @@ +package members + +import ( + "context" + "time" + + "github.com/go-faster/errors" + + "github.com/gotd/td/telegram/peers" + "github.com/gotd/td/tg" +) + +// ChannelMembers is channel Members. +type ChannelMembers struct { + m *peers.Manager + channel peers.Channel +} + +// ChannelMember is channel Member. +type ChannelMember struct { + parent *ChannelMembers + creatorDate time.Time + user peers.User + inviter peers.User + raw tg.ChannelParticipantClass +} + +// Status returns member Status. +func (c ChannelMember) Status() Status { + switch c.raw.(type) { + case *tg.ChannelParticipant: + return Plain + case *tg.ChannelParticipantSelf: + return Plain + case *tg.ChannelParticipantCreator: + return Creator + case *tg.ChannelParticipantAdmin: + return Admin + case *tg.ChannelParticipantBanned: + return Banned + case *tg.ChannelParticipantLeft: + return Left + default: + return -1 + } +} + +// Rank returns admin "rank". +func (c ChannelMember) Rank() (string, bool) { + switch p := c.raw.(type) { + case *tg.ChannelParticipant: + return "", false + case *tg.ChannelParticipantSelf: + return "", false + case *tg.ChannelParticipantCreator: + return p.GetRank() + case *tg.ChannelParticipantAdmin: + return p.GetRank() + case *tg.ChannelParticipantBanned: + return "", false + case *tg.ChannelParticipantLeft: + return "", false + default: + return "", false + } +} + +// JoinDate returns member join date, if it is available. +func (c ChannelMember) JoinDate() (time.Time, bool) { + switch p := c.raw.(type) { + case *tg.ChannelParticipant: + return time.Unix(int64(p.Date), 0), true + case *tg.ChannelParticipantSelf: + return time.Unix(int64(p.Date), 0), true + case *tg.ChannelParticipantCreator: + return c.creatorDate, false + case *tg.ChannelParticipantAdmin: + return time.Unix(int64(p.Date), 0), true + case *tg.ChannelParticipantBanned: + return time.Unix(int64(p.Date), 0), true + case *tg.ChannelParticipantLeft: + return time.Time{}, false + default: + return time.Time{}, false + } +} + +// InvitedBy returns user that invited this member. +func (c ChannelMember) InvitedBy() (peers.User, bool) { + switch p := c.raw.(type) { + case *tg.ChannelParticipant: + return peers.User{}, false + case *tg.ChannelParticipantSelf: + return c.inviter, true + case *tg.ChannelParticipantCreator: + return peers.User{}, false + case *tg.ChannelParticipantAdmin: + _, has := p.GetInviterID() + return c.inviter, has + case *tg.ChannelParticipantBanned: + return peers.User{}, false + case *tg.ChannelParticipantLeft: + return peers.User{}, false + default: + return peers.User{}, false + } +} + +// User returns member User object. +func (c ChannelMember) User() peers.User { + return c.user +} + +func (c *ChannelMembers) query(ctx context.Context, offset, limit int) (*tg.ChannelsChannelParticipants, error) { + raw := c.m.API() + p, err := raw.ChannelsGetParticipants(ctx, &tg.ChannelsGetParticipantsRequest{ + Channel: c.channel.InputChannel(), + Filter: &tg.ChannelParticipantsRecent{}, + Offset: offset, + Limit: limit, + }) + if err != nil { + return nil, errors.Wrap(err, "get members") + } + + m, ok := p.AsModified() + if !ok { + return nil, errors.Errorf("unexpected type %T", p) + } + if err := c.m.Apply(ctx, m.Users, m.Chats); err != nil { + return nil, errors.Wrap(err, "apply entities") + } + return m, nil +} + +// ForEach calls cb for every member of channel. +// +// May return ChannelInfoUnavailableError. +func (c *ChannelMembers) ForEach(ctx context.Context, cb Callback) error { + const limit = 100 + + full, err := c.channel.FullRaw(ctx) + if err != nil { + return errors.Wrap(err, "get full") + } + if !full.CanViewParticipants { + return &ChannelInfoUnavailableError{} + } + channelDate := time.Unix(int64(c.channel.Raw().Date), 0) + + offset := 0 + for { + m, err := c.query(ctx, offset, limit) + if err != nil { + return errors.Wrap(err, "query") + } + + if len(m.Participants) < 1 { + return nil + } + for i, participant := range m.Participants { + var ( + userID int64 + inviterID int64 + err error + ) + switch p := participant.(type) { + case *tg.ChannelParticipant: + userID = p.UserID + case *tg.ChannelParticipantSelf: + userID = p.UserID + inviterID = p.InviterID + case *tg.ChannelParticipantCreator: + userID = p.UserID + case *tg.ChannelParticipantAdmin: + userID = p.UserID + inviterID = p.InviterID + case *tg.ChannelParticipantBanned: + userPeer, ok := p.Peer.(*tg.PeerUser) + if !ok { + return errors.Errorf("unexpected type %T", p.Peer) + } + userID = userPeer.UserID + case *tg.ChannelParticipantLeft: + userPeer, ok := p.Peer.(*tg.PeerUser) + if !ok { + return errors.Errorf("unexpected type %T", p.Peer) + } + userID = userPeer.UserID + default: + return errors.Errorf("unexpected type %T", p) + } + + user, err := c.m.ResolveUserID(ctx, userID) + if err != nil { + return errors.Wrapf(err, "get member %d", userID) + } + member := ChannelMember{ + parent: c, + creatorDate: channelDate, + user: user, + inviter: peers.User{}, + raw: participant, + } + if inviterID != 0 { + inviter, err := c.m.ResolveUserID(ctx, inviterID) + if err != nil { + return errors.Wrapf(err, "get inviter %d", inviterID) + } + member.inviter = inviter + } + + if err := cb(member); err != nil { + return errors.Wrapf(err, "callback (index: %d)", i) + } + } + + offset += limit + } +} + +// Count returns total count of members. +func (c *ChannelMembers) Count(ctx context.Context) (int, error) { + m, err := c.query(ctx, 0, 1) + if err != nil { + return 0, errors.Wrap(err, "query") + } + return m.Count, nil +} + +// Channel returns recent channel members. +func Channel(ctx context.Context, channel peers.Channel) (*ChannelMembers, error) { + m := channel.Manager() + return &ChannelMembers{ + m: m, + channel: channel, + }, nil +} diff --git a/telegram/peers/members/channel_test.go b/telegram/peers/members/channel_test.go new file mode 100644 index 0000000000..a196ea9362 --- /dev/null +++ b/telegram/peers/members/channel_test.go @@ -0,0 +1,51 @@ +package members + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gotd/td/internal/testutil" + "github.com/gotd/td/tg" +) + +func TestChannelMembers_Count(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + ch := m.Channel(getTestChannel()) + members, err := Channel(ctx, ch) + a.NoError(err) + + mock.ExpectCall(&tg.ChannelsGetParticipantsRequest{ + Channel: ch.InputChannel(), + Filter: &tg.ChannelParticipantsRecent{}, + Offset: 0, + Limit: 1, + }).ThenErr(testutil.TestError()) + _, err = members.Count(ctx) + a.Error(err) + + mock.ExpectCall(&tg.ChannelsGetParticipantsRequest{ + Channel: ch.InputChannel(), + Filter: &tg.ChannelParticipantsRecent{}, + Offset: 0, + Limit: 1, + }).ThenResult(&tg.ChannelsChannelParticipantsNotModified{}) + _, err = members.Count(ctx) + a.Error(err) + + mock.ExpectCall(&tg.ChannelsGetParticipantsRequest{ + Channel: ch.InputChannel(), + Filter: &tg.ChannelParticipantsRecent{}, + Offset: 0, + Limit: 1, + }).ThenResult(&tg.ChannelsChannelParticipants{ + Count: 10, + }) + count, err := members.Count(ctx) + a.NoError(err) + a.Equal(10, count) +} diff --git a/telegram/peers/members/chat.go b/telegram/peers/members/chat.go new file mode 100644 index 0000000000..d2f945f263 --- /dev/null +++ b/telegram/peers/members/chat.go @@ -0,0 +1,144 @@ +package members + +import ( + "context" + "time" + + "github.com/go-faster/errors" + + "github.com/gotd/td/telegram/peers" + "github.com/gotd/td/tg" +) + +// ChatMembers is chat Members. +type ChatMembers struct { + m *peers.Manager + chat peers.Chat + p []ChatMember +} + +// ChatMember is chat Member. +type ChatMember struct { + parent *ChatMembers + creatorDate time.Time + user peers.User + inviter peers.User + raw tg.ChatParticipantClass +} + +// Status returns member Status. +func (c ChatMember) Status() Status { + switch c.raw.(type) { + case *tg.ChatParticipant: + return Plain + case *tg.ChatParticipantCreator: + return Creator + case *tg.ChatParticipantAdmin: + return Admin + default: + return -1 + } +} + +// JoinDate returns member join date, if it is available. +func (c ChatMember) JoinDate() (time.Time, bool) { + switch p := c.raw.(type) { + case *tg.ChatParticipant: + return time.Unix(int64(p.Date), 0), true + case *tg.ChatParticipantCreator: + return c.creatorDate, true + case *tg.ChatParticipantAdmin: + return time.Unix(int64(p.Date), 0), true + default: + return time.Time{}, false + } +} + +// InvitedBy returns user that invited this member. +func (c ChatMember) InvitedBy() (peers.User, bool) { + switch c.raw.(type) { + case *tg.ChatParticipant: + return c.inviter, true + case *tg.ChatParticipantCreator: + return peers.User{}, false + case *tg.ChatParticipantAdmin: + return c.inviter, true + default: + return peers.User{}, false + } +} + +// User returns member User object. +func (c ChatMember) User() peers.User { + return c.user +} + +// ForEach calls cb for every member of chat. +func (c *ChatMembers) ForEach(ctx context.Context, cb Callback) error { + for i, p := range c.p { + if err := cb(p); err != nil { + return errors.Wrapf(err, "callback (index: %d)", i) + } + } + return nil +} + +// Count returns total count of members. +func (c *ChatMembers) Count(ctx context.Context) (int, error) { + return len(c.p), nil +} + +// Chat returns recent chat members. +// +// May return ChatInfoUnavailableError. +func Chat(ctx context.Context, chat peers.Chat) (*ChatMembers, error) { + full, err := chat.FullRaw(ctx) + if err != nil { + return nil, errors.Wrap(err, "get full") + } + m := chat.Manager() + chatDate := time.Unix(int64(chat.Raw().Date), 0) + + switch p := full.Participants.(type) { + case *tg.ChatParticipantsForbidden: + return nil, &ChatInfoUnavailableError{Info: p} + case *tg.ChatParticipants: + members := make([]ChatMember, len(p.Participants)) + result := &ChatMembers{ + m: m, + chat: chat, + p: members, + } + + for i, participant := range p.Participants { + userID := participant.GetUserID() + user, err := m.ResolveUserID(ctx, userID) + if err != nil { + return nil, errors.Wrapf(err, "get member %d", userID) + } + + var inviter peers.User + switch p := participant.(type) { + case *tg.ChatParticipant: + inviter, err = m.ResolveUserID(ctx, p.InviterID) + case *tg.ChatParticipantAdmin: + inviter, err = m.ResolveUserID(ctx, p.InviterID) + } + if err != nil { + return nil, errors.Wrap(err, "get inviter") + } + + members[i] = ChatMember{ + parent: result, + creatorDate: chatDate, + user: user, + inviter: inviter, + raw: participant, + } + } + + return result, nil + default: + return nil, errors.Errorf("unexpected type %T", p) + } +} diff --git a/telegram/peers/members/errors.go b/telegram/peers/members/errors.go new file mode 100644 index 0000000000..0f646eb09d --- /dev/null +++ b/telegram/peers/members/errors.go @@ -0,0 +1,24 @@ +package members + +import ( + "github.com/gotd/td/tg" +) + +// ChatInfoUnavailableError reports that chat members info is not available. +type ChatInfoUnavailableError struct { + Info *tg.ChatParticipantsForbidden +} + +// Error implements error. +func (c *ChatInfoUnavailableError) Error() string { + return "chat members info is unavailable" +} + +// ChannelInfoUnavailableError reports that channel members info is not available. +type ChannelInfoUnavailableError struct { +} + +// Error implements error. +func (c *ChannelInfoUnavailableError) Error() string { + return "channel members info is unavailable" +} diff --git a/telegram/peers/members/members.go b/telegram/peers/members/members.go new file mode 100644 index 0000000000..1f08bed85a --- /dev/null +++ b/telegram/peers/members/members.go @@ -0,0 +1,42 @@ +// Package members defines interfaces for working with chat/channel members. +package members + +import ( + "context" + "time" + + "github.com/gotd/td/telegram/peers" +) + +var _ = []Member{ + ChatMember{}, + ChannelMember{}, +} + +// Member represents chat/channel member. +type Member interface { + // Status returns member Status. + Status() Status + // JoinDate returns member join date, if it is available. + JoinDate() (time.Time, bool) + // InvitedBy returns user that invited this member. + InvitedBy() (peers.User, bool) + // User returns member User object. + User() peers.User +} + +// Callback is type for member iterator callback. +type Callback = func(p Member) error + +var _ = []Members{ + &ChatMembers{}, + &ChannelMembers{}, +} + +// Members represents chat/channel members. +type Members interface { + // ForEach calls cb for every member of chat/channel. + ForEach(ctx context.Context, cb Callback) error + // Count returns total count of members. + Count(ctx context.Context) (int, error) +} diff --git a/telegram/peers/members/members_test.go b/telegram/peers/members/members_test.go new file mode 100644 index 0000000000..c5242f9988 --- /dev/null +++ b/telegram/peers/members/members_test.go @@ -0,0 +1,38 @@ +package members + +import ( + "testing" + "time" + + "go.uber.org/zap/zaptest" + + "github.com/gotd/td/telegram/peers" + "github.com/gotd/td/tg" + "github.com/gotd/td/tgmock" +) + +func testManager(t *testing.T) (*tgmock.Mock, *peers.Manager) { + mock := tgmock.New(t) + return mock, peers.Options{ + Logger: zaptest.NewLogger(t), + Cache: &peers.InmemoryCache{}, + }.Build(tg.NewClient(mock)) +} + +func getTestChannel() *tg.Channel { + return &tg.Channel{ + Broadcast: true, + Noforwards: true, + ID: 11, + AccessHash: 11, + Title: "I hate mondays", + Username: "", + Photo: &tg.ChatPhotoEmpty{}, + Date: int(time.Now().Unix()), + RestrictionReason: nil, + AdminRights: tg.ChatAdminRights{}, + BannedRights: tg.ChatBannedRights{}, + DefaultBannedRights: tg.ChatBannedRights{}, + ParticipantsCount: 1, + } +} diff --git a/telegram/peers/members/status.go b/telegram/peers/members/status.go new file mode 100644 index 0000000000..65d8a1ec97 --- /dev/null +++ b/telegram/peers/members/status.go @@ -0,0 +1,19 @@ +package members + +//go:generate go run -modfile=../../../_tools/go.mod golang.org/x/tools/cmd/stringer -type=Status + +// Status defines participant status. +type Status int + +const ( + // Plain is status for plain participant. + Plain Status = iota + // Creator is status for chat/channel creator. + Creator + // Admin is status for chat/channel admin. + Admin + // Banned is status for banned user. + Banned + // Left is status for user that left chat/channel. + Left +) diff --git a/telegram/peers/members/status_string.go b/telegram/peers/members/status_string.go new file mode 100644 index 0000000000..1066720d88 --- /dev/null +++ b/telegram/peers/members/status_string.go @@ -0,0 +1,27 @@ +// Code generated by "stringer -type=Status"; DO NOT EDIT. + +package members + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Plain-0] + _ = x[Creator-1] + _ = x[Admin-2] + _ = x[Banned-3] + _ = x[Left-4] +} + +const _Status_name = "PlainCreatorAdminBannedLeft" + +var _Status_index = [...]uint8{0, 5, 12, 17, 23, 27} + +func (i Status) String() string { + if i < 0 || i >= Status(len(_Status_index)-1) { + return "Status(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Status_name[_Status_index[i]:_Status_index[i+1]] +} From 2ba8a0fb0b20ec14f67b2ec4d6a486702cb6860b Mon Sep 17 00:00:00 2001 From: tdakkota Date: Wed, 9 Mar 2022 02:58:58 +0300 Subject: [PATCH 08/10] test(members): add tests for members --- telegram/peers/members/channel.go | 2 +- telegram/peers/members/channel_test.go | 123 +++++++++++++++++++++++++ telegram/peers/members/chat_test.go | 100 ++++++++++++++++++++ telegram/peers/members/errors_test.go | 15 +++ telegram/peers/members/members_test.go | 39 ++++++++ 5 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 telegram/peers/members/chat_test.go create mode 100644 telegram/peers/members/errors_test.go diff --git a/telegram/peers/members/channel.go b/telegram/peers/members/channel.go index 8a1d216c4a..6d17b2af70 100644 --- a/telegram/peers/members/channel.go +++ b/telegram/peers/members/channel.go @@ -73,7 +73,7 @@ func (c ChannelMember) JoinDate() (time.Time, bool) { case *tg.ChannelParticipantSelf: return time.Unix(int64(p.Date), 0), true case *tg.ChannelParticipantCreator: - return c.creatorDate, false + return c.creatorDate, true case *tg.ChannelParticipantAdmin: return time.Unix(int64(p.Date), 0), true case *tg.ChannelParticipantBanned: diff --git a/telegram/peers/members/channel_test.go b/telegram/peers/members/channel_test.go index a196ea9362..15ee8c5c5a 100644 --- a/telegram/peers/members/channel_test.go +++ b/telegram/peers/members/channel_test.go @@ -3,6 +3,7 @@ package members import ( "context" "testing" + "time" "github.com/stretchr/testify/require" @@ -49,3 +50,125 @@ func TestChannelMembers_Count(t *testing.T) { a.NoError(err) a.Equal(10, count) } + +func TestChannelMembers_ForEach(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + now := time.Now() + date := int(now.Unix()) + + rawCh := getTestChannel() + rawCh.Date = date + ch := m.Channel(rawCh) + members, err := Channel(ctx, ch) + a.NoError(err) + + mock.ExpectCall(&tg.ChannelsGetFullChannelRequest{ + Channel: ch.InputChannel(), + }).ThenResult(&tg.MessagesChatFull{ + FullChat: getTestChannelFull(), + }) + mock.ExpectCall(&tg.ChannelsGetParticipantsRequest{ + Channel: ch.InputChannel(), + Filter: &tg.ChannelParticipantsRecent{}, + Offset: 0, + Limit: 100, + }).ThenResult(&tg.ChannelsChannelParticipants{ + Count: 10, + Participants: []tg.ChannelParticipantClass{ + &tg.ChannelParticipant{ + UserID: 10, + Date: date, + }, + &tg.ChannelParticipantSelf{ + UserID: 10, + InviterID: 11, + Date: date, + }, + &tg.ChannelParticipantCreator{ + UserID: 10, + Rank: "rank", + }, + &tg.ChannelParticipantAdmin{ + UserID: 10, + InviterID: 11, + Date: date, + Rank: "rank", + }, + &tg.ChannelParticipantBanned{ + Peer: &tg.PeerUser{UserID: 10}, + Date: date, + }, + &tg.ChannelParticipantLeft{ + Peer: &tg.PeerUser{UserID: 10}, + }, + }, + Users: []tg.UserClass{ + &tg.User{ + ID: 10, + AccessHash: 10, + }, + &tg.User{ + ID: 11, + AccessHash: 10, + }, + }, + }).ExpectCall(&tg.ChannelsGetParticipantsRequest{ + Channel: ch.InputChannel(), + Filter: &tg.ChannelParticipantsRecent{}, + Offset: 100, + Limit: 100, + }).ThenResult(&tg.ChannelsChannelParticipants{ + Count: 10, + }) + + expected := []struct { + Status Status + JoinDate time.Time + JoinDateSet bool + Rank string + RankSet bool + InviterID int64 + }{ + {Status: Plain, JoinDate: now, JoinDateSet: true}, + {Status: Plain, JoinDate: now, JoinDateSet: true, InviterID: 11}, + {Status: Creator, Rank: "rank", RankSet: true, JoinDate: now, JoinDateSet: true}, + {Status: Admin, Rank: "rank", RankSet: true, JoinDate: now, JoinDateSet: true, InviterID: 11}, + {Status: Banned, JoinDate: now, JoinDateSet: true}, + {Status: Left}, + } + + i := 0 + a.NoError(members.ForEach(ctx, func(m Member) error { + p := m.(ChannelMember) + e := expected[i] + + a.Equal(e.Status, p.Status(), i) + a.Equal(int64(10), p.User().ID()) + if join, ok := p.JoinDate(); e.JoinDateSet { + a.True(ok, i) + a.Equal(e.JoinDate.Unix(), join.Unix(), i) + } else { + a.False(ok, i) + } + + if rank, ok := p.Rank(); e.RankSet { + a.True(ok, i) + a.Equal(e.Rank, rank, i) + } else { + a.False(ok, i) + } + + if inviter, ok := p.InvitedBy(); e.InviterID != 0 { + a.True(ok, i) + a.Equal(e.InviterID, inviter.ID()) + } else { + a.False(ok, i) + } + + i++ + return nil + })) +} diff --git a/telegram/peers/members/chat_test.go b/telegram/peers/members/chat_test.go new file mode 100644 index 0000000000..619034e51e --- /dev/null +++ b/telegram/peers/members/chat_test.go @@ -0,0 +1,100 @@ +package members + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/gotd/td/tg" +) + +func TestChat(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + now := time.Now() + date := int(now.Unix()) + + rawCh := getTestChat() + rawCh.Date = date + ch := m.Chat(rawCh) + + mock.ExpectCall(&tg.MessagesGetFullChatRequest{ + ChatID: ch.ID(), + }).ThenResult(&tg.MessagesChatFull{ + FullChat: getTestChatFull(&tg.ChatParticipants{ + ChatID: 10, + Participants: []tg.ChatParticipantClass{ + &tg.ChatParticipant{ + UserID: 10, + InviterID: 11, + Date: date, + }, + &tg.ChatParticipantCreator{ + UserID: 10, + }, + &tg.ChatParticipantAdmin{ + UserID: 10, + InviterID: 11, + Date: date, + }, + }, + Version: 1, + }), + Users: []tg.UserClass{ + &tg.User{ + ID: 10, + AccessHash: 10, + }, + &tg.User{ + ID: 11, + AccessHash: 10, + }, + }, + }) + members, err := Chat(ctx, ch) + a.NoError(err) + + count, err := members.Count(ctx) + a.Equal(3, count) + a.NoError(err) + + expected := []struct { + Status Status + JoinDate time.Time + JoinDateSet bool + InviterID int64 + }{ + {Status: Plain, JoinDate: now, JoinDateSet: true, InviterID: 11}, + {Status: Creator, JoinDate: now, JoinDateSet: true}, + {Status: Admin, JoinDate: now, JoinDateSet: true, InviterID: 11}, + } + + i := 0 + a.NoError(members.ForEach(ctx, func(m Member) error { + p := m.(ChatMember) + e := expected[i] + + a.Equal(e.Status, p.Status(), i) + a.Equal(int64(10), p.User().ID()) + if join, ok := p.JoinDate(); e.JoinDateSet { + a.True(ok, i) + a.Equal(e.JoinDate.Unix(), join.Unix(), i) + } else { + a.False(ok, i) + } + + if inviter, ok := p.InvitedBy(); e.InviterID != 0 { + a.True(ok, i) + a.Equal(e.InviterID, inviter.ID()) + } else { + a.False(ok, i) + } + + i++ + return nil + })) +} diff --git a/telegram/peers/members/errors_test.go b/telegram/peers/members/errors_test.go new file mode 100644 index 0000000000..cc5b500dbd --- /dev/null +++ b/telegram/peers/members/errors_test.go @@ -0,0 +1,15 @@ +package members + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestChatInfoUnavailableError_Error(t *testing.T) { + require.Equal(t, (&ChatInfoUnavailableError{}).Error(), "chat members info is unavailable") +} + +func TestChannelInfoUnavailableError_Error(t *testing.T) { + require.Equal(t, (&ChannelInfoUnavailableError{}).Error(), "channel members info is unavailable") +} diff --git a/telegram/peers/members/members_test.go b/telegram/peers/members/members_test.go index c5242f9988..9274a5ab28 100644 --- a/telegram/peers/members/members_test.go +++ b/telegram/peers/members/members_test.go @@ -36,3 +36,42 @@ func getTestChannel() *tg.Channel { ParticipantsCount: 1, } } + +func getTestChannelFull() *tg.ChannelFull { + u := &tg.ChannelFull{ + CanViewParticipants: true, + HasScheduled: true, + ID: 11, + About: "garfield blog", + ParticipantsCount: 1, + ChatPhoto: &tg.PhotoEmpty{}, + } + u.SetFlags() + return u +} + +func getTestChat() *tg.Chat { + u := &tg.Chat{ + Noforwards: true, + ID: 10, + Title: "I hate mondays", + ParticipantsCount: 1, + Date: int(time.Now().Unix()), + Version: 1, + Photo: &tg.ChatPhotoEmpty{}, + } + u.SetFlags() + return u +} + +func getTestChatFull(participants tg.ChatParticipantsClass) *tg.ChatFull { + u := &tg.ChatFull{ + CanSetUsername: false, + HasScheduled: true, + ID: 10, + About: "garfield blog", + Participants: participants, + } + u.SetFlags() + return u +} From 1f93a3996a32691d144934632d1d222781e9077e Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 10 Mar 2022 02:59:30 +0300 Subject: [PATCH 09/10] refactor(peers): move rights editing to members package --- telegram/peers/channel.go | 67 ------- telegram/peers/channel_test.go | 62 ------- telegram/peers/chat.go | 24 --- telegram/peers/members/channel.go | 182 +++++++++---------- telegram/peers/members/channel_member.go | 108 ++++++++++++ telegram/peers/members/channel_test.go | 89 +++++++++- telegram/peers/members/chat.go | 184 +++++++++----------- telegram/peers/members/chat_member.go | 69 ++++++++ telegram/peers/members/chat_test.go | 107 +++++++++++- telegram/peers/members/common.go | 39 +++++ telegram/peers/members/members.go | 9 + telegram/peers/members/members_test.go | 51 ++++++ telegram/peers/{ => members}/rights.go | 12 +- telegram/peers/{ => members}/rights_test.go | 10 +- telegram/peers/multichat.go | 10 -- telegram/peers/multichat_test.go | 29 --- telegram/peers/query.go | 20 --- 17 files changed, 638 insertions(+), 434 deletions(-) create mode 100644 telegram/peers/members/channel_member.go create mode 100644 telegram/peers/members/chat_member.go create mode 100644 telegram/peers/members/common.go rename telegram/peers/{ => members}/rights.go (92%) rename telegram/peers/{ => members}/rights_test.go (85%) diff --git a/telegram/peers/channel.go b/telegram/peers/channel.go index c551d2ae5d..c5e5a65bee 100644 --- a/telegram/peers/channel.go +++ b/telegram/peers/channel.go @@ -296,71 +296,4 @@ func (c Channel) DisableReactions(ctx context.Context) error { return c.m.editReactions(ctx, c.InputPeer()) } -// KickUser kicks user participant. -// -// Needed for parity with Chat to define common interface. -// -// If revokeHistory is set, will delete all messages from this participant. -func (c Channel) KickUser(ctx context.Context, participant tg.InputUserClass, revokeHistory bool) error { - p := convertInputUserToInputPeer(participant) - if revokeHistory { - if _, err := c.m.api.ChannelsDeleteParticipantHistory(ctx, &tg.ChannelsDeleteParticipantHistoryRequest{ - Channel: c.InputChannel(), - Participant: p, - }); err != nil { - return errors.Wrap(err, "revoke history") - } - } - return c.KickParticipant(ctx, p) -} - -// KickParticipant kicks participant. -func (c Channel) KickParticipant(ctx context.Context, participant tg.InputPeerClass) error { - return c.EditParticipantRights(ctx, participant, ParticipantRights{ - DenyViewMessages: true, - }) -} - -// EditParticipantRights edits participant rights in this channel. -func (c Channel) EditParticipantRights( - ctx context.Context, - participant tg.InputPeerClass, - options ParticipantRights, -) error { - return c.editParticipantRights(ctx, participant, options) -} - -func (c Channel) editParticipantRights(ctx context.Context, p tg.InputPeerClass, options ParticipantRights) error { - if _, err := c.m.api.ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{ - Channel: c.InputChannel(), - Participant: p, - BannedRights: options.IntoChatBannedRights(), - }); err != nil { - return errors.Wrap(err, "edit participant rights") - } - return nil -} - -// EditRights edits rights of all participants in this channel. -func (c Channel) EditRights(ctx context.Context, options ParticipantRights) error { - return c.m.editDefaultRights(ctx, c.InputPeer(), options) -} - -// EditAdminRights edits admin rights of given user in this channel. -func (c Channel) EditAdminRights( - ctx context.Context, - admin tg.InputUserClass, - options AdminRights, -) error { - if _, err := c.m.api.ChannelsEditAdmin(ctx, &tg.ChannelsEditAdminRequest{ - Channel: c.InputChannel(), - UserID: admin, - AdminRights: options.IntoChatAdminRights(), - Rank: options.Rank, - }); err != nil { - return errors.Wrap(err, "edit admin rights") - } - return nil -} - // TODO(tdakkota): add more getters, helpers and convertors diff --git a/telegram/peers/channel_test.go b/telegram/peers/channel_test.go index 7d3e11a42c..94b89f39e2 100644 --- a/telegram/peers/channel_test.go +++ b/telegram/peers/channel_test.go @@ -188,65 +188,3 @@ func TestChannel_Delete(t *testing.T) { }).ThenResult(&tg.Updates{}) a.NoError(ch.Delete(ctx)) } - -func TestChannel_KickUser(t *testing.T) { - a := require.New(t) - ctx := context.Background() - mock, m := testManager(t) - - u := m.User(getTestUser()) - ch := m.Channel(getTestChannel()) - rights := tg.ChatBannedRights{ - ViewMessages: true, - } - rights.SetFlags() - - mock.ExpectCall(&tg.ChannelsEditBannedRequest{ - Channel: ch.InputChannel(), - Participant: u.InputPeer(), - BannedRights: rights, - }).ThenRPCErr(getTestError()) - a.Error(ch.KickUser(ctx, u.InputUser(), false)) - - mock.ExpectCall(&tg.ChannelsEditBannedRequest{ - Channel: ch.InputChannel(), - Participant: u.InputPeer(), - BannedRights: rights, - }).ThenResult(&tg.Updates{}) - a.NoError(ch.KickUser(ctx, u.InputUser(), false)) -} - -func TestChannel_EditAdminRights(t *testing.T) { - a := require.New(t) - ctx := context.Background() - mock, m := testManager(t) - - u := m.User(getTestUser()) - ch := m.Channel(getTestChannel()) - rights := tg.ChatAdminRights{ - AddAdmins: true, - } - rights.SetFlags() - - mock.ExpectCall(&tg.ChannelsEditAdminRequest{ - Channel: ch.InputChannel(), - UserID: u.InputUser(), - AdminRights: rights, - Rank: "rank", - }).ThenRPCErr(getTestError()) - a.Error(ch.EditAdminRights(ctx, u.InputUser(), AdminRights{ - Rank: "rank", - AddAdmins: true, - })) - - mock.ExpectCall(&tg.ChannelsEditAdminRequest{ - Channel: ch.InputChannel(), - UserID: u.InputUser(), - AdminRights: rights, - Rank: "rank", - }).ThenResult(&tg.Updates{}) - a.NoError(ch.EditAdminRights(ctx, u.InputUser(), AdminRights{ - Rank: "rank", - AddAdmins: true, - })) -} diff --git a/telegram/peers/chat.go b/telegram/peers/chat.go index 2182e625e7..699d429299 100644 --- a/telegram/peers/chat.go +++ b/telegram/peers/chat.go @@ -296,28 +296,4 @@ func (c Chat) deleteUser(ctx context.Context, user tg.InputUserClass, revokeHist return nil } -// KickUser kicks user participant. -// -// If revokeHistory is set, will delete all messages from this participant. -func (c Chat) KickUser(ctx context.Context, participant tg.InputUserClass, revokeHistory bool) error { - return c.deleteUser(ctx, participant, revokeHistory) -} - -// EditRights edits rights of all participants in this channel. -func (c Chat) EditRights(ctx context.Context, options ParticipantRights) error { - return c.m.editDefaultRights(ctx, c.InputPeer(), options) -} - -// EditAdmin edits admin rights for given user. -func (c Chat) EditAdmin(ctx context.Context, user tg.InputUserClass, isAdmin bool) error { - if _, err := c.m.api.MessagesEditChatAdmin(ctx, &tg.MessagesEditChatAdminRequest{ - ChatID: c.ID(), - UserID: user, - IsAdmin: isAdmin, - }); err != nil { - return errors.Wrap(err, "edit admin") - } - return nil -} - // TODO(tdakkota): add more getters, helpers and convertors diff --git a/telegram/peers/members/channel.go b/telegram/peers/members/channel.go index 6d17b2af70..56e9eb51c7 100644 --- a/telegram/peers/members/channel.go +++ b/telegram/peers/members/channel.go @@ -16,101 +16,6 @@ type ChannelMembers struct { channel peers.Channel } -// ChannelMember is channel Member. -type ChannelMember struct { - parent *ChannelMembers - creatorDate time.Time - user peers.User - inviter peers.User - raw tg.ChannelParticipantClass -} - -// Status returns member Status. -func (c ChannelMember) Status() Status { - switch c.raw.(type) { - case *tg.ChannelParticipant: - return Plain - case *tg.ChannelParticipantSelf: - return Plain - case *tg.ChannelParticipantCreator: - return Creator - case *tg.ChannelParticipantAdmin: - return Admin - case *tg.ChannelParticipantBanned: - return Banned - case *tg.ChannelParticipantLeft: - return Left - default: - return -1 - } -} - -// Rank returns admin "rank". -func (c ChannelMember) Rank() (string, bool) { - switch p := c.raw.(type) { - case *tg.ChannelParticipant: - return "", false - case *tg.ChannelParticipantSelf: - return "", false - case *tg.ChannelParticipantCreator: - return p.GetRank() - case *tg.ChannelParticipantAdmin: - return p.GetRank() - case *tg.ChannelParticipantBanned: - return "", false - case *tg.ChannelParticipantLeft: - return "", false - default: - return "", false - } -} - -// JoinDate returns member join date, if it is available. -func (c ChannelMember) JoinDate() (time.Time, bool) { - switch p := c.raw.(type) { - case *tg.ChannelParticipant: - return time.Unix(int64(p.Date), 0), true - case *tg.ChannelParticipantSelf: - return time.Unix(int64(p.Date), 0), true - case *tg.ChannelParticipantCreator: - return c.creatorDate, true - case *tg.ChannelParticipantAdmin: - return time.Unix(int64(p.Date), 0), true - case *tg.ChannelParticipantBanned: - return time.Unix(int64(p.Date), 0), true - case *tg.ChannelParticipantLeft: - return time.Time{}, false - default: - return time.Time{}, false - } -} - -// InvitedBy returns user that invited this member. -func (c ChannelMember) InvitedBy() (peers.User, bool) { - switch p := c.raw.(type) { - case *tg.ChannelParticipant: - return peers.User{}, false - case *tg.ChannelParticipantSelf: - return c.inviter, true - case *tg.ChannelParticipantCreator: - return peers.User{}, false - case *tg.ChannelParticipantAdmin: - _, has := p.GetInviterID() - return c.inviter, has - case *tg.ChannelParticipantBanned: - return peers.User{}, false - case *tg.ChannelParticipantLeft: - return peers.User{}, false - default: - return peers.User{}, false - } -} - -// User returns member User object. -func (c ChannelMember) User() peers.User { - return c.user -} - func (c *ChannelMembers) query(ctx context.Context, offset, limit int) (*tg.ChannelsChannelParticipants, error) { raw := c.m.API() p, err := raw.ChannelsGetParticipants(ctx, &tg.ChannelsGetParticipantsRequest{ @@ -158,13 +63,13 @@ func (c *ChannelMembers) ForEach(ctx context.Context, cb Callback) error { if len(m.Participants) < 1 { return nil } - for i, participant := range m.Participants { + for i, member := range m.Participants { var ( userID int64 inviterID int64 err error ) - switch p := participant.(type) { + switch p := member.(type) { case *tg.ChannelParticipant: userID = p.UserID case *tg.ChannelParticipantSelf: @@ -200,7 +105,7 @@ func (c *ChannelMembers) ForEach(ctx context.Context, cb Callback) error { creatorDate: channelDate, user: user, inviter: peers.User{}, - raw: participant, + raw: member, } if inviterID != 0 { inviter, err := c.m.ResolveUserID(ctx, inviterID) @@ -228,11 +133,84 @@ func (c *ChannelMembers) Count(ctx context.Context) (int, error) { return m.Count, nil } +// Peer returns chat object. +func (c *ChannelMembers) Peer() peers.Peer { + return c.channel +} + +// Kick kicks user member. +// +// Needed for parity with ChatMembers to define common interface. +// +// If revokeHistory is set, will delete all messages from this member. +func (c *ChannelMembers) Kick(ctx context.Context, member tg.InputUserClass, revokeHistory bool) error { + p := convertInputUserToInputPeer(member) + if revokeHistory { + if _, err := c.m.API().ChannelsDeleteParticipantHistory(ctx, &tg.ChannelsDeleteParticipantHistoryRequest{ + Channel: c.channel.InputChannel(), + Participant: p, + }); err != nil { + return errors.Wrap(err, "revoke history") + } + } + return c.KickMember(ctx, p) +} + +// KickMember kicks member. +// +// Unlike Kick, KickMember can be used to kick chat member that uses send-as-channel mode. +func (c *ChannelMembers) KickMember(ctx context.Context, member tg.InputPeerClass) error { + return c.EditMemberRights(ctx, member, MemberRights{ + DenyViewMessages: true, + }) +} + +// EditMemberRights edits member rights in this channel. +func (c *ChannelMembers) EditMemberRights( + ctx context.Context, + member tg.InputPeerClass, + options MemberRights, +) error { + return c.editMemberRights(ctx, member, options) +} + +func (c *ChannelMembers) editMemberRights(ctx context.Context, p tg.InputPeerClass, options MemberRights) error { + if _, err := c.m.API().ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{ + Channel: c.channel.InputChannel(), + Participant: p, + BannedRights: options.IntoChatBannedRights(), + }); err != nil { + return errors.Wrap(err, "edit member rights") + } + return nil +} + +// EditRights edits rights of all members in this channel. +func (c *ChannelMembers) EditRights(ctx context.Context, options MemberRights) error { + return editDefaultRights(ctx, c.m.API(), c.channel.InputPeer(), options) +} + +// EditAdminRights edits admin rights of given user in this channel. +func (c *ChannelMembers) EditAdminRights( + ctx context.Context, + admin tg.InputUserClass, + options AdminRights, +) error { + if _, err := c.m.API().ChannelsEditAdmin(ctx, &tg.ChannelsEditAdminRequest{ + Channel: c.channel.InputChannel(), + UserID: admin, + AdminRights: options.IntoChatAdminRights(), + Rank: options.Rank, + }); err != nil { + return errors.Wrap(err, "edit admin rights") + } + return nil +} + // Channel returns recent channel members. -func Channel(ctx context.Context, channel peers.Channel) (*ChannelMembers, error) { - m := channel.Manager() +func Channel(channel peers.Channel) *ChannelMembers { return &ChannelMembers{ - m: m, + m: channel.Manager(), channel: channel, - }, nil + } } diff --git a/telegram/peers/members/channel_member.go b/telegram/peers/members/channel_member.go new file mode 100644 index 0000000000..4ea6c48d92 --- /dev/null +++ b/telegram/peers/members/channel_member.go @@ -0,0 +1,108 @@ +package members + +import ( + "time" + + "github.com/gotd/td/telegram/peers" + "github.com/gotd/td/tg" +) + +// ChannelMember is channel Member. +type ChannelMember struct { + parent *ChannelMembers + creatorDate time.Time + user peers.User + inviter peers.User + raw tg.ChannelParticipantClass +} + +// Raw returns raw member object. +func (c ChannelMember) Raw() tg.ChannelParticipantClass { + return c.raw +} + +// Status returns member Status. +func (c ChannelMember) Status() Status { + switch c.raw.(type) { + case *tg.ChannelParticipant: + return Plain + case *tg.ChannelParticipantSelf: + return Plain + case *tg.ChannelParticipantCreator: + return Creator + case *tg.ChannelParticipantAdmin: + return Admin + case *tg.ChannelParticipantBanned: + return Banned + case *tg.ChannelParticipantLeft: + return Left + default: + return -1 + } +} + +// Rank returns admin "rank". +func (c ChannelMember) Rank() (string, bool) { + switch p := c.raw.(type) { + case *tg.ChannelParticipant: + return "", false + case *tg.ChannelParticipantSelf: + return "", false + case *tg.ChannelParticipantCreator: + return p.GetRank() + case *tg.ChannelParticipantAdmin: + return p.GetRank() + case *tg.ChannelParticipantBanned: + return "", false + case *tg.ChannelParticipantLeft: + return "", false + default: + return "", false + } +} + +// JoinDate returns member join date, if it is available. +func (c ChannelMember) JoinDate() (time.Time, bool) { + switch p := c.raw.(type) { + case *tg.ChannelParticipant: + return time.Unix(int64(p.Date), 0), true + case *tg.ChannelParticipantSelf: + return time.Unix(int64(p.Date), 0), true + case *tg.ChannelParticipantCreator: + return c.creatorDate, true + case *tg.ChannelParticipantAdmin: + return time.Unix(int64(p.Date), 0), true + case *tg.ChannelParticipantBanned: + return time.Unix(int64(p.Date), 0), true + case *tg.ChannelParticipantLeft: + return time.Time{}, false + default: + return time.Time{}, false + } +} + +// InvitedBy returns user that invited this member. +func (c ChannelMember) InvitedBy() (peers.User, bool) { + switch p := c.raw.(type) { + case *tg.ChannelParticipant: + return peers.User{}, false + case *tg.ChannelParticipantSelf: + return c.inviter, true + case *tg.ChannelParticipantCreator: + return peers.User{}, false + case *tg.ChannelParticipantAdmin: + _, has := p.GetInviterID() + return c.inviter, has + case *tg.ChannelParticipantBanned: + return peers.User{}, false + case *tg.ChannelParticipantLeft: + return peers.User{}, false + default: + return peers.User{}, false + } +} + +// User returns member User object. +func (c ChannelMember) User() peers.User { + return c.user +} diff --git a/telegram/peers/members/channel_test.go b/telegram/peers/members/channel_test.go index 15ee8c5c5a..6d93bfd093 100644 --- a/telegram/peers/members/channel_test.go +++ b/telegram/peers/members/channel_test.go @@ -17,8 +17,7 @@ func TestChannelMembers_Count(t *testing.T) { mock, m := testManager(t) ch := m.Channel(getTestChannel()) - members, err := Channel(ctx, ch) - a.NoError(err) + members := Channel(ch) mock.ExpectCall(&tg.ChannelsGetParticipantsRequest{ Channel: ch.InputChannel(), @@ -26,7 +25,7 @@ func TestChannelMembers_Count(t *testing.T) { Offset: 0, Limit: 1, }).ThenErr(testutil.TestError()) - _, err = members.Count(ctx) + _, err := members.Count(ctx) a.Error(err) mock.ExpectCall(&tg.ChannelsGetParticipantsRequest{ @@ -62,8 +61,7 @@ func TestChannelMembers_ForEach(t *testing.T) { rawCh := getTestChannel() rawCh.Date = date ch := m.Channel(rawCh) - members, err := Channel(ctx, ch) - a.NoError(err) + members := Channel(ch) mock.ExpectCall(&tg.ChannelsGetFullChannelRequest{ Channel: ch.InputChannel(), @@ -172,3 +170,84 @@ func TestChannelMembers_ForEach(t *testing.T) { return nil })) } + +func TestChannelMembers_Kick(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + u := m.User(getTestUser()) + ch := m.Channel(getTestChannel()) + members := Channel(ch) + rights := tg.ChatBannedRights{ + ViewMessages: true, + } + rights.SetFlags() + + mock.ExpectCall(&tg.ChannelsEditBannedRequest{ + Channel: ch.InputChannel(), + Participant: u.InputPeer(), + BannedRights: rights, + }).ThenRPCErr(getTestError()) + a.Error(members.Kick(ctx, u.InputUser(), false)) + + mock.ExpectCall(&tg.ChannelsDeleteParticipantHistoryRequest{ + Channel: ch.InputChannel(), + Participant: u.InputPeer(), + }).ThenRPCErr(getTestError()) + a.Error(members.Kick(ctx, u.InputUser(), true)) + + mock.ExpectCall(&tg.ChannelsEditBannedRequest{ + Channel: ch.InputChannel(), + Participant: u.InputPeer(), + BannedRights: rights, + }).ThenResult(&tg.Updates{}) + a.NoError(members.Kick(ctx, u.InputUser(), false)) + + mock.ExpectCall(&tg.ChannelsDeleteParticipantHistoryRequest{ + Channel: ch.InputChannel(), + Participant: u.InputPeer(), + }).ThenResult(&tg.MessagesAffectedHistory{}) + mock.ExpectCall(&tg.ChannelsEditBannedRequest{ + Channel: ch.InputChannel(), + Participant: u.InputPeer(), + BannedRights: rights, + }).ThenResult(&tg.Updates{}) + a.NoError(members.Kick(ctx, u.InputUser(), true)) +} + +func TestChannelMembers_EditAdminRights(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + u := m.User(getTestUser()) + ch := m.Channel(getTestChannel()) + members := Channel(ch) + rights := tg.ChatAdminRights{ + AddAdmins: true, + } + rights.SetFlags() + + mock.ExpectCall(&tg.ChannelsEditAdminRequest{ + Channel: ch.InputChannel(), + UserID: u.InputUser(), + AdminRights: rights, + Rank: "rank", + }).ThenRPCErr(getTestError()) + a.Error(members.EditAdminRights(ctx, u.InputUser(), AdminRights{ + Rank: "rank", + AddAdmins: true, + })) + + mock.ExpectCall(&tg.ChannelsEditAdminRequest{ + Channel: ch.InputChannel(), + UserID: u.InputUser(), + AdminRights: rights, + Rank: "rank", + }).ThenResult(&tg.Updates{}) + a.NoError(members.EditAdminRights(ctx, u.InputUser(), AdminRights{ + Rank: "rank", + AddAdmins: true, + })) +} diff --git a/telegram/peers/members/chat.go b/telegram/peers/members/chat.go index d2f945f263..82b2b490b8 100644 --- a/telegram/peers/members/chat.go +++ b/telegram/peers/members/chat.go @@ -14,69 +14,58 @@ import ( type ChatMembers struct { m *peers.Manager chat peers.Chat - p []ChatMember } -// ChatMember is chat Member. -type ChatMember struct { - parent *ChatMembers - creatorDate time.Time - user peers.User - inviter peers.User - raw tg.ChatParticipantClass -} - -// Status returns member Status. -func (c ChatMember) Status() Status { - switch c.raw.(type) { - case *tg.ChatParticipant: - return Plain - case *tg.ChatParticipantCreator: - return Creator - case *tg.ChatParticipantAdmin: - return Admin - default: - return -1 +func (c *ChatMembers) queryParticipants(ctx context.Context) (*tg.ChatParticipants, error) { + full, err := c.chat.FullRaw(ctx) + if err != nil { + return nil, errors.Wrap(err, "get full") } -} - -// JoinDate returns member join date, if it is available. -func (c ChatMember) JoinDate() (time.Time, bool) { - switch p := c.raw.(type) { - case *tg.ChatParticipant: - return time.Unix(int64(p.Date), 0), true - case *tg.ChatParticipantCreator: - return c.creatorDate, true - case *tg.ChatParticipantAdmin: - return time.Unix(int64(p.Date), 0), true + switch p := full.Participants.(type) { + case *tg.ChatParticipantsForbidden: + return nil, &ChatInfoUnavailableError{Info: p} + case *tg.ChatParticipants: + return p, nil default: - return time.Time{}, false + return nil, errors.Errorf("unexpected type %T", p) } } -// InvitedBy returns user that invited this member. -func (c ChatMember) InvitedBy() (peers.User, bool) { - switch c.raw.(type) { - case *tg.ChatParticipant: - return c.inviter, true - case *tg.ChatParticipantCreator: - return peers.User{}, false - case *tg.ChatParticipantAdmin: - return c.inviter, true - default: - return peers.User{}, false +// ForEach calls cb for every member of chat. +// +// May return ChatInfoUnavailableError. +func (c *ChatMembers) ForEach(ctx context.Context, cb Callback) error { + chatDate := time.Unix(int64(c.chat.Raw().Date), 0) + p, err := c.queryParticipants(ctx) + if err != nil { + return errors.Wrap(err, "query") } -} -// User returns member User object. -func (c ChatMember) User() peers.User { - return c.user -} + for i, participant := range p.Participants { + userID := participant.GetUserID() + user, err := c.m.ResolveUserID(ctx, userID) + if err != nil { + return errors.Wrapf(err, "get member %d", userID) + } -// ForEach calls cb for every member of chat. -func (c *ChatMembers) ForEach(ctx context.Context, cb Callback) error { - for i, p := range c.p { - if err := cb(p); err != nil { + var inviter peers.User + switch p := participant.(type) { + case *tg.ChatParticipant: + inviter, err = c.m.ResolveUserID(ctx, p.InviterID) + case *tg.ChatParticipantAdmin: + inviter, err = c.m.ResolveUserID(ctx, p.InviterID) + } + if err != nil { + return errors.Wrap(err, "get inviter") + } + + if err := cb(ChatMember{ + parent: c, + creatorDate: chatDate, + user: user, + inviter: inviter, + raw: participant, + }); err != nil { return errors.Wrapf(err, "callback (index: %d)", i) } } @@ -85,60 +74,53 @@ func (c *ChatMembers) ForEach(ctx context.Context, cb Callback) error { // Count returns total count of members. func (c *ChatMembers) Count(ctx context.Context) (int, error) { - return len(c.p), nil -} - -// Chat returns recent chat members. -// -// May return ChatInfoUnavailableError. -func Chat(ctx context.Context, chat peers.Chat) (*ChatMembers, error) { - full, err := chat.FullRaw(ctx) + p, err := c.queryParticipants(ctx) if err != nil { - return nil, errors.Wrap(err, "get full") + return 0, errors.Wrap(err, "query") } - m := chat.Manager() - chatDate := time.Unix(int64(chat.Raw().Date), 0) + return len(p.Participants), nil +} - switch p := full.Participants.(type) { - case *tg.ChatParticipantsForbidden: - return nil, &ChatInfoUnavailableError{Info: p} - case *tg.ChatParticipants: - members := make([]ChatMember, len(p.Participants)) - result := &ChatMembers{ - m: m, - chat: chat, - p: members, - } +// Peer returns chat object. +func (c *ChatMembers) Peer() peers.Peer { + return c.chat +} - for i, participant := range p.Participants { - userID := participant.GetUserID() - user, err := m.ResolveUserID(ctx, userID) - if err != nil { - return nil, errors.Wrapf(err, "get member %d", userID) - } +// Kick kicks user member. +// +// If revokeHistory is set, will delete all messages from this member. +func (c *ChatMembers) Kick(ctx context.Context, member tg.InputUserClass, revokeHistory bool) error { + if _, err := c.m.API().MessagesDeleteChatUser(ctx, &tg.MessagesDeleteChatUserRequest{ + RevokeHistory: revokeHistory, + ChatID: c.chat.ID(), + UserID: member, + }); err != nil { + return errors.Wrapf(err, "delete user (revoke: %v)", revokeHistory) + } + return nil +} - var inviter peers.User - switch p := participant.(type) { - case *tg.ChatParticipant: - inviter, err = m.ResolveUserID(ctx, p.InviterID) - case *tg.ChatParticipantAdmin: - inviter, err = m.ResolveUserID(ctx, p.InviterID) - } - if err != nil { - return nil, errors.Wrap(err, "get inviter") - } +// EditRights edits rights of all members in this chat. +func (c *ChatMembers) EditRights(ctx context.Context, options MemberRights) error { + return editDefaultRights(ctx, c.m.API(), c.chat.InputPeer(), options) +} - members[i] = ChatMember{ - parent: result, - creatorDate: chatDate, - user: user, - inviter: inviter, - raw: participant, - } - } +// EditAdmin edits admin rights for given user. +func (c *ChatMembers) EditAdmin(ctx context.Context, user tg.InputUserClass, isAdmin bool) error { + if _, err := c.m.API().MessagesEditChatAdmin(ctx, &tg.MessagesEditChatAdminRequest{ + ChatID: c.chat.ID(), + UserID: user, + IsAdmin: isAdmin, + }); err != nil { + return errors.Wrap(err, "edit admin") + } + return nil +} - return result, nil - default: - return nil, errors.Errorf("unexpected type %T", p) +// Chat returns recent chat members. +func Chat(chat peers.Chat) *ChatMembers { + return &ChatMembers{ + m: chat.Manager(), + chat: chat, } } diff --git a/telegram/peers/members/chat_member.go b/telegram/peers/members/chat_member.go new file mode 100644 index 0000000000..e818a3cc56 --- /dev/null +++ b/telegram/peers/members/chat_member.go @@ -0,0 +1,69 @@ +package members + +import ( + "time" + + "github.com/gotd/td/telegram/peers" + "github.com/gotd/td/tg" +) + +// ChatMember is chat Member. +type ChatMember struct { + parent *ChatMembers + creatorDate time.Time + user peers.User + inviter peers.User + raw tg.ChatParticipantClass +} + +// Raw returns raw member object. +func (c ChatMember) Raw() tg.ChatParticipantClass { + return c.raw +} + +// Status returns member Status. +func (c ChatMember) Status() Status { + switch c.raw.(type) { + case *tg.ChatParticipant: + return Plain + case *tg.ChatParticipantCreator: + return Creator + case *tg.ChatParticipantAdmin: + return Admin + default: + return -1 + } +} + +// JoinDate returns member join date, if it is available. +func (c ChatMember) JoinDate() (time.Time, bool) { + switch p := c.raw.(type) { + case *tg.ChatParticipant: + return time.Unix(int64(p.Date), 0), true + case *tg.ChatParticipantCreator: + return c.creatorDate, true + case *tg.ChatParticipantAdmin: + return time.Unix(int64(p.Date), 0), true + default: + return time.Time{}, false + } +} + +// InvitedBy returns user that invited this member. +func (c ChatMember) InvitedBy() (peers.User, bool) { + switch c.raw.(type) { + case *tg.ChatParticipant: + return c.inviter, true + case *tg.ChatParticipantCreator: + return peers.User{}, false + case *tg.ChatParticipantAdmin: + return c.inviter, true + default: + return peers.User{}, false + } +} + +// User returns member User object. +func (c ChatMember) User() peers.User { + return c.user +} diff --git a/telegram/peers/members/chat_test.go b/telegram/peers/members/chat_test.go index 619034e51e..b1f22c2ec7 100644 --- a/telegram/peers/members/chat_test.go +++ b/telegram/peers/members/chat_test.go @@ -10,7 +10,54 @@ import ( "github.com/gotd/td/tg" ) -func TestChat(t *testing.T) { +func TestChatMembers_Count(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + now := time.Now() + date := int(now.Unix()) + + rawCh := getTestChat() + rawCh.Date = date + ch := m.Chat(rawCh) + members := Chat(ch) + + mock.ExpectCall(&tg.MessagesGetFullChatRequest{ + ChatID: ch.ID(), + }).ThenRPCErr(getTestError()) + _, err := members.Count(ctx) + a.Error(err) + + mock.ExpectCall(&tg.MessagesGetFullChatRequest{ + ChatID: ch.ID(), + }).ThenResult(&tg.MessagesChatFull{ + FullChat: getTestChatFull(&tg.ChatParticipants{ + ChatID: 10, + Participants: []tg.ChatParticipantClass{ + &tg.ChatParticipant{ + UserID: 10, + InviterID: 11, + Date: date, + }, + &tg.ChatParticipantCreator{ + UserID: 10, + }, + &tg.ChatParticipantAdmin{ + UserID: 10, + InviterID: 11, + Date: date, + }, + }, + Version: 1, + }), + }) + count, err := members.Count(ctx) + a.Equal(3, count) + a.NoError(err) +} + +func TestChatMembers_ForEach(t *testing.T) { a := require.New(t) ctx := context.Background() mock, m := testManager(t) @@ -55,8 +102,7 @@ func TestChat(t *testing.T) { }, }, }) - members, err := Chat(ctx, ch) - a.NoError(err) + members := Chat(ch) count, err := members.Count(ctx) a.Equal(3, count) @@ -98,3 +144,58 @@ func TestChat(t *testing.T) { return nil })) } + +func TestChatMembers_Kick(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + u := m.User(getTestUser()) + ch := m.Chat(getTestChat()) + members := Chat(ch) + + mock.ExpectCall(&tg.MessagesDeleteChatUserRequest{ + RevokeHistory: true, + ChatID: ch.ID(), + UserID: u.InputUser(), + }).ThenRPCErr(getTestError()) + a.Error(members.Kick(ctx, u.InputUser(), true)) + + mock.ExpectCall(&tg.MessagesDeleteChatUserRequest{ + RevokeHistory: true, + ChatID: ch.ID(), + UserID: u.InputUser(), + }).ThenResult(&tg.Updates{}) + a.NoError(members.Kick(ctx, u.InputUser(), true)) +} + +func TestChatMembers_EditAdmin(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + u := m.User(getTestUser()) + ch := m.Chat(getTestChat()) + members := Chat(ch) + + mock.ExpectCall(&tg.MessagesEditChatAdminRequest{ + IsAdmin: true, + ChatID: ch.ID(), + UserID: u.InputUser(), + }).ThenRPCErr(getTestError()) + a.Error(members.EditAdmin(ctx, u.InputUser(), true)) + + mock.ExpectCall(&tg.MessagesEditChatAdminRequest{ + IsAdmin: true, + ChatID: ch.ID(), + UserID: u.InputUser(), + }).ThenTrue() + a.NoError(members.EditAdmin(ctx, u.InputUser(), true)) + + mock.ExpectCall(&tg.MessagesEditChatAdminRequest{ + IsAdmin: false, + ChatID: ch.ID(), + UserID: u.InputUser(), + }).ThenTrue() + a.NoError(members.EditAdmin(ctx, u.InputUser(), false)) +} diff --git a/telegram/peers/members/common.go b/telegram/peers/members/common.go new file mode 100644 index 0000000000..fb199db4b0 --- /dev/null +++ b/telegram/peers/members/common.go @@ -0,0 +1,39 @@ +package members + +import ( + "context" + + "github.com/go-faster/errors" + + "github.com/gotd/td/tg" +) + +func convertInputUserToInputPeer(p tg.InputUserClass) tg.InputPeerClass { + switch p := p.(type) { + case *tg.InputUserSelf: + return &tg.InputPeerSelf{} + case *tg.InputUser: + return &tg.InputPeerUser{ + UserID: p.UserID, + AccessHash: p.AccessHash, + } + case *tg.InputUserFromMessage: + return &tg.InputPeerUserFromMessage{ + Peer: p.Peer, + MsgID: p.MsgID, + UserID: p.UserID, + } + default: + return nil + } +} + +func editDefaultRights(ctx context.Context, api *tg.Client, p tg.InputPeerClass, rights MemberRights) error { + if _, err := api.MessagesEditChatDefaultBannedRights(ctx, &tg.MessagesEditChatDefaultBannedRightsRequest{ + Peer: p, + BannedRights: rights.IntoChatBannedRights(), + }); err != nil { + return errors.Wrap(err, "edit default rights") + } + return nil +} diff --git a/telegram/peers/members/members.go b/telegram/peers/members/members.go index 1f08bed85a..39fde5b4ff 100644 --- a/telegram/peers/members/members.go +++ b/telegram/peers/members/members.go @@ -6,6 +6,7 @@ import ( "time" "github.com/gotd/td/telegram/peers" + "github.com/gotd/td/tg" ) var _ = []Member{ @@ -39,4 +40,12 @@ type Members interface { ForEach(ctx context.Context, cb Callback) error // Count returns total count of members. Count(ctx context.Context) (int, error) + // Peer returns chat object. + Peer() peers.Peer + // Kick kicks user member. + // + // If revokeHistory is set, will delete all messages from this member. + Kick(ctx context.Context, member tg.InputUserClass, revokeHistory bool) error + // EditRights edits rights of all members in this chat/channel. + EditRights(ctx context.Context, options MemberRights) error } diff --git a/telegram/peers/members/members_test.go b/telegram/peers/members/members_test.go index 9274a5ab28..b88f05301c 100644 --- a/telegram/peers/members/members_test.go +++ b/telegram/peers/members/members_test.go @@ -1,13 +1,16 @@ package members import ( + "context" "testing" "time" + "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "github.com/gotd/td/telegram/peers" "github.com/gotd/td/tg" + "github.com/gotd/td/tgerr" "github.com/gotd/td/tgmock" ) @@ -75,3 +78,51 @@ func getTestChatFull(participants tg.ChatParticipantsClass) *tg.ChatFull { u.SetFlags() return u } + +func getTestUser() *tg.User { + u := &tg.User{ + Self: false, + Bot: false, + ID: 11, + AccessHash: 10, + FirstName: "Julia", + LastName: "Ann", + Username: "aboba", + } + u.SetFlags() + return u +} + +func getTestError() *tgerr.Error { + return &tgerr.Error{ + Code: 1337, + Message: "TEST_ERROR", + Type: "TEST_ERROR", + } +} + +func TestEditRights(t *testing.T) { + a := require.New(t) + ctx := context.Background() + mock, m := testManager(t) + + rights := tg.ChatBannedRights{ + SendInline: true, + } + rights.SetFlags() + req := func(p Members) *tgmock.RequestBuilder { + return mock.ExpectCall(&tg.MessagesEditChatDefaultBannedRightsRequest{ + Peer: p.Peer().InputPeer(), + BannedRights: rights, + }) + } + for _, p := range []Members{ + Chat(m.Chat(getTestChat())), + Channel(m.Channel(getTestChannel())), + } { + req(p).ThenRPCErr(getTestError()) + a.Error(p.EditRights(ctx, MemberRights{DenySendInline: true})) + req(p).ThenResult(&tg.Updates{}) + a.NoError(p.EditRights(ctx, MemberRights{DenySendInline: true})) + } +} diff --git a/telegram/peers/rights.go b/telegram/peers/members/rights.go similarity index 92% rename from telegram/peers/rights.go rename to telegram/peers/members/rights.go index d45f010997..4f3aead998 100644 --- a/telegram/peers/rights.go +++ b/telegram/peers/members/rights.go @@ -1,4 +1,4 @@ -package peers +package members import ( "time" @@ -55,8 +55,8 @@ func (b AdminRights) IntoChatAdminRights() (r tg.ChatAdminRights) { return r } -// ParticipantRights represents participant right settings. -type ParticipantRights struct { +// MemberRights represents member right settings. +type MemberRights struct { // If set, does not allow a user to view messages in a supergroup/channel/chat. // // In fact, user will be kicked. @@ -90,12 +90,12 @@ type ParticipantRights struct { } // ApplyFor sets duration of validity of set rights. -func (b *ParticipantRights) ApplyFor(d time.Duration) { +func (b *MemberRights) ApplyFor(d time.Duration) { b.UntilDate = time.Now().Add(d) } -// IntoChatBannedRights converts ParticipantRights into tg.ChatBannedRights. -func (b ParticipantRights) IntoChatBannedRights() (r tg.ChatBannedRights) { +// IntoChatBannedRights converts MemberRights into tg.ChatBannedRights. +func (b MemberRights) IntoChatBannedRights() (r tg.ChatBannedRights) { r = tg.ChatBannedRights{ ViewMessages: b.DenyViewMessages, SendMessages: b.DenySendMessages, diff --git a/telegram/peers/rights_test.go b/telegram/peers/members/rights_test.go similarity index 85% rename from telegram/peers/rights_test.go rename to telegram/peers/members/rights_test.go index 38fa007db3..b738bf139b 100644 --- a/telegram/peers/rights_test.go +++ b/telegram/peers/members/rights_test.go @@ -1,4 +1,4 @@ -package peers +package members import ( "testing" @@ -9,14 +9,14 @@ import ( "github.com/gotd/td/tg" ) -func TestParticipantRights_ApplyFor(t *testing.T) { - var r ParticipantRights +func TestMemberRights_ApplyFor(t *testing.T) { + var r MemberRights r.ApplyFor(time.Second) require.False(t, r.UntilDate.IsZero()) } -func TestParticipantRights_IntoChatBannedRights(t *testing.T) { - r := ParticipantRights{ +func TestMemberRights_IntoChatBannedRights(t *testing.T) { + r := MemberRights{ DenyViewMessages: true, DenySendMessages: true, DenySendMedia: true, diff --git a/telegram/peers/multichat.go b/telegram/peers/multichat.go index f376d32e7b..30ac8521ef 100644 --- a/telegram/peers/multichat.go +++ b/telegram/peers/multichat.go @@ -30,13 +30,3 @@ func (m *Manager) editReactions(ctx context.Context, p tg.InputPeerClass, reacti } return nil } - -func (m *Manager) editDefaultRights(ctx context.Context, p tg.InputPeerClass, rights ParticipantRights) error { - if _, err := m.api.MessagesEditChatDefaultBannedRights(ctx, &tg.MessagesEditChatDefaultBannedRightsRequest{ - Peer: p, - BannedRights: rights.IntoChatBannedRights(), - }); err != nil { - return errors.Wrap(err, "edit default rights") - } - return nil -} diff --git a/telegram/peers/multichat_test.go b/telegram/peers/multichat_test.go index ad10b67d07..789914ea04 100644 --- a/telegram/peers/multichat_test.go +++ b/telegram/peers/multichat_test.go @@ -33,9 +33,6 @@ type multiChat interface { SetReactions(ctx context.Context, r ...string) error DisableReactions(ctx context.Context) error - - KickUser(ctx context.Context, member tg.InputUserClass, revokeHistory bool) error - EditRights(ctx context.Context, options ParticipantRights) error } var _ = []multiChat{ @@ -70,29 +67,3 @@ func TestReactions(t *testing.T) { a.NoError(p.DisableReactions(ctx)) } } - -func TestEditRights(t *testing.T) { - a := require.New(t) - ctx := context.Background() - mock, m := testManager(t) - - rights := tg.ChatBannedRights{ - SendInline: true, - } - rights.SetFlags() - req := func(p Peer) *tgmock.RequestBuilder { - return mock.ExpectCall(&tg.MessagesEditChatDefaultBannedRightsRequest{ - Peer: p.InputPeer(), - BannedRights: rights, - }) - } - for _, p := range []multiChat{ - m.Chat(getTestChat()), - m.Channel(getTestChannel()), - } { - req(p).ThenRPCErr(getTestError()) - a.Error(p.EditRights(ctx, ParticipantRights{DenySendInline: true})) - req(p).ThenResult(&tg.Updates{}) - a.NoError(p.EditRights(ctx, ParticipantRights{DenySendInline: true})) - } -} diff --git a/telegram/peers/query.go b/telegram/peers/query.go index e1eaafe130..4fd8b91786 100644 --- a/telegram/peers/query.go +++ b/telegram/peers/query.go @@ -9,26 +9,6 @@ import ( "github.com/gotd/td/tg" ) -func convertInputUserToInputPeer(p tg.InputUserClass) tg.InputPeerClass { - switch p := p.(type) { - case *tg.InputUserSelf: - return &tg.InputPeerSelf{} - case *tg.InputUser: - return &tg.InputPeerUser{ - UserID: p.UserID, - AccessHash: p.AccessHash, - } - case *tg.InputUserFromMessage: - return &tg.InputPeerUserFromMessage{ - Peer: p.Peer, - MsgID: p.MsgID, - UserID: p.UserID, - } - default: - return nil - } -} - func (m *Manager) getIDFromInputUser(p tg.InputUserClass) (int64, bool) { switch p := p.(type) { case *tg.InputUserSelf: From d171365ebc4bb44a912967f52ea02cb166f6398d Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 10 Mar 2022 07:07:05 +0300 Subject: [PATCH 10/10] feat(members): add Kick method for member types --- telegram/peers/members/channel_member.go | 8 ++++++++ telegram/peers/members/chat_member.go | 8 ++++++++ telegram/peers/members/members.go | 4 ++++ 3 files changed, 20 insertions(+) diff --git a/telegram/peers/members/channel_member.go b/telegram/peers/members/channel_member.go index 4ea6c48d92..0e40098fdf 100644 --- a/telegram/peers/members/channel_member.go +++ b/telegram/peers/members/channel_member.go @@ -1,6 +1,7 @@ package members import ( + "context" "time" "github.com/gotd/td/telegram/peers" @@ -106,3 +107,10 @@ func (c ChannelMember) InvitedBy() (peers.User, bool) { func (c ChannelMember) User() peers.User { return c.user } + +// Kick kicks this member. +// +// If revokeHistory is set, will delete all messages from this member. +func (c ChannelMember) Kick(ctx context.Context, revokeHistory bool) error { + return c.parent.Kick(ctx, c.user.InputUser(), revokeHistory) +} diff --git a/telegram/peers/members/chat_member.go b/telegram/peers/members/chat_member.go index e818a3cc56..e0639b45c6 100644 --- a/telegram/peers/members/chat_member.go +++ b/telegram/peers/members/chat_member.go @@ -1,6 +1,7 @@ package members import ( + "context" "time" "github.com/gotd/td/telegram/peers" @@ -67,3 +68,10 @@ func (c ChatMember) InvitedBy() (peers.User, bool) { func (c ChatMember) User() peers.User { return c.user } + +// Kick kicks this member. +// +// If revokeHistory is set, will delete all messages from this member. +func (c ChatMember) Kick(ctx context.Context, revokeHistory bool) error { + return c.parent.Kick(ctx, c.user.InputUser(), revokeHistory) +} diff --git a/telegram/peers/members/members.go b/telegram/peers/members/members.go index 39fde5b4ff..dfa4caac14 100644 --- a/telegram/peers/members/members.go +++ b/telegram/peers/members/members.go @@ -24,6 +24,10 @@ type Member interface { InvitedBy() (peers.User, bool) // User returns member User object. User() peers.User + // Kick kicks this member. + // + // If revokeHistory is set, will delete all messages from this member. + Kick(ctx context.Context, revokeHistory bool) error } // Callback is type for member iterator callback.