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
+235
View File
@@ -0,0 +1,235 @@
package peers
import (
"context"
"github.com/go-faster/errors"
"go.uber.org/multierr"
"go.mau.fi/mautrix-telegram/pkg/gotd/constant"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func (m *Manager) applyUsers(ctx context.Context, input ...tg.UserClass) error {
var (
users []*tg.User
ids = make([]constant.TDLibPeerID, 0, 16)
)
if len(ids) < len(input) {
ids = make([]constant.TDLibPeerID, 0, len(input))
}
for _, user := range input {
user, ok := user.(*tg.User)
if !ok {
// Got nil or Empty.
continue
}
if user.Min {
// TODO(tdakkota): call some hook to get actual user if got min (e.g. force gaps to getDifference)
continue
}
users = append(users, user)
id := user.GetID()
k := Key{
Prefix: usersPrefix,
ID: id,
}
v := Value{
AccessHash: user.AccessHash,
}
if err := m.storage.Save(ctx, k, v); err != nil {
// FIXME(tdakkota): just log errors?
return errors.Wrapf(err, "save user %d", user.ID)
}
if user.Phone != "" {
if err := m.storage.SavePhone(ctx, user.Phone, k); err != nil {
return errors.Wrapf(err, "save user %d", user.ID)
}
}
ids = append(ids, userPeerID(id))
}
if err := m.cache.SaveUsers(ctx, users...); err != nil {
return errors.Wrap(err, "cache users")
}
m.updated(ids...)
return nil
}
func (m *Manager) applyChats(ctx context.Context, input ...tg.ChatClass) error {
var (
chats []*tg.Chat
channels []*tg.Channel
ids = make([]constant.TDLibPeerID, 0, 16)
)
if len(ids) < len(input) {
ids = make([]constant.TDLibPeerID, 0, len(input))
}
for _, ch := range input {
var (
k Key
v Value
)
// FIXME(tdakkota): check min constructors
switch ch := ch.(type) {
case *tg.Chat:
k.ID = ch.ID
k.Prefix = chatsPrefix
chats = append(chats, ch)
ids = append(ids, chatPeerID(ch.ID))
case *tg.ChatForbidden:
k.ID = ch.ID
k.Prefix = chatsPrefix
case *tg.Channel:
k.ID = ch.ID
v.AccessHash = ch.AccessHash
k.Prefix = channelPrefix
channels = append(channels, ch)
ids = append(ids, channelPeerID(ch.ID))
case *tg.ChannelForbidden:
k.ID = ch.ID
v.AccessHash = ch.AccessHash
k.Prefix = channelPrefix
default:
// Got nil or Empty
continue
}
if err := m.storage.Save(ctx, k, v); err != nil {
// FIXME(tdakkota): just log errors?
return errors.Wrapf(err, "save chat %d", k.ID)
}
}
if err := m.cache.SaveChats(ctx, chats...); err != nil {
return errors.Wrap(err, "cache chats")
}
if err := m.cache.SaveChannels(ctx, channels...); err != nil {
return errors.Wrap(err, "cache channels")
}
m.updated(ids...)
return nil
}
// Apply adds given entities to manager state.
func (m *Manager) Apply(ctx context.Context, users []tg.UserClass, chats []tg.ChatClass) error {
return m.applyEntities(ctx, users, chats)
}
func (m *Manager) applyEntities(ctx context.Context, users []tg.UserClass, chats []tg.ChatClass) error {
return multierr.Append(m.applyUsers(ctx, users...), m.applyChats(ctx, chats...))
}
func (m *Manager) applyFullUser(ctx context.Context, user *tg.UserFull) error {
if user == nil {
return nil
}
m.updatedFull(userPeerID(user.ID))
return m.cache.SaveUserFulls(ctx, user)
}
func (m *Manager) applyFullChat(ctx context.Context, chat *tg.ChatFull) error {
if chat == nil {
return nil
}
m.updatedFull(chatPeerID(chat.ID))
return m.cache.SaveChatFulls(ctx, chat)
}
func (m *Manager) applyFullChannel(ctx context.Context, ch *tg.ChannelFull) error {
if ch == nil {
return nil
}
m.updatedFull(channelPeerID(ch.ID))
return m.cache.SaveChannelFulls(ctx, ch)
}
func (m *Manager) updateContacts(ctx context.Context) ([]tg.UserClass, error) {
ch := m.sg.DoChan("_contacts", func() (interface{}, error) {
hash, err := m.storage.GetContactsHash(ctx)
if err != nil {
return nil, errors.Wrap(err, "get contacts hash")
}
r, err := m.api.ContactsGetContacts(ctx, hash)
if err != nil {
return nil, errors.Wrap(err, "get contacts")
}
switch c := r.(type) {
case *tg.ContactsContacts:
if err := m.applyUsers(ctx, c.Users...); err != nil {
return nil, errors.Wrap(err, "update users")
}
myID, ok := m.myID()
if !ok {
return c.Users, nil
}
if err := m.storage.SaveContactsHash(ctx, contactsHash(myID, c)); err != nil {
return nil, errors.Wrap(err, "update contacts hash")
}
return c.Users, nil
case *tg.ContactsContactsNotModified:
return nil, nil
default:
return nil, errors.Errorf("unexpected type %T", r)
}
})
select {
case r := <-ch:
if err := r.Err; err != nil {
return nil, err
}
users, ok := r.Val.([]tg.UserClass)
if !ok {
return nil, nil
}
return users, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
type vectorHash struct {
state uint64
}
// See https://github.com/tdlib/td/blob/aa8a4979df8fc56032f134471a2cb939a7b0839f/td/telegram/misc.cpp#L242.
func (h *vectorHash) apply(n uint64) {
h.state ^= h.state >> 21
h.state ^= h.state << 35
h.state ^= h.state >> 4
h.state += n
}
// See https://github.com/tdlib/td/blob/aa8a4979df8fc56032f134471a2cb939a7b0839f/td/telegram/ContactsManager.cpp#L5125.
func contactsHash(myID int64, contacts *tg.ContactsContacts) int64 {
contacts.MapUsers().SortStableByID()
var lesserIDx = len(contacts.Users) - 1
for i, user := range contacts.Users {
if user.GetID() < myID {
lesserIDx = i
break
}
}
var h vectorHash
h.apply(uint64(len(contacts.Users)))
for i, contact := range contacts.Users {
h.apply(uint64(contact.GetID()))
if i == lesserIDx {
h.apply(uint64(myID))
}
}
return int64(h.state)
}
+46
View File
@@ -0,0 +1,46 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestManager_applyChats(t *testing.T) {
ctx := context.Background()
a := require.New(t)
_, m := testManager(t)
chats := []tg.ChatClass{
&tg.ChatEmpty{ID: 1},
&tg.Chat{ID: 2},
&tg.ChatForbidden{ID: 3},
&tg.Channel{ID: 4, AccessHash: 14},
&tg.ChannelForbidden{ID: 5, AccessHash: 15},
}
// Ensure nil safety.
a.NoError(m.applyChats(ctx, nil, nil, nil))
a.NoError(m.applyChats(ctx, chats...))
_, ok, err := m.storage.Find(ctx, Key{ID: 2, Prefix: chatsPrefix})
a.NoError(err)
a.True(ok)
_, ok, err = m.storage.Find(ctx, Key{ID: 3, Prefix: chatsPrefix})
a.NoError(err)
a.True(ok)
v, ok, err := m.storage.Find(ctx, Key{ID: 4, Prefix: channelPrefix})
a.NoError(err)
a.True(ok)
a.Equal(int64(14), v.AccessHash)
v, ok, err = m.storage.Find(ctx, Key{ID: 5, Prefix: channelPrefix})
a.NoError(err)
a.True(ok)
a.Equal(int64(15), v.AccessHash)
}
+20
View File
@@ -0,0 +1,20 @@
package peers
import (
"go.uber.org/atomic"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
type atomicUser struct {
value atomic.Value // holds *tg.User
}
func (u *atomicUser) Load() (*tg.User, bool) {
v, ok := u.value.Load().(*tg.User)
return v, ok
}
func (u *atomicUser) Store(user *tg.User) {
u.value.Store(user)
}
+48
View File
@@ -0,0 +1,48 @@
package peers
import (
"context"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// Bot is a bot User.
type Bot struct {
User
}
// ChatHistory whether can the bot see all messages in groups.
func (b Bot) ChatHistory() bool {
return b.raw.GetBotChatHistory()
}
// CanBeAdded whether can the bot be added to group.
func (b Bot) CanBeAdded() bool {
return !b.raw.GetBotNochats()
}
// InlineGeo whether the bot can request our geolocation in inline mode.
func (b Bot) InlineGeo() bool {
return b.raw.GetBotInlineGeo()
}
// InlinePlaceholder returns inline placeholder for this inline bot.
func (b Bot) InlinePlaceholder() (string, bool) {
return b.raw.GetBotInlinePlaceholder()
}
// SupportsInline whether the bot supports inline queries.
func (b Bot) SupportsInline() bool {
_, ok := b.InlinePlaceholder()
return ok
}
// BotInfo returns bot info.
func (b Bot) BotInfo(ctx context.Context) (tg.BotInfo, error) {
full, err := b.m.getUserFull(ctx, b.InputUser())
if err != nil {
return tg.BotInfo{}, err
}
return full.BotInfo, nil
}
+56
View File
@@ -0,0 +1,56 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestBot_BotInfo(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
u := m.User(&tg.User{
Bot: true,
ID: 10,
AccessHash: 10,
Username: "thebot",
})
b, ok := u.ToBot()
a.True(ok)
input := u.InputUser()
mock.ExpectCall(&tg.UsersGetFullUserRequest{ID: input}).ThenRPCErr(getTestError())
_, err := b.BotInfo(ctx)
a.Error(err)
testUserFull := getTestUserFull()
testUserFull.ID = u.raw.ID
{
i := tg.BotInfo{
UserID: u.raw.ID,
Description: "Test bot",
Commands: nil,
MenuButton: &tg.BotMenuButtonDefault{},
}
i.SetFlags()
testUserFull.SetBotInfo(i)
}
mock.ExpectCall(&tg.UsersGetFullUserRequest{ID: input}).ThenResult(&tg.UsersUserFull{
FullUser: testUserFull,
})
info, err := b.BotInfo(ctx)
a.NoError(err)
a.Equal(testUserFull.BotInfo, info)
// Test caching
info, err = b.BotInfo(ctx)
a.NoError(err)
a.Equal(testUserFull.BotInfo, info)
}
+71
View File
@@ -0,0 +1,71 @@
package peers
import (
"context"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// Broadcast is a broadcast Channel.
type Broadcast struct {
Channel
}
// Signatures whether signatures are enabled (channels).
func (b Broadcast) Signatures() bool {
return b.raw.GetSignatures()
}
// DiscussionGroup returns linked chat, if any.
func (b Broadcast) DiscussionGroup(ctx context.Context) (Channel, bool, error) {
full, err := b.FullRaw(ctx)
if err != nil {
return Channel{}, false, err
}
id, ok := full.GetLinkedChatID()
if !ok {
return Channel{}, false, nil
}
ch, err := b.m.GetChat(ctx, id)
if err != nil {
return Channel{}, false, err
}
actual, ok, err := ch.ActualChat(ctx)
if err != nil {
return Channel{}, false, err
}
if !ok {
return Channel{}, false, errors.Errorf("chat %d is linked, but not migrated", id)
}
return actual, true, nil
}
// SetDiscussionGroup associates a group to a channel as discussion group for that channel.
func (b Broadcast) SetDiscussionGroup(ctx context.Context, p tg.InputChannelClass) error {
if _, err := b.m.api.ChannelsSetDiscussionGroup(ctx, &tg.ChannelsSetDiscussionGroupRequest{
Broadcast: b.InputChannel(),
Group: p,
}); err != nil {
return errors.Wrap(err, "set discussion group")
}
return nil
}
// ToggleSignatures enable/disable message signatures in channels.
func (b Broadcast) ToggleSignatures(ctx context.Context, enabled bool) error {
if _, err := b.m.api.ChannelsToggleSignatures(ctx, &tg.ChannelsToggleSignaturesRequest{
Channel: b.InputChannel(),
SignaturesEnabled: enabled,
}); err != nil {
return errors.Wrap(err, "toggle signatures")
}
return nil
}
+99
View File
@@ -0,0 +1,99 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func getTestBroadcast() *tg.Channel {
testChannel := getTestChannel()
testChannel.ID *= 3
testChannel.Broadcast = true
testChannel.Megagroup = false
return testChannel
}
func TestBroadcast_SetDiscussionGroup(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
b := m.Channel(getTestSuperGroup())
s, ok := m.Channel(getTestBroadcast()).ToBroadcast()
a.True(ok)
mock.ExpectCall(&tg.ChannelsSetDiscussionGroupRequest{
Broadcast: s.InputChannel(),
Group: b.InputChannel(),
}).ThenRPCErr(getTestError())
a.Error(s.SetDiscussionGroup(ctx, b.InputChannel()))
mock.ExpectCall(&tg.ChannelsSetDiscussionGroupRequest{
Broadcast: s.InputChannel(),
Group: b.InputChannel(),
}).ThenTrue()
a.NoError(s.SetDiscussionGroup(ctx, b.InputChannel()))
}
func TestBroadcast_ToggleSignatures(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
ch := m.Channel(getTestBroadcast())
s, ok := ch.ToBroadcast()
a.True(ok)
mock.ExpectCall(&tg.ChannelsToggleSignaturesRequest{
Channel: s.InputChannel(),
SignaturesEnabled: true,
}).ThenRPCErr(getTestError())
a.Error(s.ToggleSignatures(ctx, true))
mock.ExpectCall(&tg.ChannelsToggleSignaturesRequest{
Channel: s.InputChannel(),
SignaturesEnabled: true,
}).ThenResult(&tg.Updates{})
a.NoError(s.ToggleSignatures(ctx, true))
}
func TestBroadcast_DiscussionGroup(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
linkedChat := getTestChat()
linkedSupergroup := getTestSuperGroup()
linkedChat.SetMigratedTo(linkedSupergroup.AsInput())
ch := m.Channel(getTestBroadcast())
s, ok := ch.ToBroadcast()
a.True(ok)
mock.ExpectCall(&tg.ChannelsGetFullChannelRequest{
Channel: s.InputChannel(),
}).ThenRPCErr(getTestError())
_, ok, err := s.DiscussionGroup(ctx)
a.False(ok)
a.Error(err)
full := getTestChannelFull()
full.SetLinkedChatID(linkedChat.ID)
mock.ExpectCall(&tg.ChannelsGetFullChannelRequest{
Channel: s.InputChannel(),
}).ThenResult(&tg.MessagesChatFull{
FullChat: full,
Chats: []tg.ChatClass{linkedChat, linkedSupergroup},
})
d, ok, err := s.DiscussionGroup(ctx)
a.True(ok)
a.NoError(err)
a.Equal(linkedSupergroup.ID, d.ID())
}
+301
View File
@@ -0,0 +1,301 @@
package peers
import (
"context"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/constant"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// Channel is channel peer.
type Channel struct {
raw *tg.Channel
m *Manager
}
// Channel creates new Channel, attached to this manager.
func (m *Manager) Channel(u *tg.Channel) Channel {
m.needsUpdate(channelPeerID(u.ID))
return Channel{
raw: u,
m: m,
}
}
// GetChannel gets Channel using given tg.InputChannelClass.
func (m *Manager) GetChannel(ctx context.Context, p tg.InputChannelClass) (Channel, error) {
ch, err := m.getChannel(ctx, p)
if err != nil {
return Channel{}, err
}
return m.Channel(ch), nil
}
// Raw returns raw *tg.Channel.
func (c Channel) Raw() *tg.Channel {
return c.raw
}
// ID returns entity ID.
func (c Channel) ID() int64 {
return c.raw.GetID()
}
// TDLibPeerID returns TDLibPeerID for this entity.
func (c Channel) TDLibPeerID() constant.TDLibPeerID {
return channelPeerID(c.raw.GetID())
}
// VisibleName returns visible name of peer.
//
// It returns FirstName + " " + LastName for users, and title for chats and channels.
func (c Channel) VisibleName() string {
return c.raw.GetTitle()
}
// Username returns peer username, if any.
func (c Channel) Username() (string, bool) {
return c.raw.GetUsername()
}
// Restricted whether this user/chat/channel is restricted.
func (c Channel) Restricted() ([]tg.RestrictionReason, bool) {
reason, ok := c.raw.GetRestrictionReason()
return reason, ok || c.raw.GetRestricted()
}
// Verified whether this user/chat/channel is verified by Telegram.
func (c Channel) Verified() bool {
return c.raw.Verified
}
// Scam whether this user/chat/channel is probably a scam.
func (c Channel) Scam() bool {
return c.raw.Scam
}
// Fake whether this user/chat/channel was reported by many users as a fake or scam: be
// careful when interacting with it.
func (c Channel) Fake() bool {
return c.raw.Fake
}
// InputPeer returns input peer for this peer.
func (c Channel) InputPeer() tg.InputPeerClass {
return &tg.InputPeerChannel{
ChannelID: c.raw.ID,
AccessHash: c.raw.AccessHash,
}
}
// Sync updates current object.
func (c Channel) Sync(ctx context.Context) error {
raw, err := c.m.updateChannel(ctx, c.raw.AsInput())
if err != nil {
return errors.Wrap(err, "get channel")
}
*c.raw = *raw
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{
Peer: c.InputPeer(),
Reason: reason,
Message: message,
}); err != nil {
return errors.Wrap(err, "report")
}
return nil
}
// Photo returns peer photo, if any.
func (c Channel) Photo(ctx context.Context) (*tg.Photo, bool, error) {
full, err := c.FullRaw(ctx)
if err != nil {
return nil, false, err
}
p, ok := full.ChatPhoto.AsNotEmpty()
return p, ok, nil
}
// FullRaw returns *tg.ChannelFull for this Channel.
func (c Channel) FullRaw(ctx context.Context) (*tg.ChannelFull, error) {
return c.m.getChannelFull(ctx, c.InputChannel())
}
// ToBroadcast tries to convert this Channel to Broadcast.
func (c Channel) ToBroadcast() (Broadcast, bool) {
if !c.IsBroadcast() {
return Broadcast{}, false
}
return Broadcast{
Channel: c,
}, 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.IsSupergroup() {
return Supergroup{}, false
}
return Supergroup{
Channel: c,
}, 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{
peer: c,
m: c.m,
}
}
// InputChannel returns input user for this user.
func (c Channel) InputChannel() tg.InputChannelClass {
return &tg.InputChannel{
ChannelID: c.raw.ID,
AccessHash: c.raw.AccessHash,
}
}
// Creator whether the current user is the creator of this channel.
func (c Channel) Creator() bool {
return c.raw.Creator
}
// Left whether the current user has left this channel.
func (c Channel) Left() bool {
return c.raw.Left
}
// HasLink whether this channel has a private join link.
func (c Channel) HasLink() bool {
return c.raw.HasLink
}
// HasGeo whether this channel has a geoposition.
func (c Channel) HasGeo() bool {
return c.raw.HasGeo
}
// CallActive whether a group call or livestream is currently active.
func (c Channel) CallActive() bool {
return c.raw.CallActive
}
// CallNotEmpty whether there's anyone in the group call or livestream.
func (c Channel) CallNotEmpty() bool {
return c.raw.CallNotEmpty
}
// NoForwards whether that message forwarding from this channel is not allowed.
func (c Channel) NoForwards() bool {
return c.raw.Noforwards
}
// AdminRights returns admin rights of the user in this channel.
//
// 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()
}
// BannedRights returns banned rights of the user in this channel.
//
// 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()
}
// DefaultBannedRights returns default chat rights.
//
// 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()
}
// ParticipantsCount returns count of participants.
func (c Channel) ParticipantsCount() int {
v, _ := c.raw.GetParticipantsCount()
return v
}
// Join joins this channel.
func (c Channel) Join(ctx context.Context) error {
if _, err := c.m.api.ChannelsJoinChannel(ctx, c.InputChannel()); err != nil {
return errors.Wrap(err, "join channel")
}
return nil
}
// Delete deletes this channel.
func (c Channel) Delete(ctx context.Context) error {
if _, err := c.m.api.ChannelsDeleteChannel(ctx, c.InputChannel()); err != nil {
return errors.Wrap(err, "delete channel")
}
return nil
}
// Leave leaves this channel.
func (c Channel) Leave(ctx context.Context) error {
if _, err := c.m.api.ChannelsLeaveChannel(ctx, c.InputChannel()); err != nil {
return errors.Wrap(err, "leave channel")
}
return nil
}
// SetTitle sets new title for this Chat.
func (c Channel) SetTitle(ctx context.Context, title string) error {
if _, err := c.m.api.ChannelsEditTitle(ctx, &tg.ChannelsEditTitleRequest{
Channel: c.InputChannel(),
Title: title,
}); err != nil {
return errors.Wrap(err, "edit channel title")
}
return nil
}
// SetDescription sets new description for this Chat.
func (c Channel) SetDescription(ctx context.Context, about string) error {
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 ...tg.ReactionClass) error {
return c.m.editReactions(ctx, c.InputPeer(), &tg.ChatReactionsSome{
Reactions: reactions,
})
}
// DisableReactions disables reactions.
func (c Channel) DisableReactions(ctx context.Context) error {
return c.m.editReactions(ctx, c.InputPeer(), &tg.ChatReactionsNone{})
}
// TODO(tdakkota): add more getters, helpers and convertors
+190
View File
@@ -0,0 +1,190 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestChannelGetters(t *testing.T) {
a := require.New(t)
u := Channel{
raw: &tg.Channel{
Creator: true,
Left: true,
Broadcast: true,
Verified: true,
Megagroup: true,
Restricted: true,
Signatures: true,
Min: true,
Scam: true,
HasLink: true,
HasGeo: true,
SlowmodeEnabled: true,
CallActive: true,
CallNotEmpty: true,
Fake: true,
Gigagroup: true,
Noforwards: true,
ID: 10,
AccessHash: 10,
Title: "Title",
Username: "username",
Date: 10,
AdminRights: tg.ChatAdminRights{AddAdmins: true},
BannedRights: tg.ChatBannedRights{},
DefaultBannedRights: tg.ChatBannedRights{EmbedLinks: true},
ParticipantsCount: 10,
},
}
u.raw.SetFlags()
a.Equal(u.raw, u.Raw())
a.True(u.TDLibPeerID().IsChannel())
a.Equal("Title", u.VisibleName())
a.Equal(&tg.InputPeerChannel{ChannelID: u.raw.ID, AccessHash: u.raw.AccessHash}, u.InputPeer())
a.Equal(u.raw.GetID(), u.ID())
a.Equal(u.raw.Creator, u.Creator())
a.Equal(u.raw.Left, u.Left())
a.Equal(u.raw.Verified, u.Verified())
a.Equal(u.raw.Scam, u.Scam())
a.Equal(u.raw.HasLink, u.HasLink())
a.Equal(u.raw.HasGeo, u.HasGeo())
a.Equal(u.raw.CallActive, u.CallActive())
a.Equal(u.raw.CallNotEmpty, u.CallNotEmpty())
a.Equal(u.raw.Fake, u.Fake())
a.Equal(u.raw.Noforwards, u.NoForwards())
{
reasons, ok := u.Restricted()
a.Equal(u.raw.GetRestricted(), ok)
a.Equal(u.raw.RestrictionReason, reasons)
}
{
s, ok := u.ToSupergroup()
a.Equal(s.raw.Megagroup, ok)
a.Equal(s.raw.Signatures, s.SlowmodeEnabled())
}
{
b, ok := u.ToBroadcast()
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) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
ch := m.Channel(getTestChannel())
mock.ExpectCall(&tg.ChannelsLeaveChannelRequest{
Channel: ch.InputChannel(),
}).ThenRPCErr(getTestError())
a.Error(ch.Leave(ctx))
mock.ExpectCall(&tg.ChannelsLeaveChannelRequest{
Channel: ch.InputChannel(),
}).ThenResult(&tg.Updates{})
a.NoError(ch.Leave(ctx))
}
func TestChannel_SetTitle(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
title := "title"
ch := m.Channel(getTestChannel())
mock.ExpectCall(&tg.ChannelsEditTitleRequest{
Channel: ch.InputChannel(),
Title: title,
}).ThenRPCErr(getTestError())
a.Error(ch.SetTitle(ctx, title))
mock.ExpectCall(&tg.ChannelsEditTitleRequest{
Channel: ch.InputChannel(),
Title: title,
}).ThenResult(&tg.Updates{})
a.NoError(ch.SetTitle(ctx, title))
}
func TestChannel_SetDescription(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
about := "about"
ch := m.Channel(getTestChannel())
mock.ExpectCall(&tg.MessagesEditChatAboutRequest{
Peer: ch.InputPeer(),
About: about,
}).ThenRPCErr(getTestError())
a.Error(ch.SetDescription(ctx, about))
mock.ExpectCall(&tg.MessagesEditChatAboutRequest{
Peer: ch.InputPeer(),
About: about,
}).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))
}
+296
View File
@@ -0,0 +1,296 @@
package peers
import (
"context"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/constant"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// Chat is chat peer.
type Chat struct {
raw *tg.Chat
m *Manager
}
// Chat creates new Chat, attached to this manager.
func (m *Manager) Chat(u *tg.Chat) Chat {
m.needsUpdate(chatPeerID(u.ID))
return Chat{
raw: u,
m: m,
}
}
// GetChat gets Chat using given id.
func (m *Manager) GetChat(ctx context.Context, id int64) (Chat, error) {
ch, err := m.getChat(ctx, id)
if err != nil {
return Chat{}, err
}
return m.Chat(ch), nil
}
// Raw returns raw *tg.Chat.
func (c Chat) Raw() *tg.Chat {
return c.raw
}
// ID returns entity ID.
func (c Chat) ID() int64 {
return c.raw.GetID()
}
// TDLibPeerID returns TDLibPeerID for this entity.
func (c Chat) TDLibPeerID() constant.TDLibPeerID {
return chatPeerID(c.raw.GetID())
}
// VisibleName returns visible name of peer.
//
// It returns FirstName + " " + LastName for users, and title for chats and channels.
func (c Chat) VisibleName() string {
return c.raw.GetTitle()
}
// Username returns peer username, if any.
func (c Chat) Username() (string, bool) {
return "", false
}
// Restricted whether this user/chat/channel is restricted.
func (c Chat) Restricted() ([]tg.RestrictionReason, bool) {
return nil, false
}
// Verified whether this user/chat/channel is verified by Telegram.
func (c Chat) Verified() bool {
return false
}
// Scam whether this user/chat/channel is probably a scam.
func (c Chat) Scam() bool {
return false
}
// Fake whether this user/chat/channel was reported by many users as a fake or scam: be
// careful when interacting with it.
func (c Chat) Fake() bool {
return false
}
// InputPeer returns input peer for this peer.
func (c Chat) InputPeer() tg.InputPeerClass {
return c.raw.AsInputPeer()
}
// Sync updates current object.
func (c Chat) Sync(ctx context.Context) error {
raw, err := c.m.updateChat(ctx, c.raw.ID)
if err != nil {
return errors.Wrap(err, "get chat")
}
*c.raw = *raw
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{
Peer: c.InputPeer(),
Reason: reason,
Message: message,
}); err != nil {
return errors.Wrap(err, "report")
}
return nil
}
// Photo returns peer photo, if any.
func (c Chat) Photo(ctx context.Context) (*tg.Photo, bool, error) {
full, err := c.FullRaw(ctx)
if err != nil {
return nil, false, errors.Wrap(err, "get full chat")
}
chatPhoto, ok := full.GetChatPhoto()
if !ok {
return nil, false, nil
}
p, ok := chatPhoto.AsNotEmpty()
return p, ok, nil
}
// FullRaw returns *tg.ChatFull for this Chat.
func (c Chat) FullRaw(ctx context.Context) (*tg.ChatFull, error) {
return c.m.getChatFull(ctx, c.ID())
}
// InviteLinks returns InviteLinks for this peer.
func (c Chat) InviteLinks() InviteLinks {
return InviteLinks{
peer: c,
m: c.m,
}
}
// ToBroadcast tries to convert this Chat to Broadcast.
func (c Chat) ToBroadcast() (Broadcast, bool) {
return Broadcast{}, c.IsBroadcast()
}
// 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{}, c.IsSupergroup()
}
// 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()
}
// Left whether the current user has left this group.
func (c Chat) Left() bool {
return c.raw.GetLeft()
}
// Deactivated whether the group was migrated.
func (c Chat) Deactivated() bool {
return c.raw.GetDeactivated()
}
// CallActive whether a group call or livestream is currently active.
func (c Chat) CallActive() bool {
return c.raw.GetCallActive()
}
// CallNotEmpty whether there's anyone in the group call or livestream.
func (c Chat) CallNotEmpty() bool {
return c.raw.GetCallNotEmpty()
}
// NoForwards whether that message forwarding from this channel is not allowed.
func (c Chat) NoForwards() bool {
return c.raw.GetNoforwards()
}
// MigratedTo returns a supergroup to which this chat migrated.
func (c Chat) MigratedTo() (tg.InputChannelClass, bool) {
return c.raw.GetMigratedTo()
}
// ParticipantsCount returns count of participants.
func (c Chat) ParticipantsCount() int {
return c.raw.GetParticipantsCount()
}
// AdminRights returns admin rights of the user in this channel.
//
// 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()
}
// DefaultBannedRights returns default chat rights.
//
// 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()
}
// ActualChat returns Channel to which this chat migrated.
//
// Also see MigratedTo.
func (c Chat) ActualChat(ctx context.Context) (Channel, bool, error) {
m, ok := c.MigratedTo()
if !ok {
return Channel{}, false, nil
}
ch, err := c.m.GetChannel(ctx, m)
if err != nil {
return Channel{}, false, errors.Wrap(err, "get actual chat")
}
return ch, true, nil
}
// Leave leaves this chat.
func (c Chat) Leave(ctx context.Context) error {
return c.deleteMe(ctx, false)
}
// SetTitle sets new title for this Chat.
func (c Chat) SetTitle(ctx context.Context, title string) error {
if _, err := c.m.api.MessagesEditChatTitle(ctx, &tg.MessagesEditChatTitleRequest{
ChatID: c.ID(),
Title: title,
}); err != nil {
return errors.Wrap(err, "edit chat title")
}
return nil
}
// SetDescription sets new description for this Chat.
func (c Chat) SetDescription(ctx context.Context, about string) error {
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 ...tg.ReactionClass) error {
return c.m.editReactions(ctx, c.InputPeer(), &tg.ChatReactionsSome{
Reactions: reactions,
})
}
// DisableReactions disables reactions.
func (c Chat) DisableReactions(ctx context.Context) error {
return c.m.editReactions(ctx, c.InputPeer(), &tg.ChatReactionsNone{})
}
// 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
}
// TODO(tdakkota): add more getters, helpers and convertors
+162
View File
@@ -0,0 +1,162 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestChatGetters(t *testing.T) {
a := require.New(t)
u := Chat{
raw: &tg.Chat{
Creator: true,
Left: true,
Deactivated: true,
CallActive: true,
CallNotEmpty: true,
Noforwards: true,
ID: 10,
Title: "Title",
ParticipantsCount: 10,
Date: 10,
Version: 1,
AdminRights: tg.ChatAdminRights{AddAdmins: true},
DefaultBannedRights: tg.ChatBannedRights{EmbedLinks: true},
},
}
u.raw.SetFlags()
a.Equal(u.raw, u.Raw())
a.True(u.TDLibPeerID().IsChat())
a.Equal("Title", u.VisibleName())
a.Equal(&tg.InputPeerChat{ChatID: u.raw.ID}, u.InputPeer())
a.False(u.Verified())
a.False(u.Scam())
a.False(u.Fake())
a.Equal(u.raw.GetID(), u.ID())
a.Equal(u.raw.Creator, u.Creator())
a.Equal(u.raw.Left, u.Left())
a.Equal(u.raw.Deactivated, u.Deactivated())
a.Equal(u.raw.CallActive, u.CallActive())
a.Equal(u.raw.CallNotEmpty, u.CallNotEmpty())
a.Equal(u.raw.Noforwards, u.NoForwards())
{
_, ok := u.ToSupergroup()
a.False(ok)
}
{
_, ok := u.ToBroadcast()
a.False(ok)
}
}
func TestChat_Leave(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
ch := m.Chat(getTestChat())
mock.ExpectCall(&tg.MessagesDeleteChatUserRequest{
RevokeHistory: false,
ChatID: ch.ID(),
UserID: &tg.InputUserSelf{},
}).ThenRPCErr(getTestError())
a.Error(ch.Leave(ctx))
mock.ExpectCall(&tg.MessagesDeleteChatUserRequest{
RevokeHistory: false,
ChatID: ch.ID(),
UserID: &tg.InputUserSelf{},
}).ThenResult(&tg.Updates{})
a.NoError(ch.Leave(ctx))
}
func TestChat_SetTitle(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
title := "title"
ch := m.Chat(getTestChat())
mock.ExpectCall(&tg.MessagesEditChatTitleRequest{
ChatID: ch.ID(),
Title: title,
}).ThenRPCErr(getTestError())
a.Error(ch.SetTitle(ctx, title))
mock.ExpectCall(&tg.MessagesEditChatTitleRequest{
ChatID: ch.ID(),
Title: title,
}).ThenResult(&tg.Updates{})
a.NoError(ch.SetTitle(ctx, title))
}
func TestChat_SetDescription(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
about := "about"
ch := m.Chat(getTestChat())
mock.ExpectCall(&tg.MessagesEditChatAboutRequest{
Peer: ch.InputPeer(),
About: about,
}).ThenRPCErr(getTestError())
a.Error(ch.SetDescription(ctx, about))
mock.ExpectCall(&tg.MessagesEditChatAboutRequest{
Peer: ch.InputPeer(),
About: about,
}).ThenTrue()
a.NoError(ch.SetDescription(ctx, about))
}
func TestChat_LeaveAndDelete(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
ch := m.Chat(getTestChat())
mock.ExpectCall(&tg.MessagesDeleteChatUserRequest{
RevokeHistory: true,
ChatID: ch.ID(),
UserID: &tg.InputUserSelf{},
}).ThenRPCErr(getTestError())
a.Error(ch.LeaveAndDelete(ctx))
mock.ExpectCall(&tg.MessagesDeleteChatUserRequest{
RevokeHistory: true,
ChatID: ch.ID(),
UserID: &tg.InputUserSelf{},
}).ThenResult(&tg.Updates{})
a.NoError(ch.LeaveAndDelete(ctx))
}
func TestChat_ActualChat(t *testing.T) {
a := require.New(t)
ctx := context.Background()
_, m := testManager(t)
ch := m.Chat(getTestChat())
_, ok, err := ch.ActualChat(ctx)
a.NoError(err)
a.False(ok)
newChat := m.Channel(getTestChannel())
a.NoError(m.applyChats(ctx, newChat.raw))
ch.raw.SetMigratedTo(newChat.InputChannel())
actual, ok, err := ch.ActualChat(ctx)
a.NoError(err)
a.True(ok)
a.Equal(newChat.ID(), actual.ID())
}
+27
View File
@@ -0,0 +1,27 @@
package peers
import (
"fmt"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// PhoneNotFoundError is returned when Manager unable to find contact with given phone.
type PhoneNotFoundError struct {
Phone string
}
// Error implements error.
func (c *PhoneNotFoundError) Error() string {
return "contact not found"
}
// PeerNotFoundError is returned when Manager unable to find Peer with given tg.PeerClass.
type PeerNotFoundError struct {
Peer tg.PeerClass
}
// Error implements error.
func (p *PeerNotFoundError) Error() string {
return fmt.Sprintf("can't resolve %v", p.Peer)
}
+74
View File
@@ -0,0 +1,74 @@
package peers
import (
"context"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/constant"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
"go.mau.fi/mautrix-telegram/pkg/gotd/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
}
+103
View File
@@ -0,0 +1,103 @@
package peers
import (
"context"
"go.uber.org/multierr"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/message/entity"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/updates"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
var _ updates.AccessHasher = (*Manager)(nil)
func (m *Manager) SetChannelAccessHash(ctx context.Context, forUserID, channelID, accessHash int64) error {
myID, ok := m.myID()
if !ok || myID != forUserID {
return nil
}
return m.storage.Save(ctx, Key{
Prefix: channelPrefix,
ID: channelID,
}, Value{
AccessHash: accessHash,
})
}
func (m *Manager) GetChannelAccessHash(ctx context.Context, forUserID, channelID int64) (accessHash int64, found bool, err error) {
myID, ok := m.myID()
if !ok || myID != forUserID {
return 0, false, nil
}
v, found, err := m.storage.Find(ctx, Key{
Prefix: channelPrefix,
ID: channelID,
})
return v.AccessHash, found, err
}
func (m *Manager) SetUserAccessHash(ctx context.Context, forUserID, userID, accessHash int64) error {
myID, ok := m.myID()
if !ok || myID != forUserID {
return nil
}
return m.storage.Save(ctx, Key{
Prefix: usersPrefix,
ID: userID,
}, Value{
AccessHash: accessHash,
})
}
func (m *Manager) GetUserAccessHash(ctx context.Context, forUserID, userID int64) (accessHash int64, found bool, err error) {
myID, ok := m.myID()
if !ok || myID != forUserID {
return 0, false, nil
}
v, found, err := m.storage.Find(ctx, Key{
Prefix: usersPrefix,
ID: userID,
})
return v.AccessHash, found, err
}
// UpdateHook returns update middleware hook for collecting entities.
func (m *Manager) UpdateHook(next telegram.UpdateHandler) telegram.UpdateHandler {
f := func(ctx context.Context, u tg.UpdatesClass) error {
var (
users []tg.UserClass
chats []tg.ChatClass
updates []tg.UpdateClass
)
switch u := u.(type) {
case *tg.UpdatesCombined:
users = u.GetUsers()
chats = u.GetChats()
updates = u.GetUpdates()
case *tg.Updates:
users = u.GetUsers()
chats = u.GetChats()
updates = u.GetUpdates()
}
m.applyUpdates(updates)
applyErr := m.applyEntities(ctx, users, chats)
handleErr := next.Handle(ctx, u)
return multierr.Append(handleErr, applyErr)
}
return telegram.UpdateHandlerFunc(f)
}
// UserResolveHook creates entity.UserResolver attached to this Manager.
func (m *Manager) UserResolveHook(ctx context.Context) entity.UserResolver {
return func(id int64) (tg.InputUserClass, error) {
r, err := m.getUser(ctx, &tg.InputUser{UserID: id})
if err != nil {
return nil, err
}
return r.AsInput(), nil
}
}
+118
View File
@@ -0,0 +1,118 @@
package peers
import (
"context"
"time"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// InviteLink represents invite link.
type InviteLink struct {
peer Peer
m *Manager
raw tg.ChatInviteExported
newInvite tg.ChatInviteExported
}
func (e InviteLinks) inviteLink(raw tg.ChatInviteExported) InviteLink {
return InviteLink{
peer: e.peer,
m: e.m,
raw: raw,
}
}
func (e InviteLinks) replacedLink(raw, newInvite tg.ChatInviteExported) InviteLink {
link := e.inviteLink(raw)
link.newInvite = newInvite
return link
}
// ReplacedWith returns new InviteLink, if any.
func (l InviteLink) ReplacedWith() (InviteLink, bool) {
return InviteLink{
peer: l.peer,
m: l.m,
raw: l.newInvite,
}, !l.newInvite.Zero()
}
// Raw returns raw tg.ChatInviteExported.
func (l InviteLink) Raw() *tg.ChatInviteExported {
return &l.raw
}
// Revoked whether this chat invite was revoked
func (l InviteLink) Revoked() bool {
return l.raw.GetRevoked()
}
// Permanent whether this chat invite has no expiration
func (l InviteLink) Permanent() bool {
return l.raw.GetPermanent()
}
// RequestNeeded whether users joining the chat via the link need to be approved by chat administrators.
func (l InviteLink) RequestNeeded() bool {
return l.raw.GetRequestNeeded()
}
// Link returns chat invitation link.
func (l InviteLink) Link() string {
return l.raw.GetLink()
}
// Creator returns link creator.
func (l InviteLink) Creator(ctx context.Context) (User, error) {
return l.m.GetUser(ctx, &tg.InputUser{
UserID: l.raw.AdminID,
})
}
func telegramDate(date int) time.Time {
return time.Unix(int64(date), 0)
}
// CreatedAt returns time when was this chat invite created.
func (l InviteLink) CreatedAt() time.Time {
return telegramDate(l.raw.GetDate())
}
// StartDate returns time when was this chat invite last modified.
func (l InviteLink) StartDate() (time.Time, bool) {
v, ok := l.raw.GetStartDate()
if !ok {
return time.Time{}, false
}
return telegramDate(v), true
}
// ExpireDate returns time when does this chat invite expire.
func (l InviteLink) ExpireDate() (time.Time, bool) {
v, ok := l.raw.GetExpireDate()
if !ok {
return time.Time{}, false
}
return telegramDate(v), true
}
// UsageLimit returns maximum number of users that can join using this link.
func (l InviteLink) UsageLimit() (int, bool) {
return l.raw.GetUsageLimit()
}
// Usage returns how many users joined using this link.
func (l InviteLink) Usage() (int, bool) {
return l.raw.GetUsage()
}
// Requested returns number of pending join requests.
func (l InviteLink) Requested() (int, bool) {
return l.raw.GetRequested()
}
// Title of this link.
func (l InviteLink) Title() (string, bool) {
return l.raw.GetTitle()
}
@@ -0,0 +1,33 @@
package peers
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestInviteLinkGetters(t *testing.T) {
a := require.New(t)
testExported := testChatInviteExported()
replacer := testExported
replacer.Link += "/aboba"
link := InviteLink{
raw: *testExported,
newInvite: *replacer,
}
a.Equal(testExported, link.Raw())
{
replacedWith, ok := link.ReplacedWith()
a.True(ok)
a.Equal(replacer, replacedWith.Raw())
}
a.Equal(testExported.Revoked, link.Revoked())
a.Equal(testExported.Permanent, link.Permanent())
a.Equal(testExported.RequestNeeded, link.RequestNeeded())
a.Equal(testExported.Link, link.Link())
{
date, _ := link.ExpireDate()
a.Equal(testExported.ExpireDate, int(date.Unix()))
}
}
+205
View File
@@ -0,0 +1,205 @@
package peers
import (
"context"
"time"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// InviteLinks represents invite links of Chat or Channel.
type InviteLinks struct {
peer Peer
m *Manager
}
// ExportLinkOptions is options for ExportNew.
type ExportLinkOptions struct {
// Whether users joining the chat via the link need to be approved by chat administrators.
RequestNeeded bool
// Expiration date.
//
// If zero, will not be used.
ExpireDate time.Time
// Maximum number of users that can join using this link.
//
// If zero, will not be used.
UsageLimit int
// Title of this link.
//
// If zero, will not be used.
Title string
}
// ExportNew creates new primary invite link for a chat.
//
// Notice: Any previously generated primary link is revoked.
//
// See also AddNew.
func (e InviteLinks) ExportNew(ctx context.Context, opts ExportLinkOptions) (InviteLink, error) {
return e.newLink(ctx, true, opts)
}
// AddNew creates an additional invite link for a chat.
func (e InviteLinks) AddNew(ctx context.Context, opts ExportLinkOptions) (InviteLink, error) {
return e.newLink(ctx, false, opts)
}
// Get returns link info.
func (e InviteLinks) Get(ctx context.Context, link string) (InviteLink, error) {
r, err := e.m.api.MessagesGetExportedChatInvite(ctx, &tg.MessagesGetExportedChatInviteRequest{
Peer: e.peer.InputPeer(),
Link: link,
})
if err != nil {
return InviteLink{}, errors.Wrap(err, "get chat invite")
}
return e.applyExportedInvite(ctx, r)
}
// Edit edits link info.
func (e InviteLinks) Edit(ctx context.Context, link string, opts ExportLinkOptions) (InviteLink, error) {
req := tg.MessagesEditExportedChatInviteRequest{
Revoked: false,
Peer: e.peer.InputPeer(),
Link: link,
ExpireDate: 0,
UsageLimit: opts.UsageLimit,
RequestNeeded: opts.RequestNeeded,
Title: opts.Title,
}
if e := opts.ExpireDate; !e.IsZero() {
req.ExpireDate = int(e.Unix())
}
return e.edit(ctx, "edit chat invite", req)
}
// Revoke revokes invite link and returns revoked link info.
//
// If the primary link is revoked, a new link is automatically generated.
func (e InviteLinks) Revoke(ctx context.Context, link string) (InviteLink, error) {
return e.edit(ctx, "revoke chat invite", tg.MessagesEditExportedChatInviteRequest{
Revoked: true,
Peer: e.peer.InputPeer(),
Link: link,
})
}
func (e InviteLinks) edit(
ctx context.Context,
msg string,
req tg.MessagesEditExportedChatInviteRequest,
) (InviteLink, error) {
r, err := e.m.api.MessagesEditExportedChatInvite(ctx, &req)
if err != nil {
return InviteLink{}, errors.Wrap(err, msg)
}
return e.applyExportedInvite(ctx, r)
}
// Delete deletes invite link.
//
// Not available for bots.
func (e InviteLinks) Delete(ctx context.Context, link string) error {
if _, err := e.m.api.MessagesDeleteExportedChatInvite(ctx, &tg.MessagesDeleteExportedChatInviteRequest{
Peer: e.peer.InputPeer(),
Link: link,
}); err != nil {
return errors.Wrap(err, "delete chat invite")
}
return nil
}
// ApproveJoin approves join request for given user.
func (e InviteLinks) ApproveJoin(ctx context.Context, user tg.InputUserClass) error {
return e.hideJoinRequest(ctx, true, user)
}
// DeclineJoin declines join request for given user.
func (e InviteLinks) DeclineJoin(ctx context.Context, user tg.InputUserClass) error {
return e.hideJoinRequest(ctx, false, user)
}
func (e InviteLinks) hideJoinRequest(ctx context.Context, approved bool, user tg.InputUserClass) error {
if _, err := e.m.api.MessagesHideChatJoinRequest(ctx, &tg.MessagesHideChatJoinRequestRequest{
Approved: approved,
Peer: e.peer.InputPeer(),
UserID: user,
}); err != nil {
return errors.Wrapf(err, "hide join (approved: %t)", approved)
}
return nil
}
func (InviteLinks) chatInviteExportedFrom(v tg.ExportedChatInviteClass) (*tg.ChatInviteExported, error) {
// https://github.com/gotd/td/issues/788
// case *tg.ChatInvitePublicJoinRequests: // chatInvitePublicJoinRequests#ed107ab7 not supported
switch invite := v.(type) {
case *tg.ChatInviteExported:
return invite, nil
default:
return nil, errors.Errorf("unsupported %T", invite)
}
}
func (e InviteLinks) applyExportedInvite(
ctx context.Context,
r tg.MessagesExportedChatInviteClass,
) (InviteLink, error) {
if err := e.m.applyUsers(ctx, r.GetUsers()...); err != nil {
return InviteLink{}, errors.Wrap(err, "update users")
}
switch r := r.(type) {
case *tg.MessagesExportedChatInviteReplaced:
from, err := e.chatInviteExportedFrom(r.GetInvite())
if err != nil {
return InviteLink{}, errors.Wrap(err, "from")
}
to, err := e.chatInviteExportedFrom(r.GetNewInvite())
if err != nil {
return InviteLink{}, errors.Wrap(err, "to")
}
return e.replacedLink(*from, *to), nil
}
exported, err := e.chatInviteExportedFrom(r.GetInvite())
if err != nil {
return InviteLink{}, errors.Wrap(err, "convert invite")
}
return e.inviteLink(*exported), nil
}
func (e InviteLinks) newLink(
ctx context.Context,
revokeOld bool,
opts ExportLinkOptions,
) (InviteLink, error) {
req := &tg.MessagesExportChatInviteRequest{
LegacyRevokePermanent: revokeOld,
RequestNeeded: opts.RequestNeeded,
Peer: e.peer.InputPeer(),
ExpireDate: 0,
UsageLimit: opts.UsageLimit,
Title: opts.Title,
}
if e := opts.ExpireDate; !e.IsZero() {
req.ExpireDate = int(e.Unix())
}
invite, err := e.m.api.MessagesExportChatInvite(ctx, req)
if err != nil {
return InviteLink{}, errors.Wrap(err, "create invite")
}
exported, err := e.chatInviteExportedFrom(invite)
if err != nil {
return InviteLink{}, errors.Wrap(err, "convert invite")
}
return e.inviteLink(*exported), nil
}
// TODO(tdakkota): add methods with pagination, when query will be updated
@@ -0,0 +1,191 @@
package peers
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func testExportLinkOptions() ExportLinkOptions {
return ExportLinkOptions{
RequestNeeded: true,
ExpireDate: time.Now(),
UsageLimit: 1,
Title: "Title",
}
}
func testChatInviteExported() *tg.ChatInviteExported {
opts := testExportLinkOptions()
r := tg.ChatInviteExported{
AdminID: getTestUser().ID,
RequestNeeded: opts.RequestNeeded,
ExpireDate: int(opts.ExpireDate.Unix()),
UsageLimit: opts.UsageLimit,
Title: opts.Title,
}
r.SetFlags()
return &r
}
func TestInviteLinks_newLink(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
opts := testExportLinkOptions()
ch := m.Channel(getTestChannel())
links := ch.InviteLinks()
mock.ExpectCall(&tg.MessagesExportChatInviteRequest{
LegacyRevokePermanent: false,
RequestNeeded: opts.RequestNeeded,
Peer: ch.InputPeer(),
ExpireDate: int(opts.ExpireDate.Unix()),
UsageLimit: opts.UsageLimit,
Title: opts.Title,
}).ThenRPCErr(getTestError())
_, err := links.AddNew(ctx, opts)
a.Error(err)
result := testChatInviteExported()
mock.ExpectCall(&tg.MessagesExportChatInviteRequest{
LegacyRevokePermanent: true,
RequestNeeded: opts.RequestNeeded,
Peer: ch.InputPeer(),
ExpireDate: int(opts.ExpireDate.Unix()),
UsageLimit: opts.UsageLimit,
Title: opts.Title,
}).ThenResult(result)
got, err := links.ExportNew(ctx, opts)
a.NoError(err)
a.Equal(result, got.Raw())
}
func TestInviteLinks_Get(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
link := "https://gotd.dev"
ch := m.Chat(getTestChat())
links := ch.InviteLinks()
mock.ExpectCall(&tg.MessagesGetExportedChatInviteRequest{
Peer: ch.InputPeer(),
Link: link,
}).ThenRPCErr(getTestError())
_, err := links.Get(ctx, link)
a.Error(err)
testExported := testChatInviteExported()
replacer := testExported
replacer.Link += "/aboba"
result := &tg.MessagesExportedChatInviteReplaced{
Invite: testExported,
NewInvite: replacer,
Users: []tg.UserClass{getTestUser()},
}
mock.ExpectCall(&tg.MessagesGetExportedChatInviteRequest{
Peer: ch.InputPeer(),
Link: link,
}).ThenResult(result)
got, err := links.Get(ctx, link)
a.NoError(err)
a.Equal(result.Invite, got.Raw())
u, err := got.Creator(ctx)
a.NoError(err)
a.Equal(testExported.AdminID, u.ID())
}
func TestInviteLinks_edit(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
link := "https://gotd.dev"
opts := testExportLinkOptions()
ch := m.Chat(getTestChat())
links := ch.InviteLinks()
req := &tg.MessagesEditExportedChatInviteRequest{
Revoked: false,
Peer: ch.InputPeer(),
Link: link,
ExpireDate: int(opts.ExpireDate.Unix()),
UsageLimit: opts.UsageLimit,
RequestNeeded: opts.RequestNeeded,
Title: opts.Title,
}
mock.ExpectCall(req).ThenRPCErr(getTestError())
_, err := links.Edit(ctx, link, opts)
a.Error(err)
result := &tg.MessagesExportedChatInvite{
Invite: testChatInviteExported(),
}
mock.ExpectCall(req).ThenResult(result)
got, err := links.Edit(ctx, link, opts)
a.NoError(err)
a.Equal(result.Invite, got.Raw())
mock.ExpectCall(&tg.MessagesEditExportedChatInviteRequest{
Revoked: true,
Peer: ch.InputPeer(),
Link: link,
}).ThenResult(result)
got, err = links.Revoke(ctx, link)
a.NoError(err)
a.Equal(result.Invite, got.Raw())
}
func TestInviteLinks_Delete(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
link := "https://gotd.dev"
ch := m.Chat(getTestChat())
links := ch.InviteLinks()
mock.ExpectCall(&tg.MessagesDeleteExportedChatInviteRequest{
Peer: ch.InputPeer(),
Link: link,
}).ThenRPCErr(getTestError())
a.Error(links.Delete(ctx, link))
mock.ExpectCall(&tg.MessagesDeleteExportedChatInviteRequest{
Peer: ch.InputPeer(),
Link: link,
}).ThenTrue()
a.NoError(links.Delete(ctx, link))
}
func TestInviteLinks_hideJoinRequest(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
user := getTestUser().AsInput()
ch := m.Chat(getTestChat())
links := ch.InviteLinks()
mock.ExpectCall(&tg.MessagesHideChatJoinRequestRequest{
Approved: true,
Peer: ch.InputPeer(),
UserID: user,
}).ThenRPCErr(getTestError())
a.Error(links.ApproveJoin(ctx, user))
mock.ExpectCall(&tg.MessagesHideChatJoinRequestRequest{
Approved: false,
Peer: ch.InputPeer(),
UserID: user,
}).ThenResult(&tg.Updates{})
a.NoError(links.DeclineJoin(ctx, user))
}
+99
View File
@@ -0,0 +1,99 @@
package peers
import (
"context"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/internal/deeplink"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
type updateWithChats interface {
tg.UpdatesClass
GetChats() []tg.ChatClass
}
var _ = []updateWithChats{
(*tg.Updates)(nil),
(*tg.UpdatesCombined)(nil),
}
// JoinLink joins to private chat using given link or hash.
// Input examples:
//
// t.me/+AAAAAAAAAAAAAAAA
// https://t.me/+AAAAAAAAAAAAAAAA
// t.me/joinchat/AAAAAAAAAAAAAAAA
// https://t.me/joinchat/AAAAAAAAAAAAAAAA
// tg:join?invite=AAAAAAAAAAAAAAAA
// tg://join?invite=AAAAAAAAAAAAAAAA
func (m *Manager) JoinLink(ctx context.Context, link string) (Peer, error) {
l, err := deeplink.Expect(link, deeplink.Join)
if err != nil {
return nil, err
}
return m.ImportInvite(ctx, l.Args.Get("invite"))
}
// ImportInvite imports given hash invite.
func (m *Manager) ImportInvite(ctx context.Context, hash string) (Peer, error) {
inviteInfo, err := m.api.MessagesCheckChatInvite(ctx, hash)
if err != nil {
return Chat{}, errors.Wrap(err, "check invite")
}
switch inviteInfo := inviteInfo.(type) {
case *tg.ChatInviteAlready:
return m.extractChat(inviteInfo.GetChat())
case *tg.ChatInvite:
if err := m.applyUsers(ctx, inviteInfo.Participants...); err != nil {
return nil, errors.Wrap(err, "update users")
}
case *tg.ChatInvitePeek:
if err := m.applyChats(ctx, inviteInfo.GetChat()); err != nil {
return Chat{}, errors.Wrap(err, "update chats")
}
default:
return nil, errors.Errorf("unexpected type %T", inviteInfo)
}
ch, err := m.importInvite(ctx, hash)
if err != nil {
return nil, err
}
return m.extractChat(ch)
}
func (m *Manager) importInvite(ctx context.Context, hash string) (tg.ChatClass, error) {
u, err := m.api.MessagesImportChatInvite(ctx, hash)
if err != nil {
return nil, errors.Wrap(err, "import invite")
}
updates, ok := u.(updateWithChats)
if !ok {
return nil, errors.Errorf("bad result %T type", u)
}
// Do not apply it, update hook already did it.
chats := updates.GetChats()
if len(chats) < 1 {
return nil, errors.New("empty result")
}
return chats[0], nil
}
func (m *Manager) extractChat(p tg.ChatClass) (Peer, error) {
// TODO: handle forbidden.
switch p := p.(type) {
case *tg.Chat:
return m.Chat(p), nil
case *tg.Channel:
return m.Channel(p), nil
default:
return nil, errors.Errorf("unexpected type %T", p)
}
}
+83
View File
@@ -0,0 +1,83 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgmock"
)
func TestManager_ImportInvite(t *testing.T) {
ctx := context.Background()
hash := "aboba"
expectCheck := func(m *tgmock.Mock) *tgmock.RequestBuilder {
return m.ExpectCall(&tg.MessagesCheckChatInviteRequest{
Hash: hash,
})
}
expectResult := func(m *tgmock.Mock, class tg.ChatInviteClass) *tgmock.Mock {
return expectCheck(m).ThenResult(class)
}
t.Run("CheckError", func(t *testing.T) {
a := require.New(t)
mock, m := testManager(t)
expectCheck(mock).ThenRPCErr(getTestError())
_, err := m.ImportInvite(ctx, hash)
a.Error(err)
})
t.Run("ChatInviteAlready", func(t *testing.T) {
a := require.New(t)
mock, m := testManager(t)
testChat := getTestChannel()
expectResult(mock, &tg.ChatInviteAlready{
Chat: testChat,
})
r, err := m.ImportInvite(ctx, hash)
a.NoError(err)
a.Equal(testChat.ID, r.ID())
})
testImport := func(testChat *tg.Chat, invite tg.ChatInviteClass) func(t *testing.T) {
return func(t *testing.T) {
a := require.New(t)
mock, m := testManager(t)
expectResult(mock, invite).ExpectCall(&tg.MessagesImportChatInviteRequest{
Hash: hash,
}).ThenRPCErr(getTestError())
_, err := m.ImportInvite(ctx, hash)
a.Error(err)
expectResult(mock, invite).ExpectCall(&tg.MessagesImportChatInviteRequest{
Hash: hash,
}).ThenResult(&tg.Updates{
Chats: []tg.ChatClass{testChat},
})
r, err := m.ImportInvite(ctx, hash)
a.NoError(err)
a.Equal(testChat.ID, r.ID())
}
}
testChat := getTestChat()
t.Run("ChatInvite", testImport(testChat, &tg.ChatInvite{
Channel: false,
Broadcast: false,
Public: false,
Megagroup: false,
RequestNeeded: false,
Title: testChat.Title,
About: "",
Photo: &tg.PhotoEmpty{},
ParticipantsCount: testChat.ParticipantsCount,
}))
t.Run("ChatInvitePeek", testImport(testChat, &tg.ChatInvitePeek{
Chat: testChat,
}))
}
+46
View File
@@ -0,0 +1,46 @@
package peers
import (
"context"
"sync"
"github.com/go-faster/errors"
"go.uber.org/zap"
"golang.org/x/sync/singleflight"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// Manager is peer manager.
//
// NB: this package is completely experimental and still WIP.
type Manager struct {
api *tg.Client
storage Storage
cache Cache
// State of current user.
me *atomicUser
// needUpdate stores peers what need to be updated.
needUpdate peerIDSet
needUpdateFull peerIDSet
needUpdateMux sync.Mutex // guards needUpdate, needUpdateFull
logger *zap.Logger
sg singleflight.Group
}
// Init initializes Manager.
func (m *Manager) Init(ctx context.Context) error {
_, err := m.Self(ctx)
if err != nil {
return errors.Wrap(err, "get self")
}
return nil
}
// API returns used Client.
func (m *Manager) API() *tg.Client {
return m.api
}
@@ -0,0 +1,61 @@
package peers_test
import (
"context"
"github.com/go-faster/errors"
"go.uber.org/zap"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/peers"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/updates"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func ExampleManager() {
logger := zap.NewExample()
var (
dispatcher = tg.NewUpdateDispatcher()
h telegram.UpdateHandler
)
client, err := telegram.ClientFromEnvironment(telegram.Options{
Logger: logger.Named("client"),
UpdateHandler: telegram.UpdateHandlerFunc(func(ctx context.Context, u tg.UpdatesClass) error {
return h.Handle(ctx, u)
}),
})
if err != nil {
panic(err)
}
peerManager := peers.Options{
Logger: logger,
}.Build(client.API())
gaps := updates.New(updates.Config{
Handler: dispatcher,
AccessHasher: peerManager,
Logger: logger.Named("gaps"),
})
h = peerManager.UpdateHook(gaps)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if err := client.Run(ctx, func(ctx context.Context) error {
if err := peerManager.Init(ctx); err != nil {
return err
}
u, err := peerManager.Self(ctx)
if err != nil {
return err
}
_, isBot := u.ToBot()
if err := gaps.Run(ctx, client.API(), u.ID(), updates.AuthOptions{IsBot: isBot}); err != nil {
return errors.Wrap(err, "gaps")
}
return nil
}); err != nil {
panic(err)
}
}
+134
View File
@@ -0,0 +1,134 @@
package peers
import (
"testing"
"time"
"go.uber.org/zap/zaptest"
"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, *Manager) {
mock := tgmock.New(t)
return mock, Options{
Logger: zaptest.NewLogger(t),
Cache: &InmemoryCache{},
}.Build(tg.NewClient(mock))
}
func getTestSelf() *tg.User {
u := &tg.User{
Self: true,
Bot: true,
ID: 10,
AccessHash: 10,
FirstName: "Lana",
LastName: "Rhoades",
Username: "thebot",
}
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 getTestUserFull() tg.UserFull {
u := tg.UserFull{
PhoneCallsAvailable: true,
ID: 11,
About: "hot mommy",
}
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() *tg.ChatFull {
u := &tg.ChatFull{
CanSetUsername: false,
HasScheduled: true,
ID: 10,
About: "garfield blog",
Participants: &tg.ChatParticipants{
ChatID: 10,
Participants: []tg.ChatParticipantClass{
&tg.ChatParticipant{
UserID: 10,
InviterID: 10,
Date: 10,
},
},
Version: 1,
},
}
u.SetFlags()
return u
}
func getTestChannel() *tg.Channel {
u := &tg.Channel{
Broadcast: true,
Noforwards: true,
ID: 11,
AccessHash: 0,
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,
}
u.SetFlags()
return u
}
func getTestChannelFull() *tg.ChannelFull {
u := &tg.ChannelFull{
HasScheduled: true,
ID: 11,
About: "garfield blog",
ParticipantsCount: 1,
ChatPhoto: &tg.PhotoEmpty{},
}
u.SetFlags()
return u
}
func getTestError() *tgerr.Error {
return &tgerr.Error{
Code: 1337,
Message: "TEST_ERROR",
Type: "TEST_ERROR",
}
}
+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]]
}
+249
View File
@@ -0,0 +1,249 @@
package peers
import (
"context"
"sync"
"go.uber.org/atomic"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// InmemoryStorage is basic in-memory Storage implementation.
type InmemoryStorage struct {
phones map[string]Key
data map[Key]Value
dataMux sync.Mutex // guards phones and data
contactsHash atomic.Int64
}
var _ Storage = (*InmemoryStorage)(nil)
func (f *InmemoryStorage) initLocked() {
if f.phones == nil {
f.phones = map[string]Key{}
}
if f.data == nil {
f.data = map[Key]Value{}
}
}
// Save implements Storage.
func (f *InmemoryStorage) Save(ctx context.Context, key Key, value Value) error {
f.dataMux.Lock()
defer f.dataMux.Unlock()
f.initLocked()
f.data[key] = value
return nil
}
// Find implements Storage.
func (f *InmemoryStorage) Find(ctx context.Context, key Key) (value Value, found bool, _ error) {
f.dataMux.Lock()
defer f.dataMux.Unlock()
value, found = f.data[key]
return value, found, nil
}
// SavePhone implements Storage.
func (f *InmemoryStorage) SavePhone(ctx context.Context, phone string, key Key) error {
f.dataMux.Lock()
defer f.dataMux.Unlock()
f.initLocked()
f.phones[phone] = key
return nil
}
// FindPhone implements Storage.
func (f *InmemoryStorage) FindPhone(ctx context.Context, phone string) (key Key, value Value, found bool, err error) {
f.dataMux.Lock()
defer f.dataMux.Unlock()
key, found = f.phones[phone]
if !found {
return Key{}, Value{}, false, nil
}
value, found = f.data[key]
return key, value, found, nil
}
// GetContactsHash implements Storage.
func (f *InmemoryStorage) GetContactsHash(ctx context.Context) (int64, error) {
v := f.contactsHash.Load()
return v, nil
}
// SaveContactsHash implements Storage.
func (f *InmemoryStorage) SaveContactsHash(ctx context.Context, hash int64) error {
f.contactsHash.Store(hash)
return nil
}
// InmemoryCache is basic in-memory Cache implementation.
type InmemoryCache struct {
users map[int64]*tg.User
usersMux sync.Mutex
usersFull map[int64]*tg.UserFull
usersFullMux sync.Mutex
chats map[int64]*tg.Chat
chatsMux sync.Mutex
chatsFull map[int64]*tg.ChatFull
chatsFullMux sync.Mutex
channels map[int64]*tg.Channel
channelsMux sync.Mutex
channelsFull map[int64]*tg.ChannelFull
channelsFullMux sync.Mutex
}
// SaveUsers implements Cache.
func (f *InmemoryCache) SaveUsers(ctx context.Context, users ...*tg.User) error {
f.usersMux.Lock()
defer f.usersMux.Unlock()
if f.channelsFull == nil {
f.users = map[int64]*tg.User{}
}
for _, u := range users {
f.users[u.GetID()] = u
}
return nil
}
// SaveUserFulls implements Cache.
func (f *InmemoryCache) SaveUserFulls(ctx context.Context, users ...*tg.UserFull) error {
f.usersFullMux.Lock()
defer f.usersFullMux.Unlock()
if f.channelsFull == nil {
f.usersFull = map[int64]*tg.UserFull{}
}
for _, u := range users {
f.usersFull[u.GetID()] = u
}
return nil
}
// FindUser implements Cache.
func (f *InmemoryCache) FindUser(ctx context.Context, id int64) (*tg.User, bool, error) {
f.usersMux.Lock()
defer f.usersMux.Unlock()
u, ok := f.users[id]
return u, ok, nil
}
// FindUserFull implements Cache.
func (f *InmemoryCache) FindUserFull(ctx context.Context, id int64) (*tg.UserFull, bool, error) {
f.usersFullMux.Lock()
defer f.usersFullMux.Unlock()
u, ok := f.usersFull[id]
return u, ok, nil
}
// SaveChats implements Cache.
func (f *InmemoryCache) SaveChats(ctx context.Context, chats ...*tg.Chat) error {
f.chatsMux.Lock()
defer f.chatsMux.Unlock()
if f.channelsFull == nil {
f.chats = map[int64]*tg.Chat{}
}
for _, c := range chats {
f.chats[c.GetID()] = c
}
return nil
}
// SaveChatFulls implements Cache.
func (f *InmemoryCache) SaveChatFulls(ctx context.Context, chats ...*tg.ChatFull) error {
f.chatsFullMux.Lock()
defer f.chatsFullMux.Unlock()
if f.channelsFull == nil {
f.chatsFull = map[int64]*tg.ChatFull{}
}
for _, c := range chats {
f.chatsFull[c.GetID()] = c
}
return nil
}
// FindChat implements Cache.
func (f *InmemoryCache) FindChat(ctx context.Context, id int64) (*tg.Chat, bool, error) {
f.chatsMux.Lock()
defer f.chatsMux.Unlock()
c, ok := f.chats[id]
return c, ok, nil
}
// FindChatFull implements Cache.
func (f *InmemoryCache) FindChatFull(ctx context.Context, id int64) (*tg.ChatFull, bool, error) {
f.chatsFullMux.Lock()
defer f.chatsFullMux.Unlock()
c, ok := f.chatsFull[id]
return c, ok, nil
}
// SaveChannels implements Cache.
func (f *InmemoryCache) SaveChannels(ctx context.Context, channels ...*tg.Channel) error {
f.channelsMux.Lock()
defer f.channelsMux.Unlock()
if f.channelsFull == nil {
f.channels = map[int64]*tg.Channel{}
}
for _, c := range channels {
f.channels[c.GetID()] = c
}
return nil
}
// SaveChannelFulls implements Cache.
func (f *InmemoryCache) SaveChannelFulls(ctx context.Context, channels ...*tg.ChannelFull) error {
f.channelsFullMux.Lock()
defer f.channelsFullMux.Unlock()
if f.channelsFull == nil {
f.channelsFull = map[int64]*tg.ChannelFull{}
}
for _, c := range channels {
f.channelsFull[c.GetID()] = c
}
return nil
}
// FindChannel implements Cache.
func (f *InmemoryCache) FindChannel(ctx context.Context, id int64) (*tg.Channel, bool, error) {
f.channelsMux.Lock()
defer f.channelsMux.Unlock()
c, ok := f.channels[id]
return c, ok, nil
}
// FindChannelFull implements Cache.
func (f *InmemoryCache) FindChannelFull(ctx context.Context, id int64) (*tg.ChannelFull, bool, error) {
f.channelsFullMux.Lock()
defer f.channelsFullMux.Unlock()
c, ok := f.channelsFull[id]
return c, ok, nil
}
@@ -0,0 +1,149 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestMemoryStorage(t *testing.T) {
ctx := context.Background()
a := require.New(t)
k := Key{
Prefix: usersPrefix,
ID: 1,
}
v := Value{
AccessHash: 10,
}
phone := "phone"
var m InmemoryStorage
_, found, err := m.Find(ctx, k)
a.NoError(err)
a.False(found)
a.NoError(m.Save(ctx, k, v))
v2, found, err := m.Find(ctx, k)
a.NoError(err)
a.True(found)
a.Equal(v, v2)
_, _, found, err = m.FindPhone(ctx, phone)
a.NoError(err)
a.False(found)
a.NoError(m.SavePhone(ctx, phone, k))
k2, v2, found, err := m.FindPhone(ctx, phone)
a.NoError(err)
a.True(found)
a.Equal(k, k2)
a.Equal(v, v2)
hash, err := m.GetContactsHash(ctx)
a.NoError(err)
a.Zero(hash)
a.NoError(m.SaveContactsHash(ctx, 1))
hash, err = m.GetContactsHash(ctx)
a.NoError(err)
a.Equal(int64(1), hash)
}
func TestInmemoryCache(t *testing.T) {
ctx := context.Background()
a := require.New(t)
var m InmemoryCache
{
value := &tg.User{ID: 10}
_, found, err := m.FindUser(ctx, value.GetID())
a.NoError(err)
a.False(found)
a.NoError(m.SaveUsers(ctx, value))
r, found, err := m.FindUser(ctx, value.GetID())
a.NoError(err)
a.True(found)
a.Equal(value, r)
}
{
value := &tg.UserFull{ID: 10}
_, found, err := m.FindUserFull(ctx, value.GetID())
a.NoError(err)
a.False(found)
a.NoError(m.SaveUserFulls(ctx, value))
r, found, err := m.FindUserFull(ctx, value.GetID())
a.NoError(err)
a.True(found)
a.Equal(value, r)
}
{
value := &tg.Chat{ID: 10}
_, found, err := m.FindChat(ctx, value.GetID())
a.NoError(err)
a.False(found)
a.NoError(m.SaveChats(ctx, value))
r, found, err := m.FindChat(ctx, value.GetID())
a.NoError(err)
a.True(found)
a.Equal(value, r)
}
{
value := &tg.ChatFull{ID: 10}
_, found, err := m.FindChatFull(ctx, value.GetID())
a.NoError(err)
a.False(found)
a.NoError(m.SaveChatFulls(ctx, value))
r, found, err := m.FindChatFull(ctx, value.GetID())
a.NoError(err)
a.True(found)
a.Equal(value, r)
}
{
value := &tg.Channel{ID: 10}
_, found, err := m.FindChannel(ctx, value.GetID())
a.NoError(err)
a.False(found)
a.NoError(m.SaveChannels(ctx, value))
r, found, err := m.FindChannel(ctx, value.GetID())
a.NoError(err)
a.True(found)
a.Equal(value, r)
}
{
value := &tg.ChannelFull{ID: 10}
_, found, err := m.FindChannelFull(ctx, value.GetID())
a.NoError(err)
a.False(found)
a.NoError(m.SaveChannelFulls(ctx, value))
r, found, err := m.FindChannelFull(ctx, value.GetID())
a.NoError(err)
a.True(found)
a.Equal(value, r)
}
}
+32
View File
@@ -0,0 +1,32 @@
package peers
import (
"context"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/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 tg.ChatReactionsClass) error {
if _, err := m.api.MessagesSetChatAvailableReactions(ctx, &tg.MessagesSetChatAvailableReactionsRequest{
Peer: p,
AvailableReactions: reactions,
}); err != nil {
return errors.Wrap(err, "set reactions")
}
return nil
}
+76
View File
@@ -0,0 +1,76 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgmock"
)
type multiChat interface {
Peer
Creator() bool
Left() bool
NoForwards() bool
CallActive() bool
CallNotEmpty() bool
ParticipantsCount() int
AdminRights() (tg.ChatAdminRights, bool)
DefaultBannedRights() (tg.ChatBannedRights, bool)
Leave(ctx context.Context) error
SetTitle(ctx context.Context, title string) error
SetDescription(ctx context.Context, about string) error
InviteLinks() InviteLinks
ToBroadcast() (Broadcast, bool)
IsBroadcast() bool
ToSupergroup() (Supergroup, bool)
IsSupergroup() bool
SetReactions(ctx context.Context, r ...tg.ReactionClass) error
DisableReactions(ctx context.Context) error
}
var _ = []multiChat{
Chat{},
Channel{},
}
func TestReactions(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
req := func(p Peer, r ...tg.ReactionClass) *tgmock.RequestBuilder {
var reactions tg.ChatReactionsClass = &tg.ChatReactionsSome{Reactions: r}
if len(r) == 0 {
reactions = &tg.ChatReactionsNone{}
}
return mock.ExpectCall(&tg.MessagesSetChatAvailableReactionsRequest{
Peer: p.InputPeer(),
AvailableReactions: reactions,
})
}
reactions := []tg.ReactionClass{
&tg.ReactionEmoji{Emoticon: "👍"},
&tg.ReactionEmoji{Emoticon: "A"},
}
for _, p := range []multiChat{
m.Chat(getTestChat()),
m.Channel(getTestChannel()),
} {
req(p, reactions...).ThenRPCErr(getTestError())
a.Error(p.SetReactions(ctx, reactions...))
req(p, reactions...).ThenResult(&tg.Updates{})
a.NoError(p.SetReactions(ctx, reactions...))
req(p).ThenRPCErr(getTestError())
a.Error(p.DisableReactions(ctx))
req(p).ThenResult(&tg.Updates{})
a.NoError(p.DisableReactions(ctx))
}
}
+40
View File
@@ -0,0 +1,40 @@
package peers
import (
"go.uber.org/zap"
"golang.org/x/sync/singleflight"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// Options is options of Manager
type Options struct {
Storage Storage
Cache Cache
Logger *zap.Logger
}
func (o *Options) setDefaults() {
if o.Storage == nil {
o.Storage = &InmemoryStorage{}
}
if o.Cache == nil {
o.Cache = NoopCache{}
}
if o.Logger == nil {
o.Logger = zap.NewNop()
}
}
// Build creates new Manager.
func (o Options) Build(api *tg.Client) *Manager {
o.setDefaults()
return &Manager{
api: api,
storage: o.Storage,
cache: o.Cache,
me: new(atomicUser),
logger: o.Logger,
sg: singleflight.Group{},
}
}
+26
View File
@@ -0,0 +1,26 @@
package peers
import (
"go.mau.fi/mautrix-telegram/pkg/gotd/constant"
)
type peerIDSet struct {
m map[constant.TDLibPeerID]struct{}
}
func (p *peerIDSet) add(ids ...constant.TDLibPeerID) {
for _, id := range ids {
p.m[id] = struct{}{}
}
}
func (p *peerIDSet) delete(ids ...constant.TDLibPeerID) {
for _, id := range ids {
delete(p.m, id)
}
}
func (p *peerIDSet) has(id constant.TDLibPeerID) bool {
_, ok := p.m[id]
return ok
}
+104
View File
@@ -0,0 +1,104 @@
// Package peers contains helpers to work with Telegram peers.
//
// NB: this package is completely experimental and still WIP.
// API and behavior may be changed dramatically, so use it with caution.
package peers
import (
"context"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/constant"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// Peer represents generic peer.
type Peer interface {
// ID returns entity ID.
ID() int64
// TDLibPeerID returns TDLibPeerID for this entity.
TDLibPeerID() constant.TDLibPeerID
// VisibleName returns visible name of peer.
//
// It returns FirstName + " " + LastName for users, and title for chats and channels.
VisibleName() string
// Username returns peer username, if any.
Username() (string, bool)
// Restricted whether this user/chat/channel is restricted.
Restricted() ([]tg.RestrictionReason, bool)
// Verified whether this user/chat/channel is verified by Telegram.
Verified() bool
// Scam whether this user/chat/channel is probably a scam.
Scam() bool
// Fake whether this user/chat/channel was reported by many users as a fake or scam: be
// careful when interacting with it.
Fake() bool
// InputPeer returns input peer for this peer.
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
// Photo returns peer photo, if any.
Photo(ctx context.Context) (*tg.Photo, bool, error)
}
var _ = []Peer{
User{},
Chat{},
Channel{},
}
// 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:
return m.ResolveUserID(ctx, p.UserID)
case *tg.PeerChat:
return m.ResolveChatID(ctx, p.ChatID)
case *tg.PeerChannel:
return m.ResolveChannelID(ctx, p.ChannelID)
default:
return nil, errors.Errorf("unexpected type %T", p)
}
}
// FromInputPeer gets Peer from tg.InputPeerClass.
func (m *Manager) FromInputPeer(ctx context.Context, p tg.InputPeerClass) (Peer, error) {
switch p := p.(type) {
case *tg.InputPeerSelf:
return m.Self(ctx)
case *tg.InputPeerChat:
return m.GetChat(ctx, p.ChatID)
case *tg.InputPeerUser:
return m.GetUser(ctx, &tg.InputUser{
UserID: p.UserID,
AccessHash: p.AccessHash,
})
case *tg.InputPeerChannel:
return m.GetChannel(ctx, &tg.InputChannel{
ChannelID: p.ChannelID,
AccessHash: p.AccessHash,
})
case *tg.InputPeerUserFromMessage:
return m.GetUser(ctx, &tg.InputUserFromMessage{
Peer: p.Peer,
MsgID: p.MsgID,
UserID: p.UserID,
})
case *tg.InputPeerChannelFromMessage:
return m.GetChannel(ctx, &tg.InputChannelFromMessage{
Peer: p.Peer,
MsgID: p.MsgID,
ChannelID: p.ChannelID,
})
default:
return nil, errors.Errorf("unexpected type %T", p)
}
}
+140
View File
@@ -0,0 +1,140 @@
package peers
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestReport(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
reason := &tg.InputReportReasonSpam{}
message := "message"
peers := []Peer{
m.User(getTestUser()),
m.Chat(getTestChat()),
m.Channel(getTestChannel()),
}
for _, p := range peers {
req := &tg.AccountReportPeerRequest{
Peer: p.InputPeer(),
Reason: reason,
Message: message,
}
mock.ExpectCall(req).ThenRPCErr(getTestError())
a.Error(p.Report(ctx, reason, message))
mock.ExpectCall(req).ThenTrue()
a.NoError(p.Report(ctx, reason, message))
}
}
func TestManager_FromInputPeer(t *testing.T) {
testUser := getTestUser()
testChat := getTestChat()
testChannel := getTestChannel()
getUser := func(input tg.InputUserClass) *tg.UsersGetUsersRequest {
return &tg.UsersGetUsersRequest{
ID: []tg.InputUserClass{input},
}
}
getChannel := func(input tg.InputChannelClass) *tg.ChannelsGetChannelsRequest {
return &tg.ChannelsGetChannelsRequest{
ID: []tg.InputChannelClass{input},
}
}
var tests = []struct {
input tg.InputPeerClass
expect bin.Encoder
result bin.Encoder
wantErr bool
}{
{
&tg.InputPeerSelf{},
getUser(&tg.InputUserSelf{}),
&tg.UserClassVector{Elems: []tg.UserClass{getTestSelf()}},
false,
},
{
testUser.AsInputPeer(),
getUser(testUser.AsInput()),
&tg.UserClassVector{Elems: []tg.UserClass{testUser}},
false,
},
{
&tg.InputPeerUserFromMessage{
Peer: getTestChannel().AsInputPeer(),
MsgID: 10,
UserID: testUser.ID,
},
getUser(&tg.InputUserFromMessage{
Peer: getTestChannel().AsInputPeer(),
MsgID: 10,
UserID: testUser.ID,
}),
&tg.UserClassVector{Elems: []tg.UserClass{testUser}},
false,
},
{
testChannel.AsInputPeer(),
getChannel(testChannel.AsInput()),
&tg.MessagesChats{Chats: []tg.ChatClass{testChannel}},
false,
},
{
&tg.InputPeerChannelFromMessage{
Peer: getTestChannel().AsInputPeer(),
MsgID: 10,
ChannelID: testChannel.ID,
},
getChannel(&tg.InputChannelFromMessage{
Peer: getTestChannel().AsInputPeer(),
MsgID: 10,
ChannelID: testChannel.ID,
}),
&tg.MessagesChats{Chats: []tg.ChatClass{testChannel}},
false,
},
{
&tg.InputPeerChat{
ChatID: testChat.ID,
},
&tg.MessagesGetChatsRequest{
ID: []int64{testChat.ID},
},
&tg.MessagesChats{Chats: []tg.ChatClass{testChat}},
false,
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%T", tt.input), func(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
mock.ExpectCall(tt.expect).ThenRPCErr(getTestError())
_, err := m.FromInputPeer(ctx, tt.input)
a.Error(err)
mock.ExpectCall(tt.expect).ThenResult(tt.result)
p, err := m.FromInputPeer(ctx, tt.input)
if tt.wantErr {
a.Error(err)
return
}
a.NoError(err)
a.NotZero(p)
})
}
}
+199
View File
@@ -0,0 +1,199 @@
package peers
import (
"context"
"github.com/go-faster/errors"
"go.uber.org/zap"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func (m *Manager) getIDFromInputUser(p tg.InputUserClass) (int64, bool) {
switch p := p.(type) {
case *tg.InputUserSelf:
return m.myID()
case *tg.InputUser:
return p.UserID, true
case *tg.InputUserFromMessage:
return p.UserID, true
default:
return 0, false
}
}
// getUser gets tg.User using given tg.InputUserClass.
func (m *Manager) getUser(ctx context.Context, p tg.InputUserClass) (*tg.User, error) {
switch p := p.(type) {
case *tg.InputUserSelf:
u, ok := m.me.Load()
if ok && !m.needsUpdate(userPeerID(u.ID)) {
return u, nil
}
default:
userID, ok := m.getIDFromInputUser(p)
if !ok || m.needsUpdate(userPeerID(userID)) {
break
}
if me, ok := m.me.Load(); ok && me.GetID() == userID {
return me, nil
}
u, found, err := m.cache.FindUser(ctx, userID)
if err == nil && found {
u.SetFlags()
return u, nil
}
if err != nil {
m.logger.Warn("Find user error", zap.Int64("user_id", userID), zap.Error(err))
}
}
return m.updateUser(ctx, p)
}
// updateUser forcibly updates tg.User using given tg.InputUserClass.
func (m *Manager) updateUser(ctx context.Context, p tg.InputUserClass) (*tg.User, error) {
// TODO(tdakkota): batch requests.
users, err := m.api.UsersGetUsers(ctx, []tg.InputUserClass{p})
if err != nil {
return nil, errors.Wrap(err, "get users")
}
if len(users) < 1 {
return nil, errors.Errorf("got empty result for %+v", p)
}
if err := m.applyUsers(ctx, users...); err != nil {
return nil, errors.Wrap(err, "update users")
}
user, ok := users[0].AsNotEmpty()
if !ok {
return nil, errors.New("got empty user")
}
if user.Self {
m.me.Store(user)
}
return user, nil
}
// getChat gets tg.Chat using given id.
func (m *Manager) getChat(ctx context.Context, p int64) (*tg.Chat, error) {
if !m.needsUpdate(chatPeerID(p)) {
c, found, err := m.cache.FindChat(ctx, p)
if err == nil && found {
c.SetFlags()
return c, nil
}
if err != nil {
m.logger.Warn("Find chat error", zap.Int64("chat_id", p), zap.Error(err))
}
}
return m.updateChat(ctx, p)
}
// updateChat forcibly updates tg.Chat using given id.
func (m *Manager) updateChat(ctx context.Context, id int64) (*tg.Chat, error) {
r, err := m.api.MessagesGetChats(ctx, []int64{id})
if err != nil {
return nil, errors.Wrap(err, "get chats")
}
chats := r.GetChats()
if len(chats) < 1 {
return nil, errors.Errorf("got empty result for chat %d", id)
}
if err := m.applyChats(ctx, chats...); err != nil {
return nil, errors.Wrap(err, "update chats")
}
var found tg.ChatClass
for _, chat := range chats {
switch chat := chat.(type) {
case *tg.Chat:
if chat.ID == id {
found = chat
break
}
}
}
ch, ok := found.(*tg.Chat)
if !ok {
// TODO(tdakkota): get better error for forbidden.
return nil, errors.Errorf("got unexpected type %T", found)
}
return ch, nil
}
func getIDFromInputChannel(p tg.InputChannelClass) (int64, bool) {
switch p := p.(type) {
case *tg.InputChannel:
return p.ChannelID, true
case *tg.InputChannelFromMessage:
return p.ChannelID, true
default:
return 0, false
}
}
// getChannel gets tg.Channel using given tg.InputChannelClass.
func (m *Manager) getChannel(ctx context.Context, p tg.InputChannelClass) (*tg.Channel, error) {
if id, ok := getIDFromInputChannel(p); ok && !m.needsUpdate(channelPeerID(id)) {
c, found, err := m.cache.FindChannel(ctx, id)
if err == nil && found {
c.SetFlags()
return c, nil
}
if err != nil {
m.logger.Warn("Find channel error", zap.Int64("channel_id", id), zap.Error(err))
}
}
return m.updateChannel(ctx, p)
}
// updateChannel forcibly updates tg.Channel using given tg.InputChannelClass.
func (m *Manager) updateChannel(ctx context.Context, p tg.InputChannelClass) (*tg.Channel, error) {
r, err := m.api.ChannelsGetChannels(ctx, []tg.InputChannelClass{p})
if err != nil {
return nil, errors.Wrap(err, "get channels")
}
chats := r.GetChats()
if len(chats) < 1 {
return nil, errors.Errorf("got empty result for %+v", p)
}
if err := m.applyChats(ctx, chats...); err != nil {
return nil, errors.Wrap(err, "update chats")
}
var found tg.ChatClass
if inputHasID, ok := p.AsNotEmpty(); ok {
id := inputHasID.GetChannelID()
for _, chat := range chats {
switch chat := chat.(type) {
case *tg.Channel:
if chat.ID == id {
found = chat
break
}
}
}
} else {
found = chats[0]
}
ch, ok := found.(*tg.Channel)
if !ok {
// TODO(tdakkota): get better error for forbidden.
return nil, errors.Errorf("got unexpected type %T", found)
}
return ch, nil
}
+121
View File
@@ -0,0 +1,121 @@
package peers
import (
"context"
"github.com/go-faster/errors"
"go.uber.org/zap"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// getUserFull gets tg.UserFull using given tg.InputUserClass.
func (m *Manager) getUserFull(ctx context.Context, p tg.InputUserClass) (*tg.UserFull, error) {
if userID, ok := m.getIDFromInputUser(p); ok && !m.needsUpdateFull(userPeerID(userID)) {
// TODO(tdakkota): save full self.
u, found, err := m.cache.FindUserFull(ctx, userID)
if err == nil && found {
u.SetFlags()
return u, nil
}
if err != nil {
m.logger.Warn("Find full user error", zap.Int64("user_id", userID), zap.Error(err))
}
}
return m.updateUserFull(ctx, p)
}
// updateUserFull forcibly updates tg.UserFull using given tg.InputUserClass.
func (m *Manager) updateUserFull(ctx context.Context, p tg.InputUserClass) (*tg.UserFull, error) {
r, err := m.api.UsersGetFullUser(ctx, p)
if err != nil {
return nil, errors.Wrap(err, "get full user")
}
if err := m.applyEntities(ctx, r.GetUsers(), r.GetChats()); err != nil {
return nil, err
}
if err := m.applyFullUser(ctx, &r.FullUser); err != nil {
return nil, errors.Wrap(err, "update full user")
}
cp := r.FullUser
return &cp, nil
}
// getChatFull gets tg.ChatFull using given id.
func (m *Manager) getChatFull(ctx context.Context, p int64) (*tg.ChatFull, error) {
if !m.needsUpdateFull(chatPeerID(p)) {
c, found, err := m.cache.FindChatFull(ctx, p)
if err == nil && found {
c.SetFlags()
return c, nil
}
if err != nil {
m.logger.Warn("Find full chat error", zap.Int64("chat_id", p), zap.Error(err))
}
}
return m.updateChatFull(ctx, p)
}
// updateChatFull forcibly updates tg.ChatFull using given id.
func (m *Manager) updateChatFull(ctx context.Context, id int64) (*tg.ChatFull, error) {
r, err := m.api.MessagesGetFullChat(ctx, id)
if err != nil {
return nil, errors.Wrap(err, "get full chat")
}
if err := m.applyEntities(ctx, r.GetUsers(), r.GetChats()); err != nil {
return nil, err
}
ch, ok := r.FullChat.(*tg.ChatFull)
if !ok {
return nil, errors.Errorf("got unexpected type %T", r.FullChat)
}
if err := m.applyFullChat(ctx, ch); err != nil {
return nil, errors.Wrap(err, "update full chat")
}
return ch, nil
}
// getChannelFull gets tg.ChannelFull using given tg.InputChannelClass.
func (m *Manager) getChannelFull(ctx context.Context, p tg.InputChannelClass) (*tg.ChannelFull, error) {
if id, ok := getIDFromInputChannel(p); ok && !m.needsUpdateFull(channelPeerID(id)) {
c, found, err := m.cache.FindChannelFull(ctx, id)
if err == nil && found {
c.SetFlags()
return c, nil
}
if err != nil {
m.logger.Warn("Find channel error", zap.Int64("channel_id", id), zap.Error(err))
}
}
return m.updateChannelFull(ctx, p)
}
// updateChannelFull forcibly updates tg.ChannelFull using given tg.InputChannelClass.
func (m *Manager) updateChannelFull(ctx context.Context, p tg.InputChannelClass) (*tg.ChannelFull, error) {
r, err := m.api.ChannelsGetFullChannel(ctx, p)
if err != nil {
return nil, errors.Wrap(err, "get full channel")
}
if err := m.applyEntities(ctx, r.GetUsers(), r.GetChats()); err != nil {
return nil, err
}
ch, ok := r.FullChat.(*tg.ChannelFull)
if !ok {
return nil, errors.Errorf("got unexpected type %T", r.FullChat)
}
if err := m.applyFullChannel(ctx, ch); err != nil {
return nil, errors.Wrap(err, "update full channel")
}
return ch, nil
}
+123
View File
@@ -0,0 +1,123 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestManager_getUserFull(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
testUserFull := getTestUserFull()
input := &tg.InputUser{
UserID: testUserFull.ID,
AccessHash: 10,
}
mock.ExpectCall(&tg.UsersGetFullUserRequest{ID: input}).ThenResult(&tg.UsersUserFull{
FullUser: testUserFull,
})
v, err := m.getUserFull(ctx, input)
a.NoError(err)
a.Equal(&testUserFull, v)
// Test caching.
v, err = m.getUserFull(ctx, input)
a.NoError(err)
a.Equal(&testUserFull, v)
}
func TestManager_updateUserFull(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
testUserFull := getTestUserFull()
input := &tg.InputUser{
UserID: testUserFull.ID,
AccessHash: 10,
}
mock.ExpectCall(&tg.UsersGetFullUserRequest{ID: input}).ThenRPCErr(getTestError())
_, err := m.updateUserFull(ctx, input)
a.Error(err)
}
func TestManager_getChatFull(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
testChatFull := getTestChatFull()
input := testChatFull.ID
mock.ExpectCall(&tg.MessagesGetFullChatRequest{ChatID: input}).ThenResult(&tg.MessagesChatFull{
FullChat: testChatFull,
})
v, err := m.getChatFull(ctx, input)
a.NoError(err)
a.Equal(testChatFull, v)
// Test caching.
v, err = m.getChatFull(ctx, input)
a.NoError(err)
a.Equal(testChatFull, v)
}
func TestManager_updateChatFull(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
testChatFull := getTestChatFull()
input := testChatFull.ID
mock.ExpectCall(&tg.MessagesGetFullChatRequest{ChatID: input}).ThenRPCErr(getTestError())
_, err := m.updateChatFull(ctx, input)
a.Error(err)
}
func TestManager_getChannelFull(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
testChannelFull := getTestChannelFull()
input := &tg.InputChannel{
ChannelID: testChannelFull.ID,
AccessHash: 10,
}
mock.ExpectCall(&tg.ChannelsGetFullChannelRequest{Channel: input}).ThenResult(&tg.MessagesChatFull{
FullChat: testChannelFull,
})
v, err := m.getChannelFull(ctx, input)
a.NoError(err)
a.Equal(testChannelFull, v)
// Test caching.
v, err = m.getChannelFull(ctx, input)
a.NoError(err)
a.Equal(testChannelFull, v)
}
func TestManager_updateChannelFull(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
testChannelFull := getTestChannelFull()
input := &tg.InputChannel{
ChannelID: testChannelFull.ID,
AccessHash: 10,
}
mock.ExpectCall(&tg.ChannelsGetFullChannelRequest{Channel: input}).ThenRPCErr(getTestError())
_, err := m.updateChannelFull(ctx, input)
a.Error(err)
}
+185
View File
@@ -0,0 +1,185 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestManager_getUser(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
testSelf := getTestSelf()
testUser := getTestUser()
mock.ExpectCall(&tg.UsersGetUsersRequest{
ID: []tg.InputUserClass{&tg.InputUserSelf{}},
}).ThenResult(&tg.UserClassVector{Elems: []tg.UserClass{testSelf}})
v, err := m.getUser(ctx, &tg.InputUserSelf{})
a.NoError(err)
a.Equal(testSelf, v)
v, err = m.getUser(ctx, &tg.InputUser{UserID: testSelf.ID, AccessHash: testSelf.AccessHash})
a.NoError(err)
a.Equal(testSelf, v)
v, err = m.getUser(ctx, &tg.InputUserFromMessage{UserID: testSelf.ID})
a.NoError(err)
a.Equal(testSelf, v)
mock.ExpectCall(&tg.UsersGetUsersRequest{
ID: []tg.InputUserClass{&tg.InputUser{
UserID: testUser.ID,
AccessHash: testUser.AccessHash,
}},
}).ThenResult(&tg.UserClassVector{Elems: []tg.UserClass{testUser}})
v, err = m.getUser(ctx, &tg.InputUser{UserID: testUser.ID, AccessHash: testUser.AccessHash})
a.NoError(err)
a.Equal(testUser, v)
v, err = m.getUser(ctx, &tg.InputUser{UserID: testUser.ID})
a.NoError(err)
a.Equal(testUser, v)
v, err = m.getUser(ctx, &tg.InputUserFromMessage{UserID: testUser.ID})
a.NoError(err)
a.Equal(testUser, v)
}
func TestManager_updateUser(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
input := &tg.InputUser{
UserID: 10,
AccessHash: 10,
}
mock.ExpectCall(&tg.UsersGetUsersRequest{
ID: []tg.InputUserClass{input},
}).ThenRPCErr(getTestError())
_, err := m.updateUser(ctx, input)
a.Error(err)
mock.ExpectCall(&tg.UsersGetUsersRequest{
ID: []tg.InputUserClass{input},
}).ThenResult(&tg.UserClassVector{})
_, err = m.updateUser(ctx, input)
a.Error(err)
mock.ExpectCall(&tg.UsersGetUsersRequest{
ID: []tg.InputUserClass{input},
}).ThenResult(&tg.UserClassVector{Elems: []tg.UserClass{&tg.UserEmpty{}}})
_, err = m.updateUser(ctx, input)
a.Error(err)
}
func TestManager_getChat(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
testChat := getTestChat()
mock.ExpectCall(&tg.MessagesGetChatsRequest{
ID: []int64{testChat.ID},
}).ThenResult(&tg.MessagesChats{Chats: []tg.ChatClass{testChat}})
v, err := m.getChat(ctx, testChat.ID)
a.NoError(err)
a.Equal(testChat, v)
v, err = m.getChat(ctx, testChat.ID)
a.NoError(err)
a.Equal(testChat, v)
}
func TestManager_updateChat(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
var input int64 = 10
mock.ExpectCall(&tg.MessagesGetChatsRequest{
ID: []int64{input},
}).ThenRPCErr(getTestError())
_, err := m.updateChat(ctx, input)
a.Error(err)
mock.ExpectCall(&tg.MessagesGetChatsRequest{
ID: []int64{input},
}).ThenResult(&tg.MessagesChats{})
_, err = m.updateChat(ctx, input)
a.Error(err)
mock.ExpectCall(&tg.MessagesGetChatsRequest{
ID: []int64{input},
}).ThenResult(&tg.MessagesChats{Chats: []tg.ChatClass{&tg.ChatEmpty{}}})
_, err = m.updateChat(ctx, input)
a.Error(err)
}
func TestManager_getChannel(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
testChannel := getTestChannel()
input := &tg.InputChannel{
ChannelID: testChannel.ID,
AccessHash: testChannel.AccessHash,
}
mock.ExpectCall(&tg.ChannelsGetChannelsRequest{
ID: []tg.InputChannelClass{input},
}).ThenResult(&tg.MessagesChats{Chats: []tg.ChatClass{testChannel}})
v, err := m.getChannel(ctx, input)
a.NoError(err)
a.Equal(testChannel, v)
v, err = m.getChannel(ctx, input)
a.NoError(err)
a.Equal(testChannel, v)
}
func TestManager_updateChannel(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
input := &tg.InputChannel{
ChannelID: 10,
AccessHash: 10,
}
mock.ExpectCall(&tg.ChannelsGetChannelsRequest{
ID: []tg.InputChannelClass{input},
}).ThenRPCErr(getTestError())
_, err := m.updateChannel(ctx, input)
a.Error(err)
mock.ExpectCall(&tg.ChannelsGetChannelsRequest{
ID: []tg.InputChannelClass{input},
}).ThenResult(&tg.MessagesChats{})
_, err = m.updateChannel(ctx, input)
a.Error(err)
mock.ExpectCall(&tg.ChannelsGetChannelsRequest{
ID: []tg.InputChannelClass{input},
}).ThenResult(&tg.MessagesChats{Chats: []tg.ChatClass{&tg.Chat{}}})
_, err = m.updateChannel(ctx, input)
a.Error(err)
}
+239
View File
@@ -0,0 +1,239 @@
package peers
import (
"context"
"strings"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/ascii"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/internal/deeplink"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// Resolve uses given string to create new peer promise.
//
// Input examples:
//
// @telegram
// telegram
// t.me/telegram
// https://t.me/telegram
// tg:resolve?domain=telegram
// tg://resolve?domain=telegram
// +13115552368
// +1 (311) 555-0123
// +1 311 555-6162
// 13115556162
func (m *Manager) Resolve(ctx context.Context, from string) (Peer, error) {
from = strings.TrimSpace(from)
if deeplink.IsDeeplinkLike(from) {
return m.ResolveDeeplink(ctx, from)
}
if isPhoneNumber(from) {
return m.ResolvePhone(ctx, from)
}
return m.ResolveDomain(ctx, from)
}
func isPhoneNumber(s string) bool {
if s == "" {
return false
}
r := rune(s[0])
return r == '+' || ascii.IsDigit(r)
}
func cleanupPhone(phone string) string {
var needClean bool
for _, ch := range phone {
if !ascii.IsDigit(ch) {
needClean = true
break
}
}
if !needClean {
return phone
}
clean := strings.Builder{}
clean.Grow(len(phone) + 1)
for _, ch := range phone {
if ascii.IsDigit(ch) {
clean.WriteRune(ch)
}
}
return clean.String()
}
// ResolvePhone uses given phone to resolve User.
//
// Input example:
//
// +13115552368
// +1 (311) 555-0123
// +1 311 555-6162
// 13115556162
//
// Note that Telegram represents phone numbers according to the E.164 standard
// without the plus sign (”+”) prefix. The resolver therefore takes an easy
// route and just deletes any non-digit symbols from phone number string.
func (m *Manager) ResolvePhone(ctx context.Context, phone string) (User, error) {
tried := false
phone = cleanupPhone(phone)
for {
if tried {
return User{}, &PhoneNotFoundError{Phone: phone}
}
key, v, found, err := m.storage.FindPhone(ctx, phone)
if err != nil {
return User{}, errors.Wrap(err, "find by phone")
}
if found {
return m.GetUser(ctx, &tg.InputUser{
UserID: key.ID,
AccessHash: v.AccessHash,
})
}
if m.selfIsBot() {
return User{}, &PhoneNotFoundError{Phone: phone}
}
tried = true
users, err := m.updateContacts(ctx)
if err != nil {
return User{}, errors.Wrap(err, "update contacts")
}
for _, user := range users {
if u, ok := user.AsNotEmpty(); ok && u.Phone == phone {
return m.User(u), nil
}
}
}
}
func validateDomain(domain string) error {
return deeplink.ValidateDomain(domain)
}
func (m *Manager) findPeerClass(p tg.PeerClass, users []tg.UserClass, chats []tg.ChatClass) (Peer, bool) {
switch p := p.(type) {
case *tg.PeerUser:
for _, user := range users {
u, ok := user.AsNotEmpty()
if ok && u.ID == p.UserID {
return m.User(u), true
}
}
case *tg.PeerChat:
for _, chat := range chats {
c, ok := chat.(*tg.Chat)
if ok && c.ID == p.ChatID {
return m.Chat(c), true
}
}
case *tg.PeerChannel:
for _, chat := range chats {
c, ok := chat.(*tg.Channel)
if ok && c.ID == p.ChannelID {
return m.Channel(c), true
}
}
}
return nil, false
}
// ResolveDomain uses given domain to create new peer promise.
//
// May be prefixed with @ or not.
//
// Input examples:
//
// @telegram
// telegram
func (m *Manager) ResolveDomain(ctx context.Context, domain string) (Peer, error) {
domain = strings.TrimPrefix(domain, "@")
if err := validateDomain(domain); err != nil {
return nil, errors.Wrap(err, "validate domain")
}
ch := m.sg.DoChan(domain, func() (interface{}, error) {
result, err := m.api.ContactsResolveUsername(ctx, &tg.ContactsResolveUsernameRequest{
Username: domain,
})
if err != nil {
return nil, errors.Wrap(err, "resolve")
}
return result, nil
})
var result *tg.ContactsResolvedPeer
select {
case r := <-ch:
if err := r.Err; err != nil {
return nil, r.Err
}
result = r.Val.(*tg.ContactsResolvedPeer)
case <-ctx.Done():
return nil, ctx.Err()
}
if err := m.applyEntities(ctx, result.Users, result.Chats); err != nil {
return nil, err
}
p, ok := m.findPeerClass(result.Peer, result.Users, result.Chats)
if !ok {
return nil, &PeerNotFoundError{Peer: result.Peer}
}
return p, nil
}
// ResolveDeeplink uses given deeplink to create new peer promise.
//
// Input examples:
//
// t.me/telegram
// https://t.me/telegram
// tg:resolve?domain=telegram
// tg://resolve?domain=telegram
func (m *Manager) ResolveDeeplink(ctx context.Context, u string) (Peer, error) {
link, err := deeplink.Expect(u, deeplink.Resolve)
if err != nil {
return nil, err
}
domain := link.Args.Get("domain")
if err := validateDomain(domain); err != nil {
return nil, errors.Wrap(err, "validate domain")
}
return m.ResolveDomain(ctx, domain)
}
func (m *Manager) ResolveDeeplinkJoin(ctx context.Context, u string) (tg.ChatInviteClass, error) {
link, err := deeplink.Expect(u, deeplink.Join)
if err != nil {
return nil, err
}
domain := link.Args.Get("domain")
if err := validateDomain(domain); err != nil {
return nil, errors.Wrap(err, "validate domain")
}
inviteInfo, err := m.api.MessagesCheckChatInvite(ctx, link.Args.Get("invite"))
if err != nil {
return nil, errors.Wrap(err, "check invite")
}
return inviteInfo, nil
}
+167
View File
@@ -0,0 +1,167 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestManager_findPeerClass(t *testing.T) {
user := getTestSelf()
chat := &tg.Chat{
ID: 10,
}
channel := &tg.Channel{
ID: 10,
}
m := &Manager{}
type args struct {
p tg.PeerClass
users []tg.UserClass
chats []tg.ChatClass
}
tests := []struct {
name string
args args
want Peer
wantOk bool
}{
{
name: "User",
args: args{
p: &tg.PeerUser{UserID: user.GetID()},
users: []tg.UserClass{user},
},
want: User{
raw: user,
m: m,
},
wantOk: true,
},
{
name: "Chat",
args: args{
p: &tg.PeerChat{ChatID: 10},
chats: []tg.ChatClass{chat},
},
want: Chat{
raw: chat,
m: m,
},
wantOk: true,
},
{
name: "Channel",
args: args{
p: &tg.PeerChannel{ChannelID: 10},
chats: []tg.ChatClass{channel},
},
want: Channel{
raw: channel,
m: m,
},
wantOk: true,
},
{name: "NilPeer"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := require.New(t)
r, ok := m.findPeerClass(tt.args.p, tt.args.users, tt.args.chats)
if tt.wantOk {
a.Equal(tt.want, r)
a.True(ok)
} else {
a.False(ok)
}
})
}
}
func TestManager_Resolve(t *testing.T) {
testUser := getTestSelf()
inputs := []struct {
Name string
Input string
}{
{"Domain", "@gotduser"},
{"Deeplink", "https://t.me/gotduser"},
}
for _, tt := range inputs {
t.Run(tt.Name, func(t *testing.T) {
a := require.New(t)
mock, m := testManager(t)
username := "gotduser"
mock.ExpectCall(&tg.ContactsResolveUsernameRequest{
Username: username,
}).ThenResult(&tg.ContactsResolvedPeer{
Peer: &tg.PeerUser{UserID: testUser.GetID()},
Users: []tg.UserClass{
&tg.User{ID: testUser.GetID(), AccessHash: 10, Username: username},
},
}).ExpectCall(&tg.ContactsResolveUsernameRequest{
Username: username,
}).ThenRPCErr(getTestError())
ctx := context.Background()
r, err := m.Resolve(ctx, tt.Input)
a.NoError(err)
a.IsType(&tg.InputPeerUser{}, r.InputPeer())
a.Equal(testUser.GetID(), r.ID())
_, err = m.Resolve(ctx, tt.Input)
a.Error(err)
})
}
}
func TestManager_ResolvePhone(t *testing.T) {
a := require.New(t)
mock, m := testManager(t)
// To count contacts hash.
m.me.Store(&tg.User{
ID: 1,
})
phone := "+79001234567"
phone2 := "+79011234567"
mock.ExpectCall(&tg.ContactsGetContactsRequest{
Hash: 0,
}).ThenRPCErr(getTestError())
ctx := context.Background()
_, err := m.Resolve(ctx, phone)
a.Error(err)
resp := &tg.ContactsContacts{
Contacts: []tg.Contact{{
UserID: 10,
Mutual: false,
}},
SavedCount: 1,
Users: []tg.UserClass{
&tg.User{ID: 10, AccessHash: 10, Username: "rustmustdie", Phone: cleanupPhone(phone)},
},
}
mock.ExpectCall(&tg.ContactsGetContactsRequest{
Hash: 0,
}).ThenResult(resp)
r, err := m.Resolve(ctx, phone)
a.NoError(err)
a.IsType(&tg.InputPeerUser{}, r.InputPeer())
a.Equal(int64(10), r.ID())
mock.ExpectCall(&tg.ContactsGetContactsRequest{
Hash: contactsHash(1, resp),
}).ThenResult(&tg.ContactsContactsNotModified{})
_, err = m.Resolve(ctx, phone2)
a.Error(err)
}
+55
View File
@@ -0,0 +1,55 @@
package peers
import (
"context"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// SearchResult is Search query result.
type SearchResult struct {
MyResults []Peer
Results []Peer
}
// Search searches peers by given query.
func (m *Manager) Search(ctx context.Context, q string) (SearchResult, error) {
convert := func(input []tg.PeerClass) ([]Peer, error) {
resolved := make([]Peer, len(input))
for i, p := range input {
r, err := m.ResolvePeer(ctx, p)
if err != nil {
return nil, errors.Wrapf(err, "resolve %d (%+v)", i, p)
}
resolved[i] = r
}
return resolved, nil
}
found, err := m.api.ContactsSearch(ctx, &tg.ContactsSearchRequest{
Q: q,
Limit: 10,
})
if err != nil {
return SearchResult{}, errors.Wrap(err, "search")
}
if err := m.applyEntities(ctx, found.Users, found.Chats); err != nil {
return SearchResult{}, err
}
var r SearchResult
r.MyResults, err = convert(found.MyResults)
if err != nil {
return SearchResult{}, errors.Wrap(err, "my results")
}
r.Results, err = convert(found.Results)
if err != nil {
return SearchResult{}, errors.Wrap(err, "results")
}
return r, nil
}
+45
View File
@@ -0,0 +1,45 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestManager_Search(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
mock.ExpectCall(&tg.ContactsSearchRequest{
Q: "q",
Limit: 10,
}).ThenRPCErr(getTestError())
_, err := m.Search(ctx, "q")
a.Error(err)
mock.ExpectCall(&tg.ContactsSearchRequest{
Q: "q",
Limit: 10,
}).ThenResult(&tg.ContactsFound{
MyResults: []tg.PeerClass{&tg.PeerUser{UserID: getTestUser().GetID()}},
Results: []tg.PeerClass{&tg.PeerChat{ChatID: getTestChat().GetID()}},
Chats: []tg.ChatClass{
getTestChat(),
},
Users: []tg.UserClass{
getTestUser(),
},
})
r, err := m.Search(ctx, "q")
a.NoError(err)
a.Len(r.MyResults, 1)
a.Equal(getTestUser().GetID(), r.MyResults[0].ID())
a.Len(r.Results, 1)
a.Equal(getTestChat().GetID(), r.Results[0].ID())
}
+25
View File
@@ -0,0 +1,25 @@
package peers
import (
"context"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// Self returns current User.
func (m *Manager) Self(ctx context.Context) (User, error) {
return m.GetUser(ctx, &tg.InputUserSelf{})
}
func (m *Manager) selfIsBot() bool {
u, ok := m.me.Load()
return ok && u.Bot
}
func (m *Manager) myID() (int64, bool) {
u, ok := m.me.Load()
if !ok {
return 0, false
}
return u.ID, true
}
+45
View File
@@ -0,0 +1,45 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestManager_Self(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
testUser := getTestSelf()
_, ok := m.myID()
a.False(ok)
a.False(m.selfIsBot())
mock.ExpectCall(&tg.UsersGetUsersRequest{
ID: []tg.InputUserClass{&tg.InputUserSelf{}},
}).ThenRPCErr(getTestError())
u, err := m.Self(ctx)
a.Error(err)
a.Zero(u)
mock.ExpectCall(&tg.UsersGetUsersRequest{
ID: []tg.InputUserClass{&tg.InputUserSelf{}},
}).ThenResult(&tg.UserClassVector{Elems: []tg.UserClass{testUser}})
u, err = m.Self(ctx)
a.NoError(err)
a.Equal(testUser, u.Raw())
// Test caching.
u, err = m.Self(ctx)
a.NoError(err)
a.Equal(testUser, u.Raw())
id, ok := m.myID()
a.True(ok)
a.Equal(testUser.ID, id)
a.True(m.selfIsBot())
}
+119
View File
@@ -0,0 +1,119 @@
package peers
import (
"context"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// Value is storage value.
type Value struct {
AccessHash int64
}
// Key is storage key.
type Key struct {
Prefix string
ID int64
}
// Storage is peer storage.
type Storage interface {
Save(ctx context.Context, key Key, value Value) error
Find(ctx context.Context, key Key) (value Value, found bool, _ error)
SavePhone(ctx context.Context, phone string, key Key) error
FindPhone(ctx context.Context, phone string) (key Key, value Value, found bool, err error)
GetContactsHash(ctx context.Context) (int64, error)
SaveContactsHash(ctx context.Context, hash int64) error
}
const (
usersPrefix = "users_"
chatsPrefix = "chats_"
channelPrefix = "channel_"
)
// Cache is peer entities cache.
type Cache interface {
SaveUsers(ctx context.Context, users ...*tg.User) error
SaveUserFulls(ctx context.Context, users ...*tg.UserFull) error
FindUser(ctx context.Context, id int64) (*tg.User, bool, error)
FindUserFull(ctx context.Context, id int64) (*tg.UserFull, bool, error)
SaveChats(ctx context.Context, chats ...*tg.Chat) error
SaveChatFulls(ctx context.Context, chats ...*tg.ChatFull) error
FindChat(ctx context.Context, id int64) (*tg.Chat, bool, error)
FindChatFull(ctx context.Context, id int64) (*tg.ChatFull, bool, error)
SaveChannels(ctx context.Context, channels ...*tg.Channel) error
SaveChannelFulls(ctx context.Context, channels ...*tg.ChannelFull) error
FindChannel(ctx context.Context, id int64) (*tg.Channel, bool, error)
FindChannelFull(ctx context.Context, id int64) (*tg.ChannelFull, bool, error)
}
// NoopCache is no-op implementation of Cache.
type NoopCache struct{}
var _ Cache = NoopCache{}
// SaveUsers implements Cache.
func (n NoopCache) SaveUsers(ctx context.Context, users ...*tg.User) error {
return nil
}
// SaveUserFulls implements Cache.
func (n NoopCache) SaveUserFulls(ctx context.Context, users ...*tg.UserFull) error {
return nil
}
// FindUser implements Cache.
func (n NoopCache) FindUser(ctx context.Context, id int64) (*tg.User, bool, error) {
return nil, false, nil
}
// FindUserFull implements Cache.
func (n NoopCache) FindUserFull(ctx context.Context, id int64) (*tg.UserFull, bool, error) {
return nil, false, nil
}
// SaveChats implements Cache.
func (n NoopCache) SaveChats(ctx context.Context, chats ...*tg.Chat) error {
return nil
}
// SaveChatFulls implements Cache.
func (n NoopCache) SaveChatFulls(ctx context.Context, chats ...*tg.ChatFull) error {
return nil
}
// FindChat implements Cache.
func (n NoopCache) FindChat(ctx context.Context, id int64) (*tg.Chat, bool, error) {
return nil, false, nil
}
// FindChatFull implements Cache.
func (n NoopCache) FindChatFull(ctx context.Context, id int64) (*tg.ChatFull, bool, error) {
return nil, false, nil
}
// SaveChannels implements Cache.
func (n NoopCache) SaveChannels(ctx context.Context, channels ...*tg.Channel) error {
return nil
}
// SaveChannelFulls implements Cache.
func (n NoopCache) SaveChannelFulls(ctx context.Context, channels ...*tg.ChannelFull) error {
return nil
}
// FindChannel implements Cache.
func (n NoopCache) FindChannel(ctx context.Context, id int64) (*tg.Channel, bool, error) {
return nil, false, nil
}
// FindChannelFull implements Cache.
func (n NoopCache) FindChannelFull(ctx context.Context, id int64) (*tg.ChannelFull, bool, error) {
return nil, false, nil
}
+45
View File
@@ -0,0 +1,45 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestNoopCache(t *testing.T) {
a := require.New(t)
ctx := context.Background()
c := NoopCache{}
_, ok, err := c.FindUser(ctx, 1)
a.NoError(err)
a.False(ok)
_, ok, err = c.FindUserFull(ctx, 1)
a.NoError(err)
a.False(ok)
_, ok, err = c.FindChat(ctx, 1)
a.NoError(err)
a.False(ok)
_, ok, err = c.FindChatFull(ctx, 1)
a.NoError(err)
a.False(ok)
_, ok, err = c.FindChannel(ctx, 1)
a.NoError(err)
a.False(ok)
_, ok, err = c.FindChannelFull(ctx, 1)
a.NoError(err)
a.False(ok)
a.NoError(c.SaveUsers(ctx, nil))
a.NoError(c.SaveUserFulls(ctx, nil))
a.NoError(c.SaveChats(ctx, nil))
a.NoError(c.SaveChatFulls(ctx, nil))
a.NoError(c.SaveChannels(ctx, nil))
a.NoError(c.SaveChannelFulls(ctx, nil))
}
+58
View File
@@ -0,0 +1,58 @@
package peers
import (
"context"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// Supergroup is a supergroup Channel.
type Supergroup struct {
Channel
}
// SlowmodeEnabled whether slow mode is enabled for groups to prevent flood in chat.
func (c Supergroup) SlowmodeEnabled() bool {
return c.raw.GetSlowmodeEnabled()
}
// DisableSlowMode disables slow mode.
func (c Supergroup) DisableSlowMode(ctx context.Context) error {
return c.ToggleSlowMode(ctx, -1)
}
// ToggleSlowMode Toggle supergroup slow mode: if enabled, users will only be able to send one message per seconds.
//
// If seconds is zero or smaller, slow mode will be disabled.
func (c Supergroup) ToggleSlowMode(ctx context.Context, seconds int) error {
if seconds < 0 {
seconds = 0
}
if _, err := c.m.api.ChannelsToggleSlowMode(ctx, &tg.ChannelsToggleSlowModeRequest{
Channel: c.InputChannel(),
Seconds: seconds,
}); err != nil {
return errors.Wrap(err, "toggle slow mode")
}
return nil
}
// SetStickerSet associates a sticker set to this supergroup.
func (c Supergroup) SetStickerSet(ctx context.Context, set tg.InputStickerSetClass) error {
if _, err := c.m.api.ChannelsSetStickers(ctx, &tg.ChannelsSetStickersRequest{
Channel: c.InputChannel(),
Stickerset: set,
}); err != nil {
return errors.Wrap(err, "set stickers")
}
return nil
}
// ResetStickerSet resets associated sticker set of this supergroup.
func (c Supergroup) ResetStickerSet(ctx context.Context) error {
return c.SetStickerSet(ctx, &tg.InputStickerSetEmpty{})
}
@@ -0,0 +1,77 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func getTestSuperGroup() *tg.Channel {
testChannel := getTestChannel()
testChannel.ID *= 2
testChannel.Broadcast = false
testChannel.Megagroup = true
return testChannel
}
func TestSupergroup_ToggleSlowMode(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
ch := m.Channel(getTestSuperGroup())
s, ok := ch.ToSupergroup()
a.True(ok)
mock.ExpectCall(&tg.ChannelsToggleSlowModeRequest{
Channel: s.InputChannel(),
Seconds: 1,
}).ThenRPCErr(getTestError())
a.Error(s.ToggleSlowMode(ctx, 1))
mock.ExpectCall(&tg.ChannelsToggleSlowModeRequest{
Channel: s.InputChannel(),
Seconds: 1,
}).ThenResult(&tg.Updates{})
a.NoError(s.ToggleSlowMode(ctx, 1))
mock.ExpectCall(&tg.ChannelsToggleSlowModeRequest{
Channel: s.InputChannel(),
Seconds: 0,
}).ThenResult(&tg.Updates{})
a.NoError(s.DisableSlowMode(ctx))
}
func TestSupergroup_SetStickerSet(t *testing.T) {
a := require.New(t)
ctx := context.Background()
mock, m := testManager(t)
set := &tg.InputStickerSetShortName{ShortName: "gotd_stickers"}
ch := m.Channel(getTestSuperGroup())
s, ok := ch.ToSupergroup()
a.True(ok)
mock.ExpectCall(&tg.ChannelsSetStickersRequest{
Channel: s.InputChannel(),
Stickerset: set,
}).ThenRPCErr(getTestError())
a.Error(s.SetStickerSet(ctx, set))
mock.ExpectCall(&tg.ChannelsSetStickersRequest{
Channel: s.InputChannel(),
Stickerset: set,
}).ThenTrue()
a.NoError(s.SetStickerSet(ctx, set))
mock.ExpectCall(&tg.ChannelsSetStickersRequest{
Channel: s.InputChannel(),
Stickerset: &tg.InputStickerSetEmpty{},
}).ThenTrue()
a.NoError(s.ResetStickerSet(ctx))
}
+122
View File
@@ -0,0 +1,122 @@
package peers
import (
"go.mau.fi/mautrix-telegram/pkg/gotd/constant"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func userPeerID(id int64) (r constant.TDLibPeerID) {
r.User(id)
return r
}
func chatPeerID(id int64) (r constant.TDLibPeerID) {
r.Chat(id)
return r
}
func channelPeerID(id int64) (r constant.TDLibPeerID) {
r.Channel(id)
return r
}
func peerIDFromPeerClass(p tg.PeerClass) constant.TDLibPeerID {
switch p := p.(type) {
case *tg.PeerUser:
return userPeerID(p.UserID)
case *tg.PeerChat:
return chatPeerID(p.ChatID)
case *tg.PeerChannel:
return channelPeerID(p.ChannelID)
}
return 0
}
func (m *Manager) updated(ids ...constant.TDLibPeerID) {
m.needUpdateMux.Lock()
defer m.needUpdateMux.Unlock()
m.needUpdate.delete(ids...)
}
func (m *Manager) updatedFull(id constant.TDLibPeerID) {
m.needUpdateMux.Lock()
defer m.needUpdateMux.Unlock()
m.needUpdateFull.delete(id)
}
func (m *Manager) needsUpdate(id constant.TDLibPeerID) bool {
m.needUpdateMux.Lock()
defer m.needUpdateMux.Unlock()
return m.needUpdate.has(id)
}
func (m *Manager) needsUpdateFull(id constant.TDLibPeerID) bool {
m.needUpdateMux.Lock()
defer m.needUpdateMux.Unlock()
return m.needUpdateFull.has(id)
}
func (m *Manager) applyUpdates(updates []tg.UpdateClass) {
// TODO(tdakkota): support partial updates in storage
m.needUpdateMux.Lock()
defer m.needUpdateMux.Unlock()
appendBoth := func(p ...constant.TDLibPeerID) {
m.needUpdate.add(p...)
m.needUpdateFull.add(p...)
}
for _, update := range updates {
switch update := update.(type) {
case *tg.UpdateChatParticipants:
p, ok := update.Participants.(*tg.ChatParticipants)
if ok {
appendBoth(chatPeerID(p.ChatID))
}
case *tg.UpdateUserStatus:
m.needUpdate.add(userPeerID(update.UserID))
case *tg.UpdateUserName:
m.needUpdate.add(userPeerID(update.UserID))
case *tg.UpdateUser:
m.needUpdate.add(userPeerID(update.UserID))
case *tg.UpdateUserPhone:
m.needUpdate.add(userPeerID(update.UserID))
case *tg.UpdateChatParticipantAdd:
appendBoth(userPeerID(update.UserID), chatPeerID(update.ChatID))
case *tg.UpdateChatParticipantDelete:
appendBoth(userPeerID(update.UserID), chatPeerID(update.ChatID))
case *tg.UpdateNotifySettings:
if p, ok := update.Peer.(*tg.NotifyPeer); ok {
m.needUpdate.add(peerIDFromPeerClass(p.Peer))
}
case *tg.UpdateChannel:
m.needUpdate.add(channelPeerID(update.ChannelID))
case *tg.UpdateChatParticipantAdmin:
m.needUpdate.add(userPeerID(update.UserID), chatPeerID(update.ChatID))
case *tg.UpdateChatDefaultBannedRights:
appendBoth(peerIDFromPeerClass(update.Peer))
case *tg.UpdatePeerSettings:
m.needUpdate.add(peerIDFromPeerClass(update.Peer))
case *tg.UpdatePeerBlocked:
case *tg.UpdateChat:
m.needUpdate.add(chatPeerID(update.ChatID))
case *tg.UpdatePeerHistoryTTL:
m.needUpdate.add(peerIDFromPeerClass(update.Peer))
case *tg.UpdateChatParticipant:
m.needUpdate.add(userPeerID(update.UserID), chatPeerID(update.ChatID))
case *tg.UpdateChannelParticipant:
m.needUpdate.add(userPeerID(update.UserID), channelPeerID(update.ChannelID))
case *tg.UpdateBotStopped:
case *tg.UpdateBotCommands:
m.needUpdate.add(userPeerID(update.BotID))
case *tg.UpdatePendingJoinRequests:
m.needUpdate.add(peerIDFromPeerClass(update.Peer))
case *tg.UpdateBotChatInviteRequester:
}
}
}
+269
View File
@@ -0,0 +1,269 @@
package peers
import (
"context"
"fmt"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/constant"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// User is user peer.
type User struct {
raw *tg.User
m *Manager
}
// User creates new User, attached to this manager.
func (m *Manager) User(u *tg.User) User {
m.needsUpdate(userPeerID(u.ID))
return User{
raw: u,
m: m,
}
}
// GetUser gets User using given tg.InputUserClass.
func (m *Manager) GetUser(ctx context.Context, p tg.InputUserClass) (User, error) {
user, err := m.getUser(ctx, p)
if err != nil {
return User{}, err
}
return m.User(user), nil
}
// Raw returns raw *tg.User.
func (u User) Raw() *tg.User {
return u.raw
}
// ID returns entity ID.
func (u User) ID() int64 {
return u.raw.GetID()
}
// TDLibPeerID returns TDLibPeerID for this entity.
func (u User) TDLibPeerID() constant.TDLibPeerID {
return userPeerID(u.raw.GetID())
}
// VisibleName returns visible name of peer.
//
// It returns FirstName + " " + LastName for users, and title for chats and channels.
func (u User) VisibleName() string {
firstName := u.raw.FirstName
lastName := u.raw.LastName
if lastName == "" {
return firstName
}
return fmt.Sprintf("%s %s", firstName, lastName)
}
// Username returns peer username, if any.
func (u User) Username() (string, bool) {
return u.raw.GetUsername()
}
// Restricted whether this user/chat/channel is restricted.
func (u User) Restricted() ([]tg.RestrictionReason, bool) {
reason, ok := u.raw.GetRestrictionReason()
return reason, ok || u.raw.GetRestricted()
}
// Verified whether this user/chat/channel is verified by Telegram.
func (u User) Verified() bool {
return u.raw.Verified
}
// Scam whether this user/chat/channel is probably a scam.
func (u User) Scam() bool {
return u.raw.Scam
}
// Fake whether this user/chat/channel was reported by many users as a fake or scam: be
// careful when interacting with it.
func (u User) Fake() bool {
return u.raw.Fake
}
// InputPeer returns input peer for this peer.
func (u User) InputPeer() tg.InputPeerClass {
if u.Self() {
return &tg.InputPeerSelf{}
}
return &tg.InputPeerUser{
UserID: u.raw.ID,
AccessHash: u.raw.AccessHash,
}
}
// Sync updates current object.
func (u User) Sync(ctx context.Context) error {
raw, err := u.m.updateUser(ctx, u.InputUser())
if err != nil {
return errors.Wrap(err, "get user")
}
*u.raw = *raw
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{
Peer: u.InputPeer(),
Reason: reason,
Message: message,
}); err != nil {
return errors.Wrap(err, "report")
}
return nil
}
// Photo returns peer photo, if any.
func (u User) Photo(ctx context.Context) (*tg.Photo, bool, error) {
r, err := u.m.api.PhotosGetUserPhotos(ctx, &tg.PhotosGetUserPhotosRequest{
UserID: u.InputUser(),
Offset: 0,
Limit: 1,
})
if err != nil {
return nil, false, errors.Wrap(err, "get user photos")
}
if err := u.m.applyUsers(ctx, r.GetUsers()...); err != nil {
return nil, false, errors.Wrap(err, "apply users")
}
photos := r.GetPhotos()
if len(photos) < 1 {
return nil, false, nil
}
p, ok := photos[0].AsNotEmpty()
return p, ok, nil
}
// FullRaw returns *tg.UserFull for this User.
func (u User) FullRaw(ctx context.Context) (*tg.UserFull, error) {
return u.m.getUserFull(ctx, u.InputUser())
}
// ToBot tries to convert this User to Bot.
func (u User) ToBot() (Bot, bool) {
if !u.raw.Bot {
return Bot{}, false
}
return Bot{
User: u,
}, true
}
// Self whether this user indicates the currently logged-in user.
func (u User) Self() bool {
// TODO(tdakkota): return helper instead?
return u.raw.Self
}
// Contact whether this user is a contact.
func (u User) Contact() bool {
return u.raw.Contact
}
// MutualContact whether this user is a mutual contact.
func (u User) MutualContact() bool {
return u.raw.MutualContact
}
// Deleted whether the account of this user was deleted.
func (u User) Deleted() bool {
return u.raw.Deleted
}
// Support whether this is an official support user.
func (u User) Support() bool {
return u.raw.Support
}
// FirstName returns first name.
func (u User) FirstName() (string, bool) {
return u.raw.GetFirstName()
}
// LastName returns last name.
func (u User) LastName() (string, bool) {
return u.raw.GetLastName()
}
// Phone returns phone, if any.
func (u User) Phone() (string, bool) {
return u.raw.GetPhone()
}
// Status returns user status, if any.
func (u User) Status() (tg.UserStatusClass, bool) {
return u.raw.GetStatus()
}
// LangCode returns users lang code, if any.
func (u User) LangCode() (string, bool) {
return u.raw.GetLangCode()
}
// InputUser returns input user for this user.
func (u User) InputUser() tg.InputUserClass {
if u.Self() {
return &tg.InputUserSelf{}
}
return &tg.InputUser{
UserID: u.raw.ID,
AccessHash: u.raw.AccessHash,
}
}
// ReportSpam reports a new incoming chat for spam, if the peer settings of the chat allow us to do that.
func (u User) ReportSpam(ctx context.Context) error {
if _, err := u.m.api.MessagesReportSpam(ctx, u.InputPeer()); err != nil {
return errors.Wrap(err, "report spam")
}
return nil
}
// Block blocks this user.
func (u User) Block(ctx context.Context) error {
if _, err := u.m.api.ContactsBlock(ctx, &tg.ContactsBlockRequest{
ID: u.InputPeer(),
}); err != nil {
return errors.Wrap(err, "block")
}
return nil
}
// Unblock unblocks this user.
func (u User) Unblock(ctx context.Context) error {
if _, err := u.m.api.ContactsUnblock(ctx, &tg.ContactsUnblockRequest{
ID: u.InputPeer(),
}); err != nil {
return errors.Wrap(err, "unblock")
}
return nil
}
// InviteTo invites User to given channel.
func (u User) InviteTo(ctx context.Context, ch tg.InputChannelClass) error {
if _, err := u.m.api.ChannelsInviteToChannel(ctx, &tg.ChannelsInviteToChannelRequest{
Channel: ch,
Users: []tg.InputUserClass{u.InputUser()},
}); err != nil {
return errors.Wrap(err, "invite to channel")
}
return nil
}
// TODO(tdakkota): add more getters, helpers and convertors
+194
View File
@@ -0,0 +1,194 @@
package peers
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestUserGetters(t *testing.T) {
a := require.New(t)
u := User{
raw: &tg.User{
Self: true,
Contact: true,
MutualContact: true,
Deleted: true,
Bot: true,
BotChatHistory: true,
BotNochats: true,
Verified: true,
Restricted: true,
Min: true,
BotInlineGeo: true,
Support: true,
Scam: true,
ApplyMinPhoto: true,
Fake: true,
ID: 10,
AccessHash: 10,
FirstName: "FirstName",
LastName: "LastName",
Username: "Username",
Phone: "+79001234567",
Photo: &tg.UserProfilePhoto{
HasVideo: true,
PhotoID: 10,
StrippedThumb: []byte("abc"),
DCID: 1,
},
Status: &tg.UserStatusLastMonth{},
BotInfoVersion: 1,
RestrictionReason: []tg.RestrictionReason{{
Platform: "ios",
Reason: "ban",
Text: "ban",
}},
BotInlinePlaceholder: "placeholder",
LangCode: "ru",
},
}
u.raw.SetFlags()
a.Equal(u.raw, u.Raw())
a.True(u.TDLibPeerID().IsUser())
a.Equal(u.raw.GetSelf(), u.Self())
a.Equal(u.raw.GetContact(), u.Contact())
a.Equal(u.raw.GetMutualContact(), u.MutualContact())
a.Equal(u.raw.GetDeleted(), u.Deleted())
a.Equal(u.raw.GetVerified(), u.Verified())
a.Equal(u.raw.GetSupport(), u.Support())
a.Equal(u.raw.GetScam(), u.Scam())
a.Equal(u.raw.GetFake(), u.Fake())
a.Equal(u.raw.GetID(), u.ID())
{
reasons, ok := u.Restricted()
a.Equal(u.raw.GetRestricted(), ok)
a.Equal(u.raw.RestrictionReason, reasons)
}
{
v, ok := u.raw.GetFirstName()
v2, ok2 := u.FirstName()
a.Equal(ok, ok2)
a.Equal(v, v2)
}
{
v, ok := u.raw.GetLastName()
v2, ok2 := u.LastName()
a.Equal(ok, ok2)
a.Equal(v, v2)
}
{
v, ok := u.raw.GetUsername()
v2, ok2 := u.Username()
a.Equal(ok, ok2)
a.Equal(v, v2)
}
{
v, ok := u.raw.GetPhone()
v2, ok2 := u.Phone()
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)
a.Equal(b.raw.GetBotChatHistory(), b.ChatHistory())
a.Equal(!b.raw.GetBotNochats(), b.CanBeAdded())
a.Equal(b.raw.GetBotInlineGeo(), b.InlineGeo())
_, ok = b.raw.GetBotInlinePlaceholder()
a.Equal(ok, b.SupportsInline())
}
func TestUser_InputPeer(t *testing.T) {
require.Equal(t, &tg.InputPeerSelf{}, User{raw: &tg.User{Self: true}}.InputPeer())
require.Equal(t, &tg.InputPeerUser{
UserID: 10,
AccessHash: 10,
}, User{raw: &tg.User{
ID: 10,
AccessHash: 10,
}}.InputPeer())
}
func TestUser_InputUser(t *testing.T) {
require.Equal(t, &tg.InputUserSelf{}, User{raw: &tg.User{Self: true}}.InputUser())
require.Equal(t, &tg.InputUser{
UserID: 10,
AccessHash: 10,
}, User{raw: &tg.User{
ID: 10,
AccessHash: 10,
}}.InputUser())
}
func TestUser_VisibleName(t *testing.T) {
require.Equal(t, "FirstName", User{raw: &tg.User{FirstName: "FirstName"}}.VisibleName())
require.Equal(t, "FirstName LastName", User{raw: &tg.User{
FirstName: "FirstName",
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))
}