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:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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{},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user