move gotd fork into repo. (#111)

- update to latest telegram layer
- remove some references to fields in tg.Entities that don't exist in
the schema
- originally added here:
https://github.com/beeper/td/commit/820929062a2ba0104397bc01235ab58a9cff780e
  - referenced here
-
https://github.com/mautrix/telegramgo/commit/124f0967ed195b5a380c9bd02e170ada9710dde3
-
https://github.com/mautrix/telegramgo/commit/4205047aab2e0639217148b5d125bfaab668bd8e
This commit is contained in:
Adam Van Ymeren
2025-06-27 20:03:37 -07:00
committed by GitHub
parent 0952df0244
commit 7a04f298d2
19264 changed files with 1539697 additions and 84 deletions
+214
View File
@@ -0,0 +1,214 @@
package members
import (
"context"
"time"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/peers"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// ChannelMembers is channel Members.
type ChannelMembers struct {
m *peers.Manager
filter tg.ChannelParticipantsFilterClass
channel peers.Channel
}
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: c.filter,
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, member := range m.Participants {
var (
userID int64
inviterID int64
err error
)
switch p := member.(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)
}
chm := ChannelMember{
parent: c,
creatorDate: channelDate,
user: user,
inviter: peers.User{},
raw: member,
}
if inviterID != 0 {
inviter, err := c.m.ResolveUserID(ctx, inviterID)
if err != nil {
return errors.Wrapf(err, "get inviter %d", inviterID)
}
chm.inviter = inviter
}
if err := cb(chm); 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
}
// 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(channel peers.Channel) *ChannelMembers {
return ChannelQuery{Channel: channel}.Recent()
}
@@ -0,0 +1,116 @@
package members
import (
"context"
"time"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/peers"
"go.mau.fi/mautrix-telegram/pkg/gotd/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
}
// 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)
}
@@ -0,0 +1,59 @@
package members
import (
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/peers"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// ChannelQuery is builder for channel members querying.
type ChannelQuery struct {
Channel peers.Channel
}
func (q ChannelQuery) query(filter tg.ChannelParticipantsFilterClass) *ChannelMembers {
return &ChannelMembers{
m: q.Channel.Manager(),
filter: filter,
channel: q.Channel,
}
}
// Recent queries recent members.
func (q ChannelQuery) Recent() *ChannelMembers {
return q.query(&tg.ChannelParticipantsRecent{})
}
// Admins queries admins members.
func (q ChannelQuery) Admins() *ChannelMembers {
return q.query(&tg.ChannelParticipantsAdmins{})
}
// Kicked queries kicked members.
func (q ChannelQuery) Kicked(query string) *ChannelMembers {
return q.query(&tg.ChannelParticipantsKicked{Q: query})
}
// Bots queries bots members.
func (q ChannelQuery) Bots() *ChannelMembers {
return q.query(&tg.ChannelParticipantsBots{})
}
// Banned queries banned members.
func (q ChannelQuery) Banned(query string) *ChannelMembers {
return q.query(&tg.ChannelParticipantsBanned{Q: query})
}
// Search queries members by given name.
func (q ChannelQuery) Search(query string) *ChannelMembers {
return q.query(&tg.ChannelParticipantsSearch{Q: query})
}
// Contacts queries members that are also contacts.
func (q ChannelQuery) Contacts(query string) *ChannelMembers {
return q.query(&tg.ChannelParticipantsContacts{Q: query})
}
// Custom creates query with custom filter.
func (q ChannelQuery) Custom(filter tg.ChannelParticipantsFilterClass) *ChannelMembers {
return q.query(filter)
}
@@ -0,0 +1,289 @@
package members
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestChannelMembers_Count(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
ch := m.Channel(getTestChannel())
members := Channel(ch)
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)
}
func TestChannelMembers_ForEach(t *testing.T) {
ctx := context.Background()
now := time.Now()
date := int(now.Unix())
t.Run("Good", func(t *testing.T) {
a := require.New(t)
mock, m := testManager(t)
rawCh := getTestChannel()
rawCh.Date = date
ch := m.Channel(rawCh)
members := Channel(ch)
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
}))
})
t.Run("ChannelInfoUnavailableError", func(t *testing.T) {
a := require.New(t)
mock, m := testManager(t)
rawCh := getTestChannel()
rawCh.Date = date
ch := m.Channel(rawCh)
rawFull := &tg.ChannelFull{
HasScheduled: true,
ID: 11,
About: "garfield blog",
ParticipantsCount: 1,
ChatPhoto: &tg.PhotoEmpty{},
}
rawFull.SetFlags()
mock.ExpectCall(&tg.ChannelsGetFullChannelRequest{
Channel: ch.InputChannel(),
}).ThenResult(&tg.MessagesChatFull{
FullChat: rawFull,
})
members := Channel(ch)
var targetErr *ChannelInfoUnavailableError
a.ErrorAs(members.ForEach(ctx, func(p Member) error {
return nil
}), &targetErr)
})
}
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()
member := ChannelMember{
parent: members,
user: u,
raw: &tg.ChannelParticipant{},
}
mock.ExpectCall(&tg.ChannelsEditBannedRequest{
Channel: ch.InputChannel(),
Participant: u.InputPeer(),
BannedRights: rights,
}).ThenRPCErr(getTestError())
a.Error(member.Kick(ctx, false))
mock.ExpectCall(&tg.ChannelsDeleteParticipantHistoryRequest{
Channel: ch.InputChannel(),
Participant: u.InputPeer(),
}).ThenRPCErr(getTestError())
a.Error(member.Kick(ctx, true))
mock.ExpectCall(&tg.ChannelsEditBannedRequest{
Channel: ch.InputChannel(),
Participant: u.InputPeer(),
BannedRights: rights,
}).ThenResult(&tg.Updates{})
a.NoError(member.Kick(ctx, 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(member.Kick(ctx, 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,
}))
}
+126
View File
@@ -0,0 +1,126 @@
package members
import (
"context"
"time"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/peers"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// ChatMembers is chat Members.
type ChatMembers struct {
m *peers.Manager
chat peers.Chat
}
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")
}
switch p := full.Participants.(type) {
case *tg.ChatParticipantsForbidden:
return nil, &ChatInfoUnavailableError{Info: p}
case *tg.ChatParticipants:
return p, nil
default:
return nil, errors.Errorf("unexpected type %T", p)
}
}
// 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")
}
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)
}
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)
}
}
return nil
}
// Count returns total count of members.
func (c *ChatMembers) Count(ctx context.Context) (int, error) {
p, err := c.queryParticipants(ctx)
if err != nil {
return 0, errors.Wrap(err, "query")
}
return len(p.Participants), nil
}
// Peer returns chat object.
func (c *ChatMembers) Peer() peers.Peer {
return c.chat
}
// 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
}
// 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)
}
// 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
}
// Chat returns recent chat members.
func Chat(chat peers.Chat) *ChatMembers {
return &ChatMembers{
m: chat.Manager(),
chat: chat,
}
}
@@ -0,0 +1,77 @@
package members
import (
"context"
"time"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/peers"
"go.mau.fi/mautrix-telegram/pkg/gotd/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
}
// 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)
}
@@ -0,0 +1,228 @@
package members
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
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) {
ctx := context.Background()
now := time.Now()
date := int(now.Unix())
t.Run("Good", func(t *testing.T) {
a := require.New(t)
mock, m := testManager(t)
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 := Chat(ch)
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
}))
})
t.Run("ChatInfoUnavailableError", func(t *testing.T) {
a := require.New(t)
mock, m := testManager(t)
rawCh := getTestChat()
rawCh.Date = date
ch := m.Chat(rawCh)
mock.ExpectCall(&tg.MessagesGetFullChatRequest{
ChatID: ch.ID(),
}).ThenResult(&tg.MessagesChatFull{
FullChat: getTestChatFull(&tg.ChatParticipantsForbidden{}),
})
members := Chat(ch)
var targetErr *ChatInfoUnavailableError
a.ErrorAs(members.ForEach(ctx, func(p Member) error {
return nil
}), &targetErr)
})
}
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)
member := ChatMember{
parent: members,
user: u,
raw: &tg.ChatParticipant{},
}
mock.ExpectCall(&tg.MessagesDeleteChatUserRequest{
RevokeHistory: true,
ChatID: ch.ID(),
UserID: u.InputUser(),
}).ThenRPCErr(getTestError())
a.Error(member.Kick(ctx, true))
mock.ExpectCall(&tg.MessagesDeleteChatUserRequest{
RevokeHistory: true,
ChatID: ch.ID(),
UserID: u.InputUser(),
}).ThenResult(&tg.Updates{})
a.NoError(member.Kick(ctx, 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))
}
+39
View File
@@ -0,0 +1,39 @@
package members
import (
"context"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/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
}
+24
View File
@@ -0,0 +1,24 @@
package members
import (
"go.mau.fi/mautrix-telegram/pkg/gotd/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"
}
@@ -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")
}
@@ -0,0 +1,55 @@
// Package members defines interfaces for working with chat/channel members.
package members
import (
"context"
"time"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/peers"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
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
// 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.
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)
// 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
}
@@ -0,0 +1,128 @@
package members
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/peers"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
"go.mau.fi/mautrix-telegram/pkg/gotd/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,
}
}
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
}
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}))
}
}
+118
View File
@@ -0,0 +1,118 @@
package members
import (
"time"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// 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
}
// 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.
DenyViewMessages bool
// If set, does not allow a user to send messages in a supergroup/chat.
DenySendMessages bool
// If set, does not allow a user to send any media in a supergroup/chat.
DenySendMedia bool
// If set, does not allow a user to send stickers in a supergroup/chat.
DenySendStickers bool
// If set, does not allow a user to send gifs in a supergroup/chat.
DenySendGifs bool
// If set, does not allow a user to send games in a supergroup/chat.
DenySendGames bool
// If set, does not allow a user to use inline bots in a supergroup/chat.
DenySendInline bool
// If set, does not allow a user to embed links in the messages of a supergroup/chat.
DenyEmbedLinks bool
// If set, does not allow a user to send polls in a supergroup/chat.
DenySendPolls bool
// If set, does not allow any user to change the description of a supergroup/chat.
DenyChangeInfo bool
// If set, does not allow any user to invite users in a supergroup/chat.
DenyInviteUsers bool
// If set, does not allow any user to pin messages in a supergroup/chat.
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
}
// ApplyFor sets duration of validity of set rights.
func (b *MemberRights) ApplyFor(d time.Duration) {
b.UntilDate = time.Now().Add(d)
}
// IntoChatBannedRights converts MemberRights into tg.ChatBannedRights.
func (b MemberRights) IntoChatBannedRights() (r tg.ChatBannedRights) {
r = tg.ChatBannedRights{
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())
}
r.SetFlags()
return r
}
@@ -0,0 +1,57 @@
package members
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestMemberRights_ApplyFor(t *testing.T) {
var r MemberRights
r.ApplyFor(time.Second)
require.False(t, r.UntilDate.IsZero())
}
func TestMemberRights_IntoChatBannedRights(t *testing.T) {
r := MemberRights{
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)
}
+19
View File
@@ -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
)
@@ -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]]
}