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,113 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// PromiseDecorator is a decorator of peer promise.
|
||||
type PromiseDecorator = func(Promise) Promise
|
||||
|
||||
// ConstraintError is a peer resolve constraint error.
|
||||
type ConstraintError struct {
|
||||
Expected string
|
||||
Got tg.InputPeerClass
|
||||
}
|
||||
|
||||
// Error implements error.
|
||||
func (c *ConstraintError) Error() string {
|
||||
return fmt.Sprintf("expected %q, got %T", c.Expected, c.Got)
|
||||
}
|
||||
|
||||
func tryUnpackConstraint(p tg.InputPeerClass, resolveErr error) (tg.InputPeerClass, error) {
|
||||
var constraintErr *ConstraintError
|
||||
if errors.As(resolveErr, &constraintErr) {
|
||||
return constraintErr.Got, nil
|
||||
}
|
||||
return p, resolveErr
|
||||
}
|
||||
|
||||
// OnlyChannel returns Promise which returns error if resolved peer is not a channel.
|
||||
func OnlyChannel(p Promise) Promise {
|
||||
return func(ctx context.Context) (tg.InputPeerClass, error) {
|
||||
resolved, err := tryUnpackConstraint(p(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch resolved.(type) {
|
||||
case *tg.InputPeerChannel, *tg.InputPeerChannelFromMessage:
|
||||
return resolved, nil
|
||||
default:
|
||||
return nil, &ConstraintError{
|
||||
Expected: "channel",
|
||||
Got: resolved,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnlyChat returns Promise which returns error if resolved peer is not a chat.
|
||||
func OnlyChat(p Promise) Promise {
|
||||
return func(ctx context.Context) (tg.InputPeerClass, error) {
|
||||
resolved, err := tryUnpackConstraint(p(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch resolved.(type) {
|
||||
case *tg.InputPeerChat:
|
||||
return resolved, nil
|
||||
default:
|
||||
return nil, &ConstraintError{
|
||||
Expected: "chat",
|
||||
Got: resolved,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnlyUser returns Promise which returns error if resolved peer is not a user.
|
||||
func OnlyUser(p Promise) Promise {
|
||||
return func(ctx context.Context) (tg.InputPeerClass, error) {
|
||||
resolved, err := tryUnpackConstraint(p(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch resolved.(type) {
|
||||
case *tg.InputPeerUser, *tg.InputPeerUserFromMessage, *tg.InputPeerSelf:
|
||||
return resolved, nil
|
||||
default:
|
||||
return nil, &ConstraintError{
|
||||
Expected: "user",
|
||||
Got: resolved,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnlyUserID returns Promise which returns error if resolved peer is not a user object with ID.
|
||||
// Unlike OnlyUser, it returns error if resolved peer is tg.InputPeerSelf.
|
||||
func OnlyUserID(p Promise) Promise {
|
||||
return func(ctx context.Context) (tg.InputPeerClass, error) {
|
||||
resolved, err := tryUnpackConstraint(p(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch resolved.(type) {
|
||||
case *tg.InputPeerUser, *tg.InputPeerUserFromMessage:
|
||||
return resolved, nil
|
||||
default:
|
||||
return nil, &ConstraintError{
|
||||
Expected: "userID",
|
||||
Got: resolved,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
func createPromise(peer tg.InputPeerClass) Promise {
|
||||
return func(ctx context.Context) (tg.InputPeerClass, error) {
|
||||
return peer, nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstraintsCombine(t *testing.T) {
|
||||
decorate := func(promise Promise, decorators ...PromiseDecorator) Promise {
|
||||
for _, decorator := range decorators {
|
||||
promise = decorator(promise)
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
decorators []PromiseDecorator
|
||||
input tg.InputPeerClass
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"UserOrChannel",
|
||||
[]PromiseDecorator{OnlyUser, OnlyChannel},
|
||||
&tg.InputPeerChannel{
|
||||
ChannelID: 10,
|
||||
AccessHash: 10,
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"UserOrChannel",
|
||||
[]PromiseDecorator{OnlyUser, OnlyChannel},
|
||||
&tg.InputPeerChat{
|
||||
ChatID: 10,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"ChannelOrUser",
|
||||
[]PromiseDecorator{OnlyChannel, OnlyUser},
|
||||
&tg.InputPeerUser{
|
||||
UserID: 10,
|
||||
AccessHash: 10,
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"ChannelOrUser",
|
||||
[]PromiseDecorator{OnlyChannel, OnlyUser},
|
||||
&tg.InputPeerChat{
|
||||
ChatID: 10,
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
tname := "Good"
|
||||
if test.wantErr {
|
||||
tname = "Bad"
|
||||
}
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Run(tname, func(t *testing.T) {
|
||||
a := require.New(t)
|
||||
promise := decorate(createPromise(test.input), test.decorators...)
|
||||
_, err := promise(context.Background())
|
||||
if test.wantErr {
|
||||
a.Error(err)
|
||||
} else {
|
||||
a.NoError(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstraints(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
decorator func(Promise) Promise
|
||||
input tg.InputPeerClass
|
||||
wantErr bool
|
||||
}{
|
||||
{"Channel", OnlyChannel, &tg.InputPeerChannel{
|
||||
ChannelID: 10,
|
||||
AccessHash: 10,
|
||||
}, false},
|
||||
{"Channel", OnlyChannel, &tg.InputPeerUser{
|
||||
UserID: 10,
|
||||
AccessHash: 10,
|
||||
}, true},
|
||||
{"User", OnlyUser, &tg.InputPeerUser{
|
||||
UserID: 10,
|
||||
AccessHash: 10,
|
||||
}, false},
|
||||
{"User", OnlyUser, &tg.InputPeerSelf{}, false},
|
||||
{"User", OnlyUser, &tg.InputPeerChannel{
|
||||
ChannelID: 10,
|
||||
AccessHash: 10,
|
||||
}, true},
|
||||
{"UserID", OnlyUserID, &tg.InputPeerUser{
|
||||
UserID: 10,
|
||||
AccessHash: 10,
|
||||
}, false},
|
||||
{"UserID", OnlyUserID, &tg.InputPeerChannel{
|
||||
ChannelID: 10,
|
||||
AccessHash: 10,
|
||||
}, true},
|
||||
{"UserID", OnlyUserID, &tg.InputPeerSelf{}, true},
|
||||
{"Chat", OnlyChat, &tg.InputPeerChat{
|
||||
ChatID: 10,
|
||||
}, false},
|
||||
{"Chat", OnlyChat, &tg.InputPeerChannel{
|
||||
ChannelID: 10,
|
||||
AccessHash: 10,
|
||||
}, true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
tname := "Good"
|
||||
if test.wantErr {
|
||||
tname = "Bad"
|
||||
}
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Run(tname, func(t *testing.T) {
|
||||
a := require.New(t)
|
||||
promise := test.decorator(createPromise(test.input))
|
||||
_, err := promise(context.Background())
|
||||
if test.wantErr {
|
||||
a.Error(err)
|
||||
} else {
|
||||
a.NoError(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// ToInputUser converts given peer to input user if possible.
|
||||
func ToInputUser(user tg.InputPeerClass) (tg.InputUserClass, bool) {
|
||||
switch u := user.(type) {
|
||||
case *tg.InputPeerUser:
|
||||
v := new(tg.InputUser)
|
||||
v.FillFrom(u)
|
||||
return v, true
|
||||
case *tg.InputPeerUserFromMessage:
|
||||
v := new(tg.InputUserFromMessage)
|
||||
v.FillFrom(u)
|
||||
return v, true
|
||||
case *tg.InputPeerSelf:
|
||||
v := new(tg.InputUserSelf)
|
||||
return v, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// ToInputChannel converts given peer to input channel if possible.
|
||||
func ToInputChannel(channel tg.InputPeerClass) (tg.InputChannelClass, bool) {
|
||||
switch u := channel.(type) {
|
||||
case *tg.InputPeerChannel:
|
||||
v := new(tg.InputChannel)
|
||||
v.FillFrom(u)
|
||||
return v, true
|
||||
case *tg.InputPeerChannelFromMessage:
|
||||
v := new(tg.InputChannelFromMessage)
|
||||
v.FillFrom(u)
|
||||
return v, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package peer conatains some peer resolving and extracting helpers.
|
||||
package peer
|
||||
@@ -0,0 +1,159 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// Entities is simple peer entities storage.
|
||||
type Entities struct {
|
||||
users map[int64]*tg.User
|
||||
chats map[int64]*tg.Chat
|
||||
channels map[int64]*tg.Channel
|
||||
}
|
||||
|
||||
// NewEntities creates new Entities struct.
|
||||
func NewEntities(
|
||||
users map[int64]*tg.User,
|
||||
chats map[int64]*tg.Chat,
|
||||
channels map[int64]*tg.Channel,
|
||||
) Entities {
|
||||
return Entities{users: users, chats: chats, channels: channels}
|
||||
}
|
||||
|
||||
// EntitySearchResult is abstraction for different RPC responses which
|
||||
// contains entities.
|
||||
type EntitySearchResult interface {
|
||||
MapChats() tg.ChatClassArray
|
||||
MapUsers() tg.UserClassArray
|
||||
}
|
||||
|
||||
// EntitiesFromResult fills Entities struct using given context.
|
||||
func EntitiesFromResult(r EntitySearchResult) Entities {
|
||||
return NewEntities(
|
||||
r.MapUsers().UserToMap(),
|
||||
r.MapChats().ChatToMap(),
|
||||
r.MapChats().ChannelToMap(),
|
||||
)
|
||||
}
|
||||
|
||||
// EntitiesFromUpdate fills Entities struct using given context.
|
||||
func EntitiesFromUpdate(uctx tg.Entities) Entities {
|
||||
return NewEntities(
|
||||
uctx.Users,
|
||||
uctx.Chats,
|
||||
uctx.Channels,
|
||||
)
|
||||
}
|
||||
|
||||
// Users returns map of users.
|
||||
// Notice that returned map is not a copy.
|
||||
func (ent Entities) Users() map[int64]*tg.User {
|
||||
return ent.users
|
||||
}
|
||||
|
||||
// Chats returns map of chats.
|
||||
// Notice that returned map is not a copy.
|
||||
func (ent Entities) Chats() map[int64]*tg.Chat {
|
||||
return ent.chats
|
||||
}
|
||||
|
||||
// Channels returns map of channels.
|
||||
// Notice that returned map is not a copy.
|
||||
func (ent Entities) Channels() map[int64]*tg.Channel {
|
||||
return ent.channels
|
||||
}
|
||||
|
||||
// FillFromResult adds and updates all entities from given result.
|
||||
func (ent Entities) FillFromResult(r EntitySearchResult) {
|
||||
r.MapUsers().FillUserMap(ent.users)
|
||||
r.MapChats().FillChatMap(ent.chats)
|
||||
r.MapChats().FillChannelMap(ent.channels)
|
||||
}
|
||||
|
||||
// FillFromUpdate adds and updates all entities from given updates.
|
||||
func (ent Entities) FillFromUpdate(uctx tg.Entities) {
|
||||
ent.Fill(
|
||||
uctx.Users,
|
||||
uctx.Chats,
|
||||
uctx.Channels,
|
||||
)
|
||||
}
|
||||
|
||||
// Fill adds and updates all entities from given maps.
|
||||
func (ent Entities) Fill(
|
||||
users map[int64]*tg.User,
|
||||
chats map[int64]*tg.Chat,
|
||||
channels map[int64]*tg.Channel,
|
||||
) {
|
||||
for k, v := range users {
|
||||
ent.users[k] = v
|
||||
}
|
||||
|
||||
for k, v := range chats {
|
||||
ent.chats[k] = v
|
||||
}
|
||||
|
||||
for k, v := range channels {
|
||||
ent.channels[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractPeer finds and creates InputPeerClass using given PeerClass.
|
||||
func (ent Entities) ExtractPeer(peerID tg.PeerClass) (tg.InputPeerClass, error) {
|
||||
var peer tg.InputPeerClass
|
||||
switch p := peerID.(type) {
|
||||
case *tg.PeerUser: // peerUser#9db1bc6d
|
||||
dialog, ok := ent.users[p.UserID]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("user %d not found", p.UserID)
|
||||
}
|
||||
|
||||
peer = &tg.InputPeerUser{
|
||||
UserID: dialog.ID,
|
||||
AccessHash: dialog.AccessHash,
|
||||
}
|
||||
case *tg.PeerChat: // peerChat#bad0e5bb
|
||||
dialog, ok := ent.chats[p.ChatID]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("chat %d not found", p.ChatID)
|
||||
}
|
||||
|
||||
peer = &tg.InputPeerChat{
|
||||
ChatID: dialog.ID,
|
||||
}
|
||||
case *tg.PeerChannel: // peerChannel#bddde532
|
||||
dialog, ok := ent.channels[p.ChannelID]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("channel %d not found", p.ChannelID)
|
||||
}
|
||||
|
||||
peer = &tg.InputPeerChannel{
|
||||
ChannelID: dialog.ID,
|
||||
AccessHash: dialog.AccessHash,
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf("unexpected peer type %T", peerID)
|
||||
}
|
||||
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
// User finds user by given ID.
|
||||
func (ent Entities) User(id int64) (*tg.User, bool) {
|
||||
v, ok := ent.users[id]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// Chat finds chat by given ID.
|
||||
func (ent Entities) Chat(id int64) (*tg.Chat, bool) {
|
||||
v, ok := ent.chats[id]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// Channel finds channel by given ID.
|
||||
func (ent Entities) Channel(id int64) (*tg.Channel, bool) {
|
||||
v, ok := ent.channels[id]
|
||||
return v, ok
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
type badPeer struct {
|
||||
tg.PeerClass
|
||||
}
|
||||
|
||||
type mockResult struct {
|
||||
Entities
|
||||
}
|
||||
|
||||
func (m mockResult) MapUsers() (r tg.UserClassArray) {
|
||||
for _, e := range m.Entities.Users() {
|
||||
r = append(r, e)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (m mockResult) MapChats() (r tg.ChatClassArray) {
|
||||
for _, e := range m.Entities.Chats() {
|
||||
r = append(r, e)
|
||||
}
|
||||
|
||||
for _, e := range m.Entities.Channels() {
|
||||
r = append(r, e)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func TestEntities(t *testing.T) {
|
||||
users := map[int64]*tg.User{
|
||||
10: {ID: 10, AccessHash: 10},
|
||||
}
|
||||
chats := map[int64]*tg.Chat{
|
||||
10: {ID: 10},
|
||||
}
|
||||
channels := map[int64]*tg.Channel{
|
||||
10: {ID: 10, AccessHash: 10},
|
||||
}
|
||||
ent := NewEntities(users, chats, channels)
|
||||
ctx := tg.Entities{
|
||||
Users: users,
|
||||
Chats: chats,
|
||||
Channels: channels,
|
||||
}
|
||||
result := mockResult{Entities: ent}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filler func() Entities
|
||||
}{
|
||||
{"NewEntities", func() Entities {
|
||||
return ent
|
||||
}},
|
||||
{"EntitiesFromResult", func() Entities {
|
||||
return EntitiesFromResult(result)
|
||||
}},
|
||||
{"FillFromResult", func() Entities {
|
||||
e := NewEntities(
|
||||
map[int64]*tg.User{},
|
||||
map[int64]*tg.Chat{},
|
||||
map[int64]*tg.Channel{},
|
||||
)
|
||||
e.FillFromResult(result)
|
||||
return e
|
||||
}},
|
||||
{"EntitiesFromUpdate", func() Entities {
|
||||
return EntitiesFromUpdate(ctx)
|
||||
}},
|
||||
{"FillFromUpdate", func() Entities {
|
||||
e := NewEntities(
|
||||
map[int64]*tg.User{},
|
||||
map[int64]*tg.Chat{},
|
||||
map[int64]*tg.Channel{},
|
||||
)
|
||||
e.FillFromUpdate(ctx)
|
||||
return e
|
||||
}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
a := require.New(t)
|
||||
e := test.filler()
|
||||
|
||||
_, err := e.ExtractPeer(badPeer{})
|
||||
a.Error(err)
|
||||
|
||||
user, err := e.ExtractPeer(&tg.PeerUser{UserID: 10})
|
||||
peerUser := &tg.InputPeerUser{
|
||||
UserID: 10,
|
||||
AccessHash: 10,
|
||||
}
|
||||
a.Equal(peerUser, user)
|
||||
a.NoError(err)
|
||||
_, err = e.ExtractPeer(&tg.PeerUser{UserID: 11})
|
||||
a.Error(err)
|
||||
|
||||
chat, err := e.ExtractPeer(&tg.PeerChat{ChatID: 10})
|
||||
peerChat := &tg.InputPeerChat{
|
||||
ChatID: 10,
|
||||
}
|
||||
a.Equal(peerChat, chat)
|
||||
a.NoError(err)
|
||||
_, err = e.ExtractPeer(&tg.PeerChat{ChatID: 11})
|
||||
a.Error(err)
|
||||
|
||||
channel, err := e.ExtractPeer(&tg.PeerChannel{ChannelID: 10})
|
||||
peerChannel := &tg.InputPeerChannel{
|
||||
ChannelID: 10,
|
||||
AccessHash: 10,
|
||||
}
|
||||
a.Equal(peerChannel, channel)
|
||||
a.NoError(err)
|
||||
_, err = e.ExtractPeer(&tg.PeerChannel{ChannelID: 11})
|
||||
a.Error(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
type nodeData struct {
|
||||
key string
|
||||
value tg.InputPeerClass
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type linkedNode struct {
|
||||
next, prev *linkedNode
|
||||
nodeData
|
||||
}
|
||||
|
||||
// linkedList is a doubly linked list implementation.
|
||||
// This implementation is highly inspired by container/list, but type safe.
|
||||
type linkedList struct {
|
||||
root linkedNode
|
||||
len int
|
||||
}
|
||||
|
||||
// Front returns the first element of list l or nil if the list is empty.
|
||||
func (l *linkedList) Front() *linkedNode {
|
||||
if l.len == 0 {
|
||||
return nil
|
||||
}
|
||||
return l.root.next
|
||||
}
|
||||
|
||||
// Back returns the last element of list l or nil if the list is empty.
|
||||
func (l *linkedList) Back() *linkedNode {
|
||||
if l.len == 0 {
|
||||
return nil
|
||||
}
|
||||
return l.root.prev
|
||||
}
|
||||
|
||||
// insert inserts e after at, increments l.len, and returns e.
|
||||
func (l *linkedList) insert(e, at *linkedNode) *linkedNode {
|
||||
e.prev = at
|
||||
e.next = at.next
|
||||
e.prev.next = e
|
||||
e.next.prev = e
|
||||
l.len++
|
||||
return e
|
||||
}
|
||||
|
||||
func (l *linkedList) insertValue(v nodeData, at *linkedNode) *linkedNode {
|
||||
e := new(linkedNode)
|
||||
e.nodeData = v
|
||||
return l.insert(e, at)
|
||||
}
|
||||
|
||||
// PushFront inserts a new element e with value v at the front of list l and returns e.
|
||||
func (l *linkedList) PushFront(v nodeData) *linkedNode {
|
||||
l.lazyInit()
|
||||
return l.insertValue(v, &l.root)
|
||||
}
|
||||
|
||||
// lazyInit lazily initializes a zero List value.
|
||||
func (l *linkedList) lazyInit() {
|
||||
if l.root.next == nil {
|
||||
l.root.next = &l.root
|
||||
l.root.prev = &l.root
|
||||
l.len = 0
|
||||
}
|
||||
}
|
||||
|
||||
// remove removes e from its list, decrements l.len, and returns e.
|
||||
func (l *linkedList) remove(e *linkedNode) nodeData {
|
||||
e.prev.next = e.next
|
||||
e.next.prev = e.prev
|
||||
e.next = nil // avoid memory leaks
|
||||
e.prev = nil // avoid memory leaks
|
||||
l.len--
|
||||
|
||||
value := e.nodeData
|
||||
return value
|
||||
}
|
||||
|
||||
// move moves e to next to at and returns e.
|
||||
func (l *linkedList) move(e, at *linkedNode) *linkedNode {
|
||||
if e == at {
|
||||
return e
|
||||
}
|
||||
e.prev.next = e.next
|
||||
e.next.prev = e.prev
|
||||
|
||||
e.prev = at
|
||||
e.next = at.next
|
||||
e.prev.next = e
|
||||
e.next.prev = e
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// Remove removes e from l if e is an element of list l.
|
||||
// It returns the element value e.Value.
|
||||
// The element must not be nil.
|
||||
func (l *linkedList) Remove(e *linkedNode) nodeData {
|
||||
return l.remove(e)
|
||||
}
|
||||
|
||||
// MoveToFront moves element e to the front of list l.
|
||||
// If e is not an element of l, the list is not modified.
|
||||
// The element must not be nil.
|
||||
func (l *linkedList) MoveToFront(e *linkedNode) {
|
||||
if l.root.next == e {
|
||||
return
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
l.move(e, &l.root)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/clock"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// LRUResolver is simple decorator for Resolver to cache result in LRU.
|
||||
type LRUResolver struct {
|
||||
next Resolver
|
||||
clock clock.Clock
|
||||
|
||||
expiration time.Duration
|
||||
capacity int
|
||||
|
||||
cache map[string]*linkedNode
|
||||
lruList *linkedList
|
||||
// Guards LRU state — cache and lruList
|
||||
mux sync.Mutex
|
||||
|
||||
// Prevents multiple identical requests at the same time.
|
||||
sg singleflight.Group
|
||||
}
|
||||
|
||||
// NewLRUResolver creates new LRUResolver.
|
||||
func NewLRUResolver(next Resolver, capacity int) *LRUResolver {
|
||||
return &LRUResolver{
|
||||
next: next,
|
||||
clock: clock.System,
|
||||
expiration: time.Minute,
|
||||
capacity: capacity,
|
||||
cache: make(map[string]*linkedNode, capacity),
|
||||
lruList: &linkedList{},
|
||||
sg: singleflight.Group{},
|
||||
}
|
||||
}
|
||||
|
||||
// WithClock sets clock to use when counting expiration.
|
||||
func (l *LRUResolver) WithClock(c clock.Clock) *LRUResolver {
|
||||
l.clock = c
|
||||
return l
|
||||
}
|
||||
|
||||
// WithExpiration sets expiration timeout for records in cache.
|
||||
// If zero, expiration will be disabled. Default value is a minute.
|
||||
func (l *LRUResolver) WithExpiration(expiration time.Duration) *LRUResolver {
|
||||
l.expiration = expiration
|
||||
return l
|
||||
}
|
||||
|
||||
// Evict deletes record from cache.
|
||||
func (l *LRUResolver) Evict(key string) (tg.InputPeerClass, bool) {
|
||||
return l.delete(key)
|
||||
}
|
||||
|
||||
// ResolveDomain implements Resolver.
|
||||
func (l *LRUResolver) ResolveDomain(ctx context.Context, domain string) (tg.InputPeerClass, error) {
|
||||
if v, ok := l.get(domain); ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
r, err := l.next.ResolveDomain(ctx, domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.put(domain, r)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// ResolvePhone implements Resolver.
|
||||
func (l *LRUResolver) ResolvePhone(ctx context.Context, phone string) (tg.InputPeerClass, error) {
|
||||
if v, ok := l.get(phone); ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
r, err := l.next.ResolvePhone(ctx, phone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.put(phone, r)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (l *LRUResolver) get(key string) (v tg.InputPeerClass, ok bool) {
|
||||
l.mux.Lock()
|
||||
defer l.mux.Unlock()
|
||||
|
||||
if found, ok := l.cache[key]; ok {
|
||||
// Delete expired and return false.
|
||||
if l.expiration > 0 && l.clock.Now().After(found.expiresAt) {
|
||||
l.deleteLocked(key)
|
||||
return nil, false
|
||||
}
|
||||
l.lruList.MoveToFront(found)
|
||||
return found.value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *LRUResolver) put(key string, value tg.InputPeerClass) {
|
||||
l.mux.Lock()
|
||||
defer l.mux.Unlock()
|
||||
|
||||
if found, ok := l.cache[key]; ok {
|
||||
found.value = value
|
||||
l.lruList.MoveToFront(found)
|
||||
} else {
|
||||
if len(l.cache) >= l.capacity {
|
||||
l.deleteLocked(l.lruList.Back().key)
|
||||
}
|
||||
|
||||
l.cache[key] = l.lruList.PushFront(nodeData{
|
||||
key,
|
||||
value,
|
||||
l.clock.Now().Add(l.expiration),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LRUResolver) delete(key string) (tg.InputPeerClass, bool) {
|
||||
l.mux.Lock()
|
||||
defer l.mux.Unlock()
|
||||
return l.deleteLocked(key)
|
||||
}
|
||||
|
||||
// deleteLocked deletes record from cache.
|
||||
// Assumes mutex is locked.
|
||||
func (l *LRUResolver) deleteLocked(key string) (tg.InputPeerClass, bool) {
|
||||
found, ok := l.cache[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
l.lruList.Remove(found)
|
||||
delete(l.cache, key)
|
||||
return nil, true
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package peer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/message"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/message/peer"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
func resolveLRU(ctx context.Context) error {
|
||||
client, err := telegram.ClientFromEnvironment(telegram.Options{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.Run(ctx, func(ctx context.Context) error {
|
||||
raw := tg.NewClient(client)
|
||||
resolver := peer.NewLRUResolver(peer.Plain(raw), 16).WithExpiration(time.Minute)
|
||||
sender := message.NewSender(raw).WithResolver(resolver)
|
||||
|
||||
// "durovschat" will be resolved by Plain resolver.
|
||||
if _, err := sender.Resolve("@durovschat").Dice(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// "durovschat" will be resolved by cache.
|
||||
if _, err := sender.Resolve("https://t.me/durovschat").Darts(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Evict and delete record.
|
||||
resolver.Evict("durovschat")
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ExampleLRUResolver_cache() {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
if err := resolveLRU(ctx); err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%+v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gotd/neo"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
func TestLRU(t *testing.T) {
|
||||
a := require.New(t)
|
||||
c := neo.NewTime(time.Now())
|
||||
lru := NewLRUResolver(nil, 4).WithClock(c).WithExpiration(1 * time.Second)
|
||||
|
||||
// Add 5 entries.
|
||||
// State: [4 3 2 1]
|
||||
for i := range [5]struct{}{} {
|
||||
lru.put(strconv.Itoa(i), &tg.InputPeerChat{
|
||||
ChatID: int64(i),
|
||||
})
|
||||
}
|
||||
|
||||
// First entry must be evicted.
|
||||
// State: [4 3 2 1]
|
||||
_, ok := lru.get(strconv.Itoa(0))
|
||||
a.False(ok)
|
||||
|
||||
// Third must not.
|
||||
// State: [2 3 4 1]
|
||||
_, ok = lru.get(strconv.Itoa(2))
|
||||
a.True(ok)
|
||||
a.Equal("2", lru.lruList.Front().nodeData.key)
|
||||
|
||||
// Add yet another.
|
||||
// State: [6 2 3 4]
|
||||
lru.put(strconv.Itoa(6), &tg.InputPeerChat{
|
||||
ChatID: 6,
|
||||
})
|
||||
a.Equal("6", lru.lruList.Front().nodeData.key)
|
||||
|
||||
// Then yet one must be evicted.
|
||||
_, ok = lru.get(strconv.Itoa(1))
|
||||
a.False(ok)
|
||||
|
||||
// Add which already exist.
|
||||
// State: [4 6 2 3]
|
||||
lru.put(strconv.Itoa(4), &tg.InputPeerChat{
|
||||
ChatID: 6,
|
||||
})
|
||||
a.Equal("4", lru.lruList.Front().nodeData.key)
|
||||
|
||||
// Delete key which does not exist.
|
||||
// State: [4 6 2 3]
|
||||
_, ok = lru.Evict(strconv.Itoa(10))
|
||||
a.False(ok)
|
||||
|
||||
c.Travel(time.Hour)
|
||||
// Delete expired key.
|
||||
// State: [6 2 3]
|
||||
_, ok = lru.get("4")
|
||||
a.False(ok)
|
||||
}
|
||||
|
||||
type mockResolver struct {
|
||||
counter int
|
||||
returnErr bool
|
||||
domain, phone string
|
||||
peer tg.InputPeerClass
|
||||
t testing.TB
|
||||
}
|
||||
|
||||
func (m *mockResolver) ResolveDomain(ctx context.Context, domain string) (tg.InputPeerClass, error) {
|
||||
m.counter++
|
||||
|
||||
if m.returnErr && m.counter == 1 {
|
||||
return nil, testutil.TestError()
|
||||
}
|
||||
|
||||
if domain != m.domain {
|
||||
err := errors.Errorf("expected domain %q, got %q", m.domain, domain)
|
||||
m.t.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
return m.peer, nil
|
||||
}
|
||||
|
||||
func (m *mockResolver) ResolvePhone(ctx context.Context, phone string) (tg.InputPeerClass, error) {
|
||||
m.counter++
|
||||
|
||||
if m.returnErr && m.counter == 1 {
|
||||
return nil, testutil.TestError()
|
||||
}
|
||||
|
||||
if phone != m.phone {
|
||||
err := errors.Errorf("expected phone %q, got %q", m.phone, phone)
|
||||
m.t.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
return m.peer, nil
|
||||
}
|
||||
|
||||
func TestLRUResolver_Resolve(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
expectedDomain := "telegram"
|
||||
expected := &tg.InputPeerUser{
|
||||
UserID: 10,
|
||||
}
|
||||
|
||||
t.Run("Cache", func(t *testing.T) {
|
||||
a := require.New(t)
|
||||
resolver := &mockResolver{
|
||||
domain: expectedDomain,
|
||||
peer: expected,
|
||||
t: t,
|
||||
}
|
||||
|
||||
lru := NewLRUResolver(resolver, 10)
|
||||
|
||||
r, err := lru.ResolveDomain(ctx, expectedDomain)
|
||||
a.NoError(err)
|
||||
a.Equal(expected, r)
|
||||
|
||||
r2, err := lru.ResolveDomain(ctx, expectedDomain)
|
||||
a.NoError(err)
|
||||
a.Equal(expected, r2)
|
||||
|
||||
a.Equalf(1, resolver.counter, "RPC call was not cached")
|
||||
})
|
||||
|
||||
t.Run("Error", func(t *testing.T) {
|
||||
a := require.New(t)
|
||||
resolver := &mockResolver{
|
||||
returnErr: true,
|
||||
domain: expectedDomain,
|
||||
peer: expected,
|
||||
t: t,
|
||||
}
|
||||
|
||||
lru := NewLRUResolver(resolver, 10)
|
||||
|
||||
_, err := lru.ResolveDomain(ctx, expectedDomain)
|
||||
a.Error(err)
|
||||
|
||||
r2, err := lru.ResolveDomain(ctx, expectedDomain)
|
||||
a.NoError(err)
|
||||
a.Equal(expected, r2)
|
||||
|
||||
a.Equalf(2, resolver.counter, "RPC call error was cached")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package peer
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// Promise is a peer promise.
|
||||
type Promise func(ctx context.Context) (tg.InputPeerClass, error)
|
||||
|
||||
// Resolve uses given string to create new peer promise.
|
||||
// It resolves peer of message using given Resolver.
|
||||
// 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 Resolve(r Resolver, from string) Promise {
|
||||
from = strings.TrimSpace(from)
|
||||
|
||||
if deeplink.IsDeeplinkLike(from) {
|
||||
return ResolveDeeplink(r, from)
|
||||
}
|
||||
if isPhoneNumber(from) {
|
||||
return ResolvePhone(r, from)
|
||||
}
|
||||
|
||||
return ResolveDomain(r, from)
|
||||
}
|
||||
|
||||
func isPhoneNumber(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
r := rune(s[0])
|
||||
return r == '+' || ascii.IsDigit(r)
|
||||
}
|
||||
|
||||
func cleanupPhone(phone string) string {
|
||||
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 create new peer promise.
|
||||
// It resolves peer of message using given Resolver.
|
||||
// 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 ResolvePhone(r Resolver, phone string) Promise {
|
||||
return func(ctx context.Context) (tg.InputPeerClass, error) {
|
||||
return r.ResolvePhone(ctx, cleanupPhone(phone))
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveDomain uses given domain to create new peer promise.
|
||||
// It resolves peer of message using given Resolver.
|
||||
// Can has prefix with @ or not.
|
||||
// Input examples:
|
||||
//
|
||||
// @telegram
|
||||
// telegram
|
||||
func ResolveDomain(r Resolver, domain string) Promise {
|
||||
return func(ctx context.Context) (tg.InputPeerClass, error) {
|
||||
domain = strings.TrimPrefix(domain, "@")
|
||||
|
||||
if err := validateDomain(domain); err != nil {
|
||||
return nil, errors.Wrap(err, "validate domain")
|
||||
}
|
||||
|
||||
return r.ResolveDomain(ctx, domain)
|
||||
}
|
||||
}
|
||||
|
||||
func validateDomain(domain string) error {
|
||||
return deeplink.ValidateDomain(domain)
|
||||
}
|
||||
|
||||
// ResolveDeeplink uses given deeplink to create new peer promise.
|
||||
// Deeplink is a URL like https://t.me/telegram.
|
||||
// It resolves peer of message using given Resolver.
|
||||
// Input examples:
|
||||
//
|
||||
// t.me/telegram
|
||||
// https://t.me/telegram
|
||||
// tg:resolve?domain=telegram
|
||||
// tg://resolve?domain=telegram
|
||||
func ResolveDeeplink(r Resolver, u string) Promise {
|
||||
return func(ctx context.Context) (tg.InputPeerClass, 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 r.ResolveDomain(ctx, domain)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
func resolver(t *testing.T, expectedDomain string, expected tg.InputPeerClass) Resolver {
|
||||
return &mockResolver{
|
||||
domain: expectedDomain,
|
||||
peer: expected,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func TestSender_Resolve(t *testing.T) {
|
||||
formats := []struct {
|
||||
fmt string
|
||||
wantErr bool
|
||||
}{
|
||||
{`%s`, false},
|
||||
{`@%s`, false},
|
||||
{`t.me/%s`, false},
|
||||
{`t.me/%s/`, false},
|
||||
{`https://t.me/%s`, false},
|
||||
{`https://t.me/%s/`, false},
|
||||
{`tg:resolve?domain=%s`, false},
|
||||
{`tg://resolve?domain=%s`, false},
|
||||
|
||||
{`https://t.co/%s`, true},
|
||||
{`rt://resolve?domain=%s`, true},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
wantErr bool
|
||||
}{
|
||||
{"Good", "telegram", false},
|
||||
{"Good with numbers", "telegram123", false},
|
||||
{"Good with _", "telegram_test", false},
|
||||
{"Good with numbers and _", "telegram_test123", false},
|
||||
{"Bad", "_gotd_test", true},
|
||||
{"Bad", "gotd_test_", true},
|
||||
{"Bad", "_gotd_test123", true},
|
||||
{"Bad", "gotd.test", true},
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
t.Run(format.fmt, func(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
name := tt.name
|
||||
if tt.wantErr {
|
||||
name = fmt.Sprintf("%s (%q)", tt.name, tt.domain)
|
||||
}
|
||||
|
||||
expected := &tg.InputPeerUser{
|
||||
UserID: 1,
|
||||
AccessHash: 10,
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
a := require.New(t)
|
||||
|
||||
p, err := Resolve(
|
||||
resolver(t, tt.domain, expected),
|
||||
fmt.Sprintf(format.fmt, tt.domain),
|
||||
)(context.Background())
|
||||
if tt.wantErr || format.wantErr {
|
||||
a.Error(err)
|
||||
return
|
||||
}
|
||||
a.NoError(err)
|
||||
a.Equal(expected, p)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_cleanupPhone(t *testing.T) {
|
||||
tests := []struct {
|
||||
phone string
|
||||
want string
|
||||
}{
|
||||
{"+13115552368", "13115552368"},
|
||||
{"+1 (311) 555-0123", "13115550123"},
|
||||
{"+1 311 555-6162", "13115556162"},
|
||||
{"13115556162", "13115556162"},
|
||||
{"123gotd_test", "123"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.phone, func(t *testing.T) {
|
||||
r := cleanupPhone(tt.phone)
|
||||
require.Equal(t, tt.want, r)
|
||||
_, err := strconv.ParseInt(r, 10, 64)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// Resolver is an abstraction to resolve domains and Telegram deeplinks.
|
||||
type Resolver interface {
|
||||
ResolveDomain(ctx context.Context, domain string) (tg.InputPeerClass, error)
|
||||
ResolvePhone(ctx context.Context, phone string) (tg.InputPeerClass, error)
|
||||
}
|
||||
|
||||
// DefaultResolver creates and returns default resolver.
|
||||
func DefaultResolver(raw *tg.Client) Resolver {
|
||||
return NewLRUResolver(SingleflightResolver(Plain(raw)), 10)
|
||||
}
|
||||
|
||||
// Plain creates plain resolver.
|
||||
func Plain(raw *tg.Client) Resolver {
|
||||
return plainResolver{raw: raw}
|
||||
}
|
||||
|
||||
type plainResolver struct {
|
||||
raw *tg.Client
|
||||
}
|
||||
|
||||
func (p plainResolver) ResolveDomain(ctx context.Context, domain string) (tg.InputPeerClass, error) {
|
||||
peer, err := p.raw.ContactsResolveUsername(ctx, &tg.ContactsResolveUsernameRequest{
|
||||
Username: domain,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "resolve")
|
||||
}
|
||||
|
||||
return EntitiesFromResult(peer).ExtractPeer(peer.Peer)
|
||||
}
|
||||
|
||||
func (p plainResolver) ResolvePhone(ctx context.Context, phone string) (tg.InputPeerClass, error) {
|
||||
r, err := p.raw.ContactsGetContacts(ctx, 0)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get contacts")
|
||||
}
|
||||
|
||||
switch c := r.(type) {
|
||||
case *tg.ContactsContacts:
|
||||
for _, u := range c.Users {
|
||||
user, ok := u.AsNotEmpty()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if user.Phone == phone {
|
||||
return user.AsInputPeer(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("can't resolve phone %q", phone)
|
||||
default:
|
||||
return nil, errors.Errorf("unexpected type %T", r)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"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 Test_plainResolver_Resolve(t *testing.T) {
|
||||
mock := tgmock.NewRequire(t)
|
||||
raw := tg.NewClient(mock)
|
||||
|
||||
domain := "adcd"
|
||||
mock.ExpectCall(&tg.ContactsResolveUsernameRequest{
|
||||
Username: domain,
|
||||
}).ThenResult(&tg.ContactsResolvedPeer{
|
||||
Peer: &tg.PeerUser{UserID: 10},
|
||||
Users: []tg.UserClass{
|
||||
&tg.User{ID: 10, AccessHash: 10, Username: domain},
|
||||
},
|
||||
}).ExpectCall(&tg.ContactsResolveUsernameRequest{
|
||||
Username: domain,
|
||||
}).ThenRPCErr(&tgerr.Error{
|
||||
Code: 1337,
|
||||
Message: "TEST_ERROR",
|
||||
Type: "TEST_ERROR",
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
resolver := plainResolver{raw: raw}
|
||||
|
||||
r, err := resolver.ResolveDomain(ctx, domain)
|
||||
require.IsType(t, &tg.InputPeerUser{}, r)
|
||||
require.Equal(t, int64(10), r.(*tg.InputPeerUser).UserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = resolver.ResolveDomain(ctx, domain)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_plainResolver_ResolvePhone(t *testing.T) {
|
||||
mock := tgmock.New(t)
|
||||
raw := tg.NewClient(mock)
|
||||
|
||||
phone := "adcd"
|
||||
mock.ExpectCall(&tg.ContactsGetContactsRequest{
|
||||
Hash: 0,
|
||||
}).ThenResult(&tg.ContactsContacts{
|
||||
Contacts: []tg.Contact{{
|
||||
UserID: 10,
|
||||
Mutual: false,
|
||||
}},
|
||||
SavedCount: 1,
|
||||
Users: []tg.UserClass{
|
||||
&tg.User{ID: 10, AccessHash: 10, Username: "rustmustdie", Phone: phone},
|
||||
},
|
||||
}).Expect().ThenRPCErr(&tgerr.Error{
|
||||
Code: 1337,
|
||||
Message: "TEST_ERROR",
|
||||
Type: "TEST_ERROR",
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
resolver := plainResolver{raw: raw}
|
||||
|
||||
r, err := resolver.ResolvePhone(ctx, phone)
|
||||
require.NoError(t, err)
|
||||
require.IsType(t, &tg.InputPeerUser{}, r)
|
||||
require.Equal(t, int64(10), r.(*tg.InputPeerUser).UserID)
|
||||
|
||||
_, err = resolver.ResolvePhone(ctx, phone)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
type singleFlight struct {
|
||||
next Resolver
|
||||
sg singleflight.Group
|
||||
}
|
||||
|
||||
func (s *singleFlight) ResolveDomain(ctx context.Context, domain string) (tg.InputPeerClass, error) {
|
||||
ch := s.sg.DoChan(domain, func() (interface{}, error) {
|
||||
return s.next.ResolveDomain(ctx, domain)
|
||||
})
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case r := <-ch:
|
||||
if r.Err != nil {
|
||||
return nil, r.Err
|
||||
}
|
||||
return r.Val.(tg.InputPeerClass), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *singleFlight) ResolvePhone(ctx context.Context, phone string) (tg.InputPeerClass, error) {
|
||||
ch := s.sg.DoChan(phone, func() (interface{}, error) {
|
||||
return s.next.ResolvePhone(ctx, phone)
|
||||
})
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case r := <-ch:
|
||||
if r.Err != nil {
|
||||
return nil, r.Err
|
||||
}
|
||||
return r.Val.(tg.InputPeerClass), nil
|
||||
}
|
||||
}
|
||||
|
||||
// SingleflightResolver is a simple resolver decorator
|
||||
// which prevents duplicate resolve calls.
|
||||
func SingleflightResolver(next Resolver) Resolver {
|
||||
return &singleFlight{next: next}
|
||||
}
|
||||
Reference in New Issue
Block a user