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,269 @@
|
||||
// Package messages contains message iteration helper.
|
||||
package messages
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/message/peer"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// Elem is a message iterator element.
|
||||
type Elem struct {
|
||||
Msg tg.NotEmptyMessage
|
||||
Peer tg.InputPeerClass
|
||||
Entities peer.Entities
|
||||
}
|
||||
|
||||
// Iterator is a message stream iterator.
|
||||
type Iterator struct {
|
||||
// Current state.
|
||||
lastErr error
|
||||
// Buffer state.
|
||||
buf []Elem
|
||||
bufCur int
|
||||
// Request state.
|
||||
addOffset int
|
||||
limit int
|
||||
lastBatch bool
|
||||
// Offset parameters state.
|
||||
offsetID int
|
||||
offsetDate int
|
||||
offsetPeer tg.InputPeerClass
|
||||
offsetRate int
|
||||
// Remote state.
|
||||
count int
|
||||
totalGot bool
|
||||
|
||||
// Query builder.
|
||||
query Query
|
||||
}
|
||||
|
||||
// NewIterator creates new iterator.
|
||||
func NewIterator(query Query, limit int) *Iterator {
|
||||
return &Iterator{
|
||||
buf: make([]Elem, 0, limit),
|
||||
bufCur: -1,
|
||||
limit: limit,
|
||||
query: query,
|
||||
offsetPeer: &tg.InputPeerEmpty{},
|
||||
}
|
||||
}
|
||||
|
||||
// OffsetID sets OffsetID request parameter.
|
||||
func (m *Iterator) OffsetID(offsetID int) *Iterator {
|
||||
m.offsetID = offsetID
|
||||
return m
|
||||
}
|
||||
|
||||
// OffsetDate sets OffsetDate request parameter.
|
||||
func (m *Iterator) OffsetDate(offsetDate int) *Iterator {
|
||||
m.offsetDate = offsetDate
|
||||
return m
|
||||
}
|
||||
|
||||
// OffsetRate sets OffsetRate request parameter.
|
||||
func (m *Iterator) OffsetRate(offsetRate int) *Iterator {
|
||||
m.offsetRate = offsetRate
|
||||
return m
|
||||
}
|
||||
|
||||
// OffsetPeer sets OffsetPeer request parameter.
|
||||
func (m *Iterator) OffsetPeer(offsetPeer tg.InputPeerClass) *Iterator {
|
||||
m.offsetPeer = offsetPeer
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Iterator) apply(r tg.MessagesMessagesClass) error {
|
||||
if m.lastBatch {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
messages tg.MessageClassArray
|
||||
entities peer.Entities
|
||||
)
|
||||
switch msgs := r.(type) {
|
||||
case *tg.MessagesMessages: // messages.messages#8c718e87
|
||||
messages = msgs.Messages
|
||||
entities = peer.EntitiesFromResult(msgs)
|
||||
|
||||
m.count = len(messages)
|
||||
m.lastBatch = true
|
||||
case *tg.MessagesMessagesSlice: // messages.messagesSlice#3a54685e
|
||||
messages = msgs.Messages
|
||||
entities = peer.EntitiesFromResult(msgs)
|
||||
|
||||
m.offsetRate = msgs.NextRate
|
||||
m.count = msgs.Count
|
||||
m.lastBatch = len(msgs.Messages) < m.limit
|
||||
case *tg.MessagesChannelMessages: // messages.channelMessages#64479808
|
||||
messages = msgs.Messages
|
||||
entities = peer.EntitiesFromResult(msgs)
|
||||
|
||||
m.count = msgs.Count
|
||||
m.lastBatch = len(msgs.Messages) < m.limit
|
||||
default: // messages.messagesNotModified#74535f21
|
||||
return errors.Errorf("unexpected type %T", r)
|
||||
}
|
||||
m.totalGot = true
|
||||
|
||||
// Sort messages to guarantee order and find the last message.
|
||||
messages = messages.SortStable(func(a, b tg.MessageClass) bool {
|
||||
return a.GetID() > b.GetID()
|
||||
})
|
||||
|
||||
// Get the last message (with smallest ID).
|
||||
msg, ok := messages.Last()
|
||||
if !ok {
|
||||
// If Last() returned false, result is empty, so we this is a last batch.
|
||||
m.lastBatch = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update offsetID and offsetDate, if can to prevent duplication in case
|
||||
// when there a lot new messages in a chat/channel between previous and current request.
|
||||
//
|
||||
// Illustration of problem:
|
||||
//
|
||||
// Remote state:
|
||||
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
|
||||
// ^ offset = 0
|
||||
//
|
||||
// First request(offset = 0, limit = 5):
|
||||
// [10, 9, 8, 7, 6]
|
||||
// offset = 5
|
||||
//
|
||||
// Remote state:
|
||||
// [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
|
||||
// ^ offset = 5
|
||||
//
|
||||
// Second request(offset = 5, limit = 5):
|
||||
// [10, 9, 8, 7, 6]
|
||||
// offset = 10
|
||||
//
|
||||
m.offsetID = msg.GetID()
|
||||
if nonEmpty, ok := msg.AsNotEmpty(); ok {
|
||||
m.offsetDate = nonEmpty.GetDate()
|
||||
|
||||
p, err := entities.ExtractPeer(nonEmpty.GetPeerID())
|
||||
if err == nil {
|
||||
m.offsetPeer = p
|
||||
}
|
||||
}
|
||||
|
||||
m.bufCur = -1
|
||||
m.buf = m.buf[:0]
|
||||
for _, msg := range messages {
|
||||
nonEmpty, ok := msg.AsNotEmpty()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
msgPeer, err := entities.ExtractPeer(nonEmpty.GetPeerID())
|
||||
if err != nil {
|
||||
msgPeer = &tg.InputPeerEmpty{}
|
||||
}
|
||||
|
||||
m.buf = append(m.buf, Elem{
|
||||
Msg: nonEmpty,
|
||||
Peer: msgPeer,
|
||||
Entities: entities,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Iterator) requestNext(ctx context.Context) error {
|
||||
r, err := m.query.Query(ctx, Request{
|
||||
OffsetID: m.offsetID,
|
||||
AddOffset: m.addOffset,
|
||||
OffsetDate: m.offsetDate,
|
||||
OffsetRate: m.offsetRate,
|
||||
OffsetPeer: m.offsetPeer,
|
||||
Limit: m.limit,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.apply(r)
|
||||
}
|
||||
|
||||
func (m *Iterator) bufNext() bool {
|
||||
if len(m.buf)-1 <= m.bufCur {
|
||||
return false
|
||||
}
|
||||
|
||||
m.bufCur++
|
||||
return true
|
||||
}
|
||||
|
||||
// Total returns last fetched count of elements.
|
||||
// If count was not fetched before, it requests server using FetchTotal.
|
||||
func (m *Iterator) Total(ctx context.Context) (int, error) {
|
||||
if m.totalGot {
|
||||
return m.count, nil
|
||||
}
|
||||
|
||||
return m.FetchTotal(ctx)
|
||||
}
|
||||
|
||||
// FetchTotal fetches and returns count of elements.
|
||||
func (m *Iterator) FetchTotal(ctx context.Context) (int, error) {
|
||||
r, err := m.query.Query(ctx, Request{
|
||||
Limit: 1,
|
||||
OffsetPeer: &tg.InputPeerEmpty{},
|
||||
})
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "fetch total")
|
||||
}
|
||||
|
||||
switch msgs := r.(type) {
|
||||
case *tg.MessagesMessages: // messages.messages#8c718e87
|
||||
m.count = len(msgs.Messages)
|
||||
case *tg.MessagesMessagesSlice: // messages.messagesSlice#3a54685e
|
||||
m.count = msgs.Count
|
||||
case *tg.MessagesChannelMessages: // messages.channelMessages#64479808
|
||||
m.count = msgs.Count
|
||||
default: // messages.messagesNotModified#74535f21
|
||||
return 0, errors.Errorf("unexpected type %T", r)
|
||||
}
|
||||
|
||||
m.totalGot = true
|
||||
return m.count, nil
|
||||
}
|
||||
|
||||
// Next prepares the next message for reading with the Value method.
|
||||
// It returns true on success, or false if there is no next message or an error happened while preparing it.
|
||||
// Err should be consulted to distinguish between the two cases.
|
||||
func (m *Iterator) Next(ctx context.Context) bool {
|
||||
if m.lastErr != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !m.bufNext() {
|
||||
// If buffer is empty, we should fetch next batch.
|
||||
if err := m.requestNext(ctx); err != nil {
|
||||
m.lastErr = err
|
||||
return false
|
||||
}
|
||||
// Try again with new buffer.
|
||||
return m.bufNext()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Value returns current message.
|
||||
func (m *Iterator) Value() Elem {
|
||||
return m.buf[m.bufCur]
|
||||
}
|
||||
|
||||
// Err returns the error, if any, that was encountered during iteration.
|
||||
func (m *Iterator) Err() error {
|
||||
return m.lastErr
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package messages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tgmock"
|
||||
)
|
||||
|
||||
func generateMessages(count int) []tg.MessageClass {
|
||||
r := make([]tg.MessageClass, 0, count)
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
r = append(r, &tg.Message{
|
||||
ID: i,
|
||||
PeerID: &tg.PeerUser{UserID: 10},
|
||||
Message: strconv.Itoa(i),
|
||||
})
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func messagesClass(r []tg.MessageClass, count int) tg.MessagesMessagesClass {
|
||||
return &tg.MessagesChannelMessages{
|
||||
Messages: r,
|
||||
Count: count,
|
||||
}
|
||||
}
|
||||
|
||||
func TestIterator(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mock := tgmock.NewRequire(t)
|
||||
limit := 10
|
||||
totalMessages := 3 * limit
|
||||
expected := generateMessages(totalMessages)
|
||||
raw := tg.NewClient(mock)
|
||||
|
||||
mock.ExpectCall(&tg.MessagesSearchRequest{
|
||||
Q: "query",
|
||||
Peer: &tg.InputPeerSelf{},
|
||||
OffsetID: 0,
|
||||
FromID: &tg.InputPeerEmpty{},
|
||||
Filter: &tg.InputMessagesFilterEmpty{},
|
||||
SavedPeerID: &tg.InputPeerEmpty{},
|
||||
Limit: limit,
|
||||
}).ThenResult(messagesClass(expected[2*limit:3*limit], totalMessages))
|
||||
mock.ExpectCall(&tg.MessagesSearchRequest{
|
||||
Q: "query",
|
||||
Peer: &tg.InputPeerSelf{},
|
||||
OffsetID: 20,
|
||||
FromID: &tg.InputPeerEmpty{},
|
||||
Filter: &tg.InputMessagesFilterEmpty{},
|
||||
SavedPeerID: &tg.InputPeerEmpty{},
|
||||
Limit: limit,
|
||||
}).ThenResult(messagesClass(expected[limit:2*limit], totalMessages))
|
||||
mock.ExpectCall(&tg.MessagesSearchRequest{
|
||||
Q: "query",
|
||||
Peer: &tg.InputPeerSelf{},
|
||||
OffsetID: 10,
|
||||
FromID: &tg.InputPeerEmpty{},
|
||||
Filter: &tg.InputMessagesFilterEmpty{},
|
||||
SavedPeerID: &tg.InputPeerEmpty{},
|
||||
Limit: limit,
|
||||
}).ThenResult(messagesClass(expected[:limit], totalMessages))
|
||||
mock.ExpectCall(&tg.MessagesSearchRequest{
|
||||
Q: "query",
|
||||
Peer: &tg.InputPeerSelf{},
|
||||
OffsetID: 0,
|
||||
FromID: &tg.InputPeerEmpty{},
|
||||
Filter: &tg.InputMessagesFilterEmpty{},
|
||||
SavedPeerID: &tg.InputPeerEmpty{},
|
||||
Limit: limit,
|
||||
}).ThenResult(messagesClass(expected[:0], totalMessages))
|
||||
|
||||
iter := NewQueryBuilder(raw).Search(&tg.InputPeerSelf{}).
|
||||
Filter(&tg.InputMessagesFilterEmpty{}).
|
||||
Q("query").BatchSize(10).Iter()
|
||||
i := 0
|
||||
for iter.Next(ctx) {
|
||||
require.Equal(t, expected[len(expected)-i-1], iter.Value().Msg)
|
||||
i++
|
||||
}
|
||||
require.NoError(t, iter.Err())
|
||||
require.Equal(t, totalMessages, i)
|
||||
|
||||
total, err := iter.Total(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, totalMessages, total)
|
||||
|
||||
mock.ExpectCall(&tg.MessagesSearchRequest{
|
||||
Q: "query",
|
||||
Peer: &tg.InputPeerSelf{},
|
||||
OffsetID: 0,
|
||||
FromID: &tg.InputPeerEmpty{},
|
||||
Filter: &tg.InputMessagesFilterEmpty{},
|
||||
SavedPeerID: &tg.InputPeerEmpty{},
|
||||
Limit: 1,
|
||||
}).ThenResult(messagesClass(expected[:0], totalMessages))
|
||||
total, err = iter.FetchTotal(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, totalMessages, total)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
package messages
|
||||
|
||||
//go:generate go run go.mau.fi/mautrix-telegram/pkg/gotd/telegram/query/internal/itergen -out=queries.gen.go
|
||||
@@ -0,0 +1,177 @@
|
||||
package messages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// Document returns document object if message has a document attachment (video, voice, audio,
|
||||
// basically every type except photo).
|
||||
func (e Elem) Document() (*tg.Document, bool) {
|
||||
msg, ok := e.Msg.(*tg.Message)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
media, ok := msg.Media.(*tg.MessageMediaDocument)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return media.Document.AsNotEmpty()
|
||||
}
|
||||
|
||||
// Photo returns photo object if message has a photo attachment.
|
||||
func (e Elem) Photo() (*tg.Photo, bool) {
|
||||
msg, ok := e.Msg.(*tg.Message)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
media, ok := msg.Media.(*tg.MessageMediaPhoto)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return media.Photo.AsNotEmpty()
|
||||
}
|
||||
|
||||
// File represents file attachment.
|
||||
type File struct {
|
||||
Name string
|
||||
MIMEType string
|
||||
Location tg.InputFileLocationClass
|
||||
}
|
||||
|
||||
const dateLayout = "2006-01-02_15-04-05"
|
||||
|
||||
func getDocFilename(doc *tg.Document) string {
|
||||
var filename, ext string
|
||||
for _, attr := range doc.Attributes {
|
||||
switch v := attr.(type) {
|
||||
case *tg.DocumentAttributeImageSize:
|
||||
switch doc.MimeType {
|
||||
case "image/png":
|
||||
ext = ".png"
|
||||
case "image/webp":
|
||||
ext = ".webp"
|
||||
case "image/tiff":
|
||||
ext = ".tif"
|
||||
default:
|
||||
ext = ".jpg"
|
||||
}
|
||||
case *tg.DocumentAttributeAnimated:
|
||||
ext = ".gif"
|
||||
case *tg.DocumentAttributeSticker:
|
||||
ext = ".webp"
|
||||
case *tg.DocumentAttributeVideo:
|
||||
switch doc.MimeType {
|
||||
case "video/mpeg":
|
||||
ext = ".mpeg"
|
||||
case "video/webm":
|
||||
ext = ".webm"
|
||||
case "video/ogg":
|
||||
ext = ".ogg"
|
||||
default:
|
||||
ext = ".mp4"
|
||||
}
|
||||
case *tg.DocumentAttributeAudio:
|
||||
switch doc.MimeType {
|
||||
case "audio/webm":
|
||||
ext = ".webm"
|
||||
case "audio/aac":
|
||||
ext = ".aac"
|
||||
case "audio/ogg":
|
||||
ext = ".ogg"
|
||||
default:
|
||||
ext = ".mp3"
|
||||
}
|
||||
case *tg.DocumentAttributeFilename:
|
||||
filename = v.FileName
|
||||
}
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
filename = fmt.Sprintf(
|
||||
"doc%d_%s%s", doc.GetID(),
|
||||
time.Unix(int64(doc.Date), 0).Format(dateLayout),
|
||||
ext,
|
||||
)
|
||||
}
|
||||
|
||||
return filename
|
||||
}
|
||||
|
||||
type sizedPhoto interface {
|
||||
GetW() int
|
||||
GetH() int
|
||||
GetType() string
|
||||
}
|
||||
|
||||
var (
|
||||
_ sizedPhoto = (*tg.PhotoSize)(nil)
|
||||
_ sizedPhoto = (*tg.PhotoCachedSize)(nil)
|
||||
_ sizedPhoto = (*tg.PhotoSizeProgressive)(nil)
|
||||
)
|
||||
|
||||
// File returns file location if message has a file attachment.
|
||||
func (e Elem) File() (File, bool) {
|
||||
msg, ok := e.Msg.(*tg.Message)
|
||||
if !ok {
|
||||
return File{}, false
|
||||
}
|
||||
|
||||
switch media := msg.Media.(type) {
|
||||
case *tg.MessageMediaPhoto:
|
||||
photo, ok := media.Photo.AsNotEmpty()
|
||||
if !ok {
|
||||
return File{}, false
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf(
|
||||
"photo%d_%s.jpg", photo.GetID(),
|
||||
time.Unix(int64(photo.Date), 0).Format(dateLayout),
|
||||
)
|
||||
|
||||
var (
|
||||
thumbSize string
|
||||
maxW, maxH int
|
||||
)
|
||||
for _, g := range photo.Sizes {
|
||||
// TODO(tdakkota): add helpers to choose photo size.
|
||||
if sz, ok := g.(sizedPhoto); ok && maxW < sz.GetW() && maxH < sz.GetH() {
|
||||
thumbSize = sz.GetType()
|
||||
}
|
||||
}
|
||||
|
||||
if thumbSize == "" {
|
||||
return File{}, false
|
||||
}
|
||||
|
||||
return File{
|
||||
Name: filename,
|
||||
MIMEType: "image/jpeg",
|
||||
Location: &tg.InputPhotoFileLocation{
|
||||
ID: photo.ID,
|
||||
AccessHash: photo.AccessHash,
|
||||
FileReference: photo.FileReference,
|
||||
ThumbSize: thumbSize,
|
||||
},
|
||||
}, true
|
||||
case *tg.MessageMediaDocument:
|
||||
doc, ok := media.Document.AsNotEmpty()
|
||||
if !ok {
|
||||
return File{}, false
|
||||
}
|
||||
|
||||
return File{
|
||||
Name: getDocFilename(doc),
|
||||
MIMEType: doc.MimeType,
|
||||
Location: doc.AsInputDocumentFileLocation(),
|
||||
}, true
|
||||
default:
|
||||
return File{}, false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package messages
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
func Test_getDocFilename(t *testing.T) {
|
||||
date := time.Now()
|
||||
f := date.Format(dateLayout)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args *tg.Document
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"Doc",
|
||||
&tg.Document{
|
||||
Date: int(date.Unix()),
|
||||
Attributes: []tg.DocumentAttributeClass{
|
||||
&tg.DocumentAttributeFilename{FileName: "10.jpg"},
|
||||
},
|
||||
},
|
||||
"10.jpg",
|
||||
},
|
||||
{
|
||||
"Gif",
|
||||
&tg.Document{
|
||||
Date: int(date.Unix()),
|
||||
Attributes: []tg.DocumentAttributeClass{
|
||||
&tg.DocumentAttributeAnimated{},
|
||||
},
|
||||
},
|
||||
"doc0_" + f + ".gif",
|
||||
},
|
||||
{
|
||||
"Video",
|
||||
&tg.Document{
|
||||
Date: int(date.Unix()),
|
||||
Attributes: []tg.DocumentAttributeClass{
|
||||
&tg.DocumentAttributeVideo{},
|
||||
},
|
||||
},
|
||||
"doc0_" + f + ".mp4",
|
||||
},
|
||||
{
|
||||
"Photo",
|
||||
&tg.Document{
|
||||
Date: int(date.Unix()),
|
||||
Attributes: []tg.DocumentAttributeClass{
|
||||
&tg.DocumentAttributeImageSize{},
|
||||
},
|
||||
},
|
||||
"doc0_" + f + ".jpg",
|
||||
},
|
||||
{
|
||||
"Audio",
|
||||
&tg.Document{
|
||||
Date: int(date.Unix()),
|
||||
Attributes: []tg.DocumentAttributeClass{
|
||||
&tg.DocumentAttributeAudio{},
|
||||
},
|
||||
},
|
||||
"doc0_" + f + ".mp3",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.want, getDocFilename(tt.args))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestElem_File(t *testing.T) {
|
||||
type results struct {
|
||||
file, doc, photo bool
|
||||
}
|
||||
tests := []struct {
|
||||
Name string
|
||||
Msg tg.NotEmptyMessage
|
||||
results
|
||||
}{
|
||||
{"EmptyMessage", &tg.Message{}, results{}},
|
||||
{"ServiceMessage", &tg.MessageService{}, results{}},
|
||||
{"EmptyPhoto", &tg.Message{
|
||||
Media: &tg.MessageMediaPhoto{
|
||||
Photo: &tg.PhotoEmpty{},
|
||||
},
|
||||
}, results{}},
|
||||
{"EmptyDoc", &tg.Message{
|
||||
Media: &tg.MessageMediaDocument{
|
||||
Document: &tg.DocumentEmpty{},
|
||||
},
|
||||
}, results{}},
|
||||
{"Photo", &tg.Message{
|
||||
Media: &tg.MessageMediaPhoto{
|
||||
Photo: &tg.Photo{
|
||||
Sizes: []tg.PhotoSizeClass{
|
||||
&tg.PhotoSize{
|
||||
Type: "cock",
|
||||
W: 10,
|
||||
H: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, results{file: true, photo: true}},
|
||||
{"Document", &tg.Message{
|
||||
Media: &tg.MessageMediaDocument{
|
||||
Document: &tg.Document{},
|
||||
},
|
||||
}, results{file: true, doc: true}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
a := require.New(t)
|
||||
var ok bool
|
||||
|
||||
elem := Elem{Msg: test.Msg}
|
||||
_, ok = elem.File()
|
||||
a.Equal(test.file, ok)
|
||||
_, ok = elem.Document()
|
||||
a.Equal(test.doc, ok)
|
||||
_, ok = elem.Photo()
|
||||
a.Equal(test.photo, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Package featured contains featured stickers iteration helper.
|
||||
package featured
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// Elem is a sticker iterator element.
|
||||
type Elem struct {
|
||||
Sticker tg.StickerSetCoveredClass
|
||||
// IDs of new featured stickersets
|
||||
Unread []int64
|
||||
}
|
||||
|
||||
// Iterator is a featured stickers stream iterator.
|
||||
type Iterator struct {
|
||||
// Current state.
|
||||
lastErr error
|
||||
// Buffer state.
|
||||
buf []Elem
|
||||
bufCur int
|
||||
// Request state.
|
||||
limit int
|
||||
lastBatch bool
|
||||
// Offset parameters state.
|
||||
offset int
|
||||
// Remote state.
|
||||
count int
|
||||
totalGot bool
|
||||
|
||||
// Query builder.
|
||||
query Query
|
||||
}
|
||||
|
||||
// NewIterator creates new iterator.
|
||||
func NewIterator(query Query, limit int) *Iterator {
|
||||
return &Iterator{
|
||||
buf: make([]Elem, 0, limit),
|
||||
bufCur: -1,
|
||||
limit: limit,
|
||||
query: query,
|
||||
}
|
||||
}
|
||||
|
||||
// Offset sets Offset request parameter.
|
||||
func (m *Iterator) Offset(offset int) *Iterator {
|
||||
m.offset = offset
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Iterator) apply(r tg.MessagesFeaturedStickersClass) error {
|
||||
if m.lastBatch {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
stickers []tg.StickerSetCoveredClass
|
||||
unread []int64
|
||||
)
|
||||
switch stks := r.(type) {
|
||||
case *tg.MessagesFeaturedStickers: // messages.featuredStickers#b6abc341
|
||||
stickers = stks.Sets
|
||||
unread = stks.Unread
|
||||
|
||||
m.count = stks.Count
|
||||
m.lastBatch = len(stickers) < m.limit
|
||||
default:
|
||||
return errors.Errorf("unexpected type %T", r)
|
||||
}
|
||||
m.totalGot = true
|
||||
m.offset += len(stickers)
|
||||
|
||||
m.bufCur = -1
|
||||
m.buf = m.buf[:0]
|
||||
for i := range stickers {
|
||||
m.buf = append(m.buf, Elem{Sticker: stickers[i], Unread: unread})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Iterator) requestNext(ctx context.Context) error {
|
||||
r, err := m.query.Query(ctx, Request{
|
||||
Offset: m.offset,
|
||||
Limit: m.limit,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.apply(r)
|
||||
}
|
||||
|
||||
func (m *Iterator) bufNext() bool {
|
||||
if len(m.buf)-1 <= m.bufCur {
|
||||
return false
|
||||
}
|
||||
|
||||
m.bufCur++
|
||||
return true
|
||||
}
|
||||
|
||||
// Total returns last fetched count of elements.
|
||||
// If count was not fetched before, it requests server using FetchTotal.
|
||||
func (m *Iterator) Total(ctx context.Context) (int, error) {
|
||||
if m.totalGot {
|
||||
return m.count, nil
|
||||
}
|
||||
|
||||
return m.FetchTotal(ctx)
|
||||
}
|
||||
|
||||
// FetchTotal fetches and returns count of elements.
|
||||
func (m *Iterator) FetchTotal(ctx context.Context) (int, error) {
|
||||
r, err := m.query.Query(ctx, Request{
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "fetch total")
|
||||
}
|
||||
|
||||
switch stks := r.(type) {
|
||||
case *tg.MessagesFeaturedStickers: // messages.featuredStickers#b6abc341
|
||||
m.count = stks.Count
|
||||
default:
|
||||
return 0, errors.Errorf("unexpected type %T", r)
|
||||
}
|
||||
|
||||
m.totalGot = true
|
||||
return m.count, nil
|
||||
}
|
||||
|
||||
// Next prepares the next message for reading with the Value method.
|
||||
// It returns true on success, or false if there is no next message or an error happened while preparing it.
|
||||
// Err should be consulted to distinguish between the two cases.
|
||||
func (m *Iterator) Next(ctx context.Context) bool {
|
||||
if m.lastErr != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !m.bufNext() {
|
||||
// If buffer is empty, we should fetch next batch.
|
||||
if err := m.requestNext(ctx); err != nil {
|
||||
m.lastErr = err
|
||||
return false
|
||||
}
|
||||
// Try again with new buffer.
|
||||
return m.bufNext()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Value returns current message.
|
||||
func (m *Iterator) Value() Elem {
|
||||
return m.buf[m.bufCur]
|
||||
}
|
||||
|
||||
// Err returns the error, if any, that was encountered during iteration.
|
||||
func (m *Iterator) Err() error {
|
||||
return m.lastErr
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package featured
|
||||
|
||||
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 generateStickers(count int) []tg.StickerSetCoveredClass {
|
||||
r := make([]tg.StickerSetCoveredClass, 0, count)
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
r = append(r, &tg.StickerSetCovered{
|
||||
Set: tg.StickerSet{
|
||||
ID: int64(i + 1),
|
||||
AccessHash: int64(i + 1),
|
||||
},
|
||||
Cover: &tg.Document{
|
||||
ID: int64(i + 1),
|
||||
AccessHash: int64(i + 1),
|
||||
FileReference: []uint8{uint8(i)},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func result(r []tg.StickerSetCoveredClass, count int) tg.MessagesFeaturedStickersClass {
|
||||
return &tg.MessagesFeaturedStickers{
|
||||
Sets: r,
|
||||
Count: count,
|
||||
}
|
||||
}
|
||||
|
||||
func TestIterator(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mock := tgmock.NewRequire(t)
|
||||
limit := 10
|
||||
totalRecords := 3 * limit
|
||||
expected := generateStickers(totalRecords)
|
||||
raw := tg.NewClient(mock)
|
||||
|
||||
mock.ExpectCall(&tg.MessagesGetOldFeaturedStickersRequest{
|
||||
Offset: 0,
|
||||
Limit: limit,
|
||||
}).ThenResult(result(expected[0:limit], totalRecords))
|
||||
mock.ExpectCall(&tg.MessagesGetOldFeaturedStickersRequest{
|
||||
Offset: limit,
|
||||
Limit: limit,
|
||||
}).ThenResult(result(expected[limit:2*limit], totalRecords))
|
||||
mock.ExpectCall(&tg.MessagesGetOldFeaturedStickersRequest{
|
||||
Offset: 2 * limit,
|
||||
Limit: limit,
|
||||
}).ThenResult(result(expected[2*limit:3*limit], totalRecords))
|
||||
mock.ExpectCall(&tg.MessagesGetOldFeaturedStickersRequest{
|
||||
Offset: 3 * limit,
|
||||
Limit: limit,
|
||||
}).ThenResult(result(expected[3*limit:], totalRecords))
|
||||
|
||||
iter := NewQueryBuilder(raw).GetOldFeaturedStickers().BatchSize(10).Iter()
|
||||
i := 0
|
||||
for iter.Next(ctx) {
|
||||
require.Equal(t, expected[i], iter.Value().Sticker)
|
||||
i++
|
||||
}
|
||||
require.NoError(t, iter.Err())
|
||||
require.Equal(t, totalRecords, i)
|
||||
|
||||
total, err := iter.Total(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, totalRecords, total)
|
||||
|
||||
mock.ExpectCall(&tg.MessagesGetOldFeaturedStickersRequest{
|
||||
Offset: 0,
|
||||
Limit: 1,
|
||||
}).ThenResult(result(expected[:0], totalRecords))
|
||||
total, err = iter.FetchTotal(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, totalRecords, total)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Code generated by itergen, DO NOT EDIT.
|
||||
|
||||
package featured
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// No-op definition for keeping imports.
|
||||
var _ = context.Background()
|
||||
|
||||
// Request is a parameter for Query.
|
||||
type Request struct {
|
||||
Offset int
|
||||
Limit int
|
||||
}
|
||||
|
||||
// Query is an abstraction for featured request.
|
||||
// NB: iterator mutates returned data (sorts, at least).
|
||||
type Query interface {
|
||||
Query(ctx context.Context, req Request) (tg.MessagesFeaturedStickersClass, error)
|
||||
}
|
||||
|
||||
// QueryFunc is a function adapter for Query.
|
||||
type QueryFunc func(ctx context.Context, req Request) (tg.MessagesFeaturedStickersClass, error)
|
||||
|
||||
// Query implements Query interface.
|
||||
func (q QueryFunc) Query(ctx context.Context, req Request) (tg.MessagesFeaturedStickersClass, error) {
|
||||
return q(ctx, req)
|
||||
}
|
||||
|
||||
// QueryBuilder is a helper to create message queries.
|
||||
type QueryBuilder struct {
|
||||
raw *tg.Client
|
||||
}
|
||||
|
||||
// NewQueryBuilder creates new QueryBuilder.
|
||||
func NewQueryBuilder(raw *tg.Client) *QueryBuilder {
|
||||
return &QueryBuilder{raw: raw}
|
||||
}
|
||||
|
||||
// GetOldFeaturedStickersQueryBuilder is query builder of MessagesGetOldFeaturedStickers.
|
||||
type GetOldFeaturedStickersQueryBuilder struct {
|
||||
raw *tg.Client
|
||||
req tg.MessagesGetOldFeaturedStickersRequest
|
||||
batchSize int
|
||||
offset int
|
||||
}
|
||||
|
||||
// GetOldFeaturedStickers creates query builder of MessagesGetOldFeaturedStickers.
|
||||
func (q *QueryBuilder) GetOldFeaturedStickers() *GetOldFeaturedStickersQueryBuilder {
|
||||
b := &GetOldFeaturedStickersQueryBuilder{
|
||||
raw: q.raw,
|
||||
batchSize: 1,
|
||||
req: tg.MessagesGetOldFeaturedStickersRequest{},
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// BatchSize sets buffer of message loaded from one request.
|
||||
// Be carefully, when set this limit, because Telegram does not return error if limit is too big,
|
||||
// so results can be incorrect.
|
||||
func (b *GetOldFeaturedStickersQueryBuilder) BatchSize(batchSize int) *GetOldFeaturedStickersQueryBuilder {
|
||||
b.batchSize = batchSize
|
||||
return b
|
||||
}
|
||||
|
||||
// Query implements Query interface.
|
||||
func (b *GetOldFeaturedStickersQueryBuilder) Query(ctx context.Context, req Request) (tg.MessagesFeaturedStickersClass, error) {
|
||||
r := &tg.MessagesGetOldFeaturedStickersRequest{
|
||||
Limit: req.Limit,
|
||||
}
|
||||
|
||||
r.Offset = req.Offset
|
||||
return b.raw.MessagesGetOldFeaturedStickers(ctx, r)
|
||||
}
|
||||
|
||||
// Iter returns iterator using built query.
|
||||
func (b *GetOldFeaturedStickersQueryBuilder) Iter() *Iterator {
|
||||
iter := NewIterator(b, b.batchSize)
|
||||
iter = iter.Offset(b.offset)
|
||||
return iter
|
||||
}
|
||||
|
||||
// ForEach calls given callback on each iterator element.
|
||||
func (b *GetOldFeaturedStickersQueryBuilder) ForEach(ctx context.Context, cb func(context.Context, Elem) error) error {
|
||||
iter := b.Iter()
|
||||
for iter.Next(ctx) {
|
||||
if err := cb(ctx, iter.Value()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return iter.Err()
|
||||
}
|
||||
|
||||
// Count fetches remote state to get number of elements.
|
||||
func (b *GetOldFeaturedStickersQueryBuilder) Count(ctx context.Context) (int, error) {
|
||||
iter := b.Iter()
|
||||
c, err := iter.Total(ctx)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "get total")
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Collect creates iterator and collects all elements to slice.
|
||||
func (b *GetOldFeaturedStickersQueryBuilder) Collect(ctx context.Context) ([]Elem, error) {
|
||||
iter := b.Iter()
|
||||
c, err := iter.Total(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get total")
|
||||
}
|
||||
|
||||
r := make([]Elem, 0, c)
|
||||
for iter.Next(ctx) {
|
||||
r = append(r, iter.Value())
|
||||
}
|
||||
|
||||
return r, iter.Err()
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package featured
|
||||
|
||||
//go:generate go run go.mau.fi/mautrix-telegram/pkg/gotd/telegram/query/internal/itergen -result=MessagesFeaturedStickersClass -package=featured -out=queries.gen.go
|
||||
Reference in New Issue
Block a user