2346 lines
81 KiB
Go
2346 lines
81 KiB
Go
package rpc
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/iamxvbaba/td/clock"
|
|
"github.com/iamxvbaba/td/tg"
|
|
"github.com/iamxvbaba/td/tgerr"
|
|
"go.uber.org/zap/zaptest"
|
|
|
|
botsapp "telesrv/internal/app/bots"
|
|
appchannels "telesrv/internal/app/channels"
|
|
appmessages "telesrv/internal/app/messages"
|
|
appusers "telesrv/internal/app/users"
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store/memory"
|
|
)
|
|
|
|
type inlineBotRPCTestFixture struct {
|
|
router *Router
|
|
bots *botsapp.Service
|
|
channels *appchannels.Service
|
|
files *fakeFiles
|
|
owner domain.User
|
|
peer domain.User
|
|
bot domain.User
|
|
photo domain.Photo
|
|
document domain.Document
|
|
}
|
|
|
|
func newInlineBotRPCTestFixture(t *testing.T) inlineBotRPCTestFixture {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
botStore := memory.NewBotStore(users)
|
|
dialogs := memory.NewDialogStore()
|
|
messageStore := memory.NewMessageStore(dialogs)
|
|
bots := botsapp.NewService(users, botStore, messageStore)
|
|
messages := appmessages.NewService(messageStore, dialogs)
|
|
channelStore := memory.NewChannelStore()
|
|
channels := appchannels.NewService(channelStore, appchannels.WithBotProfileResolver(bots))
|
|
files := &fakeFiles{
|
|
docs: map[int64]domain.Document{},
|
|
photos: map[int64]domain.Photo{},
|
|
}
|
|
photo := files.putPhoto(domain.Photo{
|
|
ID: 8101,
|
|
AccessHash: 810101,
|
|
DCID: 2,
|
|
Sizes: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "x", W: 640, H: 480, Size: 1234}},
|
|
})
|
|
document := domain.Document{
|
|
ID: 8201,
|
|
AccessHash: 820101,
|
|
DCID: 2,
|
|
MimeType: "application/pdf",
|
|
Size: 4567,
|
|
Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrFilename, FileName: "inline.pdf"}},
|
|
Thumbs: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "m", W: 160, H: 120, Size: 321}},
|
|
}
|
|
files.docs[document.ID] = document
|
|
owner, err := users.Create(ctx, domain.User{AccessHash: 7101, Phone: "15550007101", FirstName: "Owner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
peer, err := users.Create(ctx, domain.User{AccessHash: 7102, Phone: "15550007102", FirstName: "Peer"})
|
|
if err != nil {
|
|
t.Fatalf("create peer: %v", err)
|
|
}
|
|
bot, _, err := bots.CreateBot(ctx, owner.ID, "Inline Bot", "inline_shape_bot")
|
|
if err != nil {
|
|
t.Fatalf("create bot: %v", err)
|
|
}
|
|
if _, err := bots.SetInlinePlaceholder(ctx, bot.ID, "Search inline"); err != nil {
|
|
t.Fatalf("set inline placeholder: %v", err)
|
|
}
|
|
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
|
Users: appusers.NewService(users),
|
|
Bots: bots,
|
|
Messages: messages,
|
|
Channels: channels,
|
|
Files: files,
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
return inlineBotRPCTestFixture{router: r, bots: bots, channels: channels, files: files, owner: owner, peer: peer, bot: bot, photo: photo, document: document}
|
|
}
|
|
|
|
func TestInlineBotArticleTextPrivateRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
type getResult struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}
|
|
gotCh := make(chan getResult, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "hello",
|
|
})
|
|
gotCh <- getResult{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 30,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
&tg.InputBotInlineResult{
|
|
ID: "article-1",
|
|
Type: "article",
|
|
Title: "Echo",
|
|
Description: "Echo text",
|
|
SendMessage: &tg.InputBotInlineMessageText{Message: "inline hello"},
|
|
},
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set inline results = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
var got getResult
|
|
select {
|
|
case got = <-gotCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("get inline results did not resolve")
|
|
}
|
|
if got.err != nil {
|
|
t.Fatalf("get inline results: %v", got.err)
|
|
}
|
|
if got.res.QueryID != queryID || len(got.res.Results) != 1 {
|
|
t.Fatalf("inline results = query %d len %d, want query %d len 1", got.res.QueryID, len(got.res.Results), queryID)
|
|
}
|
|
if len(got.res.Users) != 1 {
|
|
t.Fatalf("inline result users = %d, want bot user", len(got.res.Users))
|
|
}
|
|
botUser, ok := got.res.Users[0].(*tg.User)
|
|
if !ok || botUser.ID != f.bot.ID || !botUser.Bot {
|
|
t.Fatalf("inline result user = %#v, want bot user %d", got.res.Users[0], f.bot.ID)
|
|
}
|
|
if placeholder, ok := botUser.GetBotInlinePlaceholder(); !ok || placeholder != "Search inline" {
|
|
t.Fatalf("bot inline placeholder = %q,%v, want Search inline,true", placeholder, ok)
|
|
}
|
|
|
|
updatesClass, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: inputPeerUser(f.peer),
|
|
RandomID: 9901,
|
|
QueryID: queryID,
|
|
ID: "article-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send inline bot result: %v", err)
|
|
}
|
|
sent := messageFromUpdates(t, updatesClass)
|
|
if via, ok := sent.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("sent via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
if sent.Message != "inline hello" {
|
|
t.Fatalf("sent message = %q, want inline hello", sent.Message)
|
|
}
|
|
|
|
historyList, err := f.router.deps.Messages.GetHistory(ownerCtx, f.owner.ID, domain.MessageFilter{
|
|
HasPeer: true,
|
|
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID},
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get history: %v", err)
|
|
}
|
|
history := tgMessagesMessages(f.owner.ID, f.router.enrichMessageList(ownerCtx, f.owner.ID, historyList))
|
|
historyMessages, ok := history.(*tg.MessagesMessages)
|
|
if !ok || len(historyMessages.Messages) != 1 {
|
|
t.Fatalf("history = %T len %d, want messagesMessages len 1", history, len(messagesFromClass(history)))
|
|
}
|
|
histMsg, ok := historyMessages.Messages[0].(*tg.Message)
|
|
if !ok {
|
|
t.Fatalf("history message = %T, want *tg.Message", historyMessages.Messages[0])
|
|
}
|
|
if via, ok := histMsg.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("history via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
foundBot := false
|
|
for _, u := range historyMessages.Users {
|
|
if user, ok := u.(*tg.User); ok && user.ID == f.bot.ID && user.Bot {
|
|
foundBot = true
|
|
}
|
|
}
|
|
if !foundBot {
|
|
t.Fatalf("history users missing via bot: %+v", historyMessages.Users)
|
|
}
|
|
}
|
|
|
|
func TestInlineBotSendKeepsQueryPeerBinding(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
queryID, _ := f.router.inlines.register(f.router.clock.Now(), f.bot.ID, f.owner.ID, domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID})
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineArticleResult("article-locked", "locked"),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set inline results = %v,%v, want true,nil", ok, err)
|
|
}
|
|
if _, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: inputPeerUser(f.owner),
|
|
RandomID: 9902,
|
|
QueryID: queryID,
|
|
ID: "article-locked",
|
|
}); !tgerr.Is(err, "PEER_ID_INVALID") {
|
|
t.Fatalf("send inline to different peer err = %v, want PEER_ID_INVALID", err)
|
|
}
|
|
}
|
|
|
|
func TestInlineBotArticleTextExternalThumbRoundTripAndCache(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
type getResult struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}
|
|
req := &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "preview",
|
|
}
|
|
gotCh := make(chan getResult, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, req)
|
|
gotCh <- getResult{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 30,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineArticleResultWithWebThumb("article-web", "preview text"),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set inline results = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
var got getResult
|
|
select {
|
|
case got = <-gotCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("get inline results did not resolve")
|
|
}
|
|
if got.err != nil {
|
|
t.Fatalf("get inline results: %v", got.err)
|
|
}
|
|
assertInlineArticleWebPreview(t, got.res, queryID)
|
|
|
|
updatesClass, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: inputPeerUser(f.peer),
|
|
RandomID: 9911,
|
|
QueryID: queryID,
|
|
ID: "article-web",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send inline bot result: %v", err)
|
|
}
|
|
sent := messageFromUpdates(t, updatesClass)
|
|
if sent.Message != "preview text" {
|
|
t.Fatalf("sent message = %q, want preview text", sent.Message)
|
|
}
|
|
if sent.Media != nil {
|
|
t.Fatalf("sent media = %T, want nil for text article", sent.Media)
|
|
}
|
|
if via, ok := sent.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("sent via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
|
|
cached, err := f.router.onMessagesGetInlineBotResults(ownerCtx, req)
|
|
if err != nil {
|
|
t.Fatalf("cached get inline results: %v", err)
|
|
}
|
|
if cached.QueryID == queryID {
|
|
t.Fatalf("cached query_id reused %d, want fresh id", cached.QueryID)
|
|
}
|
|
assertInlineArticleWebPreview(t, cached, cached.QueryID)
|
|
if f.router.inlines.unansweredSize() != 0 {
|
|
t.Fatalf("cache hit left unanswered pending queries = %d, want 0", f.router.inlines.unansweredSize())
|
|
}
|
|
}
|
|
|
|
func TestInlineBotArticleTextExternalContentWebFileDownload(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
oldFetch := fetchInlineWebFile
|
|
fetches := 0
|
|
fetchInlineWebFile = func(_ context.Context, document domain.BotInlineWebDocument) ([]byte, string, error) {
|
|
fetches++
|
|
if document.URL != "https://example.test/content.png" || document.AccessHash == 0 {
|
|
t.Fatalf("fetch document = %+v", document)
|
|
}
|
|
return []byte("abcdef"), "image/png; charset=binary", nil
|
|
}
|
|
defer func() { fetchInlineWebFile = oldFetch }()
|
|
|
|
type getResult struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}
|
|
req := &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "content",
|
|
}
|
|
gotCh := make(chan getResult, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, req)
|
|
gotCh <- getResult{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 30,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineArticleResultWithWebContent("article-content", "content text"),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set inline results = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
var got getResult
|
|
select {
|
|
case got = <-gotCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("get inline results did not resolve")
|
|
}
|
|
if got.err != nil {
|
|
t.Fatalf("get inline results: %v", got.err)
|
|
}
|
|
result, ok := got.res.Results[0].(*tg.BotInlineResult)
|
|
if !ok {
|
|
t.Fatalf("inline result = %T, want *tg.BotInlineResult", got.res.Results[0])
|
|
}
|
|
rawContent, ok := result.GetContent()
|
|
if !ok {
|
|
t.Fatal("inline result missing content")
|
|
}
|
|
content, ok := rawContent.(*tg.WebDocument)
|
|
if !ok {
|
|
t.Fatalf("inline content = %T, want *tg.WebDocument", rawContent)
|
|
}
|
|
if content.URL != "https://example.test/content.png" || content.AccessHash == 0 || content.MimeType != "image/png" {
|
|
t.Fatalf("content = %+v", content)
|
|
}
|
|
file, err := f.router.onUploadGetWebFile(ownerCtx, &tg.UploadGetWebFileRequest{
|
|
Location: &tg.InputWebFileLocation{URL: content.URL, AccessHash: content.AccessHash},
|
|
Offset: 2,
|
|
Limit: 3,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("upload.getWebFile content: %v", err)
|
|
}
|
|
if string(file.Bytes) != "cde" || file.Size != 6 || file.MimeType != "image/png" {
|
|
t.Fatalf("webfile = size %d mime %q bytes %q", file.Size, file.MimeType, file.Bytes)
|
|
}
|
|
file, err = f.router.onUploadGetWebFile(ownerCtx, &tg.UploadGetWebFileRequest{
|
|
Location: &tg.InputWebFileLocation{URL: content.URL, AccessHash: content.AccessHash},
|
|
Offset: 0,
|
|
Limit: 2,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("upload.getWebFile cached content: %v", err)
|
|
}
|
|
if string(file.Bytes) != "ab" || fetches != 1 {
|
|
t.Fatalf("cached webfile bytes=%q fetches=%d, want ab and one fetch", file.Bytes, fetches)
|
|
}
|
|
if _, err := f.router.onUploadGetWebFile(ownerCtx, &tg.UploadGetWebFileRequest{
|
|
Location: &tg.InputWebFileLocation{URL: content.URL, AccessHash: content.AccessHash + 1},
|
|
Offset: 0,
|
|
Limit: 2,
|
|
}); err == nil || !strings.Contains(err.Error(), "LOCATION_INVALID") {
|
|
t.Fatalf("bad access_hash err = %v, want LOCATION_INVALID", err)
|
|
}
|
|
if _, err := f.router.onUploadGetWebFile(ownerCtx, &tg.UploadGetWebFileRequest{
|
|
Location: &tg.InputWebFileLocation{URL: "https://example.test/other.png", AccessHash: content.AccessHash},
|
|
Offset: 0,
|
|
Limit: 2,
|
|
}); err == nil || !strings.Contains(err.Error(), "LOCATION_INVALID") {
|
|
t.Fatalf("bad url err = %v, want LOCATION_INVALID", err)
|
|
}
|
|
}
|
|
|
|
func TestInlineBotExternalPhotoContentMediaAutoRoundTripAndCache(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
oldFetch := fetchInlineWebFile
|
|
fetches := 0
|
|
body := []byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A, 1, 2, 3, 4}
|
|
fetchInlineWebFile = func(_ context.Context, document domain.BotInlineWebDocument) ([]byte, string, error) {
|
|
fetches++
|
|
if document.URL != "https://example.test/photo.png" || document.AccessHash == 0 {
|
|
t.Fatalf("fetch document = %+v", document)
|
|
}
|
|
return append([]byte(nil), body...), "image/png; charset=binary", nil
|
|
}
|
|
defer func() { fetchInlineWebFile = oldFetch }()
|
|
|
|
type getResult struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}
|
|
req := &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "external-photo",
|
|
}
|
|
gotCh := make(chan getResult, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, req)
|
|
gotCh <- getResult{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 30,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineExternalPhotoContentResult("external-photo", "external photo caption"),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set external photo result = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
var got getResult
|
|
select {
|
|
case got = <-gotCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("external photo get inline results did not resolve")
|
|
}
|
|
if got.err != nil {
|
|
t.Fatalf("get external photo results: %v", got.err)
|
|
}
|
|
result, ok := got.res.Results[0].(*tg.BotInlineResult)
|
|
if !ok {
|
|
t.Fatalf("external photo result = %T, want *tg.BotInlineResult", got.res.Results[0])
|
|
}
|
|
if _, ok := result.SendMessage.(*tg.BotInlineMessageMediaAuto); !ok {
|
|
t.Fatalf("external photo send_message = %T, want mediaAuto", result.SendMessage)
|
|
}
|
|
if content, ok := result.GetContent(); !ok {
|
|
t.Fatal("external photo result missing content")
|
|
} else if doc, ok := content.(*tg.WebDocument); !ok || doc.URL != "https://example.test/photo.png" || doc.AccessHash == 0 {
|
|
t.Fatalf("external photo content = %#v", content)
|
|
}
|
|
|
|
send := func(queryID int64, randomID int64) *tg.Message {
|
|
t.Helper()
|
|
updatesClass, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: inputPeerUser(f.peer),
|
|
RandomID: randomID,
|
|
QueryID: queryID,
|
|
ID: "external-photo",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send external photo result: %v", err)
|
|
}
|
|
sent := messageFromUpdates(t, updatesClass)
|
|
if sent.Message != "external photo caption" {
|
|
t.Fatalf("sent external photo caption = %q", sent.Message)
|
|
}
|
|
if via, ok := sent.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("sent external photo via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
if media, ok := sent.Media.(*tg.MessageMediaPhoto); !ok {
|
|
t.Fatalf("sent external media = %T, want *tg.MessageMediaPhoto", sent.Media)
|
|
} else if photo, ok := media.Photo.(*tg.Photo); !ok || photo.ID == 0 || photo.ID == f.photo.ID {
|
|
t.Fatalf("sent external photo = %#v, want generated photo", media.Photo)
|
|
}
|
|
return sent
|
|
}
|
|
send(queryID, 9921)
|
|
|
|
cached, err := f.router.onMessagesGetInlineBotResults(ownerCtx, req)
|
|
if err != nil {
|
|
t.Fatalf("cached external photo get: %v", err)
|
|
}
|
|
if cached.QueryID == queryID {
|
|
t.Fatalf("cached external photo query_id reused %d", queryID)
|
|
}
|
|
send(cached.QueryID, 9922)
|
|
if fetches != 1 {
|
|
t.Fatalf("external photo fetches = %d, want one fetch reused by cached send", fetches)
|
|
}
|
|
|
|
historyList, err := f.router.deps.Messages.GetHistory(ownerCtx, f.owner.ID, domain.MessageFilter{
|
|
HasPeer: true,
|
|
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID},
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("external photo history: %v", err)
|
|
}
|
|
history := tgMessagesMessages(f.owner.ID, f.router.enrichMessageList(ownerCtx, f.owner.ID, historyList))
|
|
messages := messagesFromClass(history)
|
|
if len(messages) < 2 {
|
|
t.Fatalf("external photo history len = %d, want at least two messages", len(messages))
|
|
}
|
|
histMsg, ok := messages[0].(*tg.Message)
|
|
if !ok {
|
|
t.Fatalf("external photo history message = %T, want *tg.Message", messages[0])
|
|
}
|
|
if _, ok := histMsg.Media.(*tg.MessageMediaPhoto); !ok {
|
|
t.Fatalf("external photo history media = %T, want *tg.MessageMediaPhoto", histMsg.Media)
|
|
}
|
|
if via, ok := histMsg.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("external photo history via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
}
|
|
|
|
func TestInlineBotExternalContentSendFetchFailureDoesNotSend(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
oldFetch := fetchInlineWebFile
|
|
fetchInlineWebFile = func(context.Context, domain.BotInlineWebDocument) ([]byte, string, error) {
|
|
return nil, "", errors.New("fetch failed")
|
|
}
|
|
defer func() { fetchInlineWebFile = oldFetch }()
|
|
|
|
queryID, _ := f.router.inlines.register(f.router.clock.Now(), f.bot.ID, f.owner.ID, domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID})
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineExternalPhotoContentResult("external-photo", "caption"),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set external photo result = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
if _, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: inputPeerUser(f.peer),
|
|
RandomID: 9923,
|
|
QueryID: queryID,
|
|
ID: "external-photo",
|
|
}); err == nil || !strings.Contains(err.Error(), "MEDIA_INVALID") {
|
|
t.Fatalf("send failed external photo err = %v, want MEDIA_INVALID", err)
|
|
}
|
|
historyList, err := f.router.deps.Messages.GetHistory(ownerCtx, f.owner.ID, domain.MessageFilter{
|
|
HasPeer: true,
|
|
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID},
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed external photo history: %v", err)
|
|
}
|
|
if len(historyList.Messages) != 0 {
|
|
t.Fatalf("failed external photo sent %d messages, want none", len(historyList.Messages))
|
|
}
|
|
}
|
|
|
|
func TestInlineBotArticleTextExternalThumbValidation(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
queryID, _ := f.router.inlines.register(f.router.clock.Now(), f.bot.ID, f.owner.ID, domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID})
|
|
defer f.router.inlines.consume(queryID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
validThumb := func() tg.InputWebDocument {
|
|
return tg.InputWebDocument{
|
|
URL: "https://example.test/thumb.jpg",
|
|
Size: 1234,
|
|
MimeType: "image/jpeg",
|
|
Attributes: []tg.DocumentAttributeClass{
|
|
&tg.DocumentAttributeImageSize{W: 96, H: 96},
|
|
},
|
|
}
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
result tg.InputBotInlineResultClass
|
|
want string
|
|
}{
|
|
{
|
|
name: "empty thumb url",
|
|
result: func() tg.InputBotInlineResultClass {
|
|
result := inlineArticleResultWithWebThumb("bad-empty-url", "text")
|
|
thumb := validThumb()
|
|
thumb.URL = ""
|
|
result.SetThumb(thumb)
|
|
return result
|
|
}(),
|
|
want: "WEBDOCUMENT_URL_EMPTY",
|
|
},
|
|
{
|
|
name: "http thumb url",
|
|
result: func() tg.InputBotInlineResultClass {
|
|
result := inlineArticleResultWithWebThumb("bad-http-url", "text")
|
|
thumb := validThumb()
|
|
thumb.URL = "http://example.test/thumb.jpg"
|
|
result.SetThumb(thumb)
|
|
return result
|
|
}(),
|
|
want: "WEBDOCUMENT_URL_INVALID",
|
|
},
|
|
{
|
|
name: "bad mime",
|
|
result: func() tg.InputBotInlineResultClass {
|
|
result := inlineArticleResultWithWebThumb("bad-mime", "text")
|
|
thumb := validThumb()
|
|
thumb.MimeType = "image"
|
|
result.SetThumb(thumb)
|
|
return result
|
|
}(),
|
|
want: "WEBDOCUMENT_MIME_INVALID",
|
|
},
|
|
{
|
|
name: "too big",
|
|
result: func() tg.InputBotInlineResultClass {
|
|
result := inlineArticleResultWithWebThumb("too-big", "text")
|
|
thumb := validThumb()
|
|
thumb.Size = domain.MaxBotInlineWebSize + 1
|
|
result.SetThumb(thumb)
|
|
return result
|
|
}(),
|
|
want: "WEBDOCUMENT_SIZE_TOO_BIG",
|
|
},
|
|
{
|
|
name: "content too big",
|
|
result: func() tg.InputBotInlineResultClass {
|
|
result := inlineArticleResultWithWebThumb("content-too-big", "text")
|
|
result.SetContent(tg.InputWebDocument{
|
|
URL: "https://example.test/content.png",
|
|
Size: domain.MaxBotInlineWebSize + 1,
|
|
MimeType: "image/png",
|
|
})
|
|
return result
|
|
}(),
|
|
want: "WEBDOCUMENT_SIZE_TOO_BIG",
|
|
},
|
|
{
|
|
name: "content bad mime",
|
|
result: func() tg.InputBotInlineResultClass {
|
|
result := inlineArticleResultWithWebThumb("content-bad-mime", "text")
|
|
result.SetContent(tg.InputWebDocument{
|
|
URL: "https://example.test/content.png",
|
|
Size: 512,
|
|
MimeType: "image",
|
|
})
|
|
return result
|
|
}(),
|
|
want: "WEBDOCUMENT_MIME_INVALID",
|
|
},
|
|
{
|
|
name: "media auto content mime mismatch",
|
|
result: func() tg.InputBotInlineResultClass {
|
|
result := inlineArticleResultWithWebThumb("content", "text")
|
|
result.Type = "photo"
|
|
result.SendMessage = &tg.InputBotInlineMessageMediaAuto{}
|
|
result.SetContent(tg.InputWebDocument{
|
|
URL: "https://example.test/content.html",
|
|
Size: 512,
|
|
MimeType: "text/html",
|
|
})
|
|
return result
|
|
}(),
|
|
want: "MEDIA_INVALID",
|
|
},
|
|
{
|
|
name: "geo thumb unsupported",
|
|
result: func() tg.InputBotInlineResultClass {
|
|
result := inlineGeoResult("geo-thumb", 39.9, 116.3).(*tg.InputBotInlineResult)
|
|
result.SetThumb(validThumb())
|
|
return result
|
|
}(),
|
|
want: "WEBDOCUMENT_INVALID",
|
|
},
|
|
{
|
|
name: "result url invalid",
|
|
result: func() tg.InputBotInlineResultClass {
|
|
result := inlineArticleResultWithWebThumb("bad-result-url", "text")
|
|
result.SetURL("ftp://example.test/result")
|
|
return result
|
|
}(),
|
|
want: "WEBDOCUMENT_URL_INVALID",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
tc.result,
|
|
},
|
|
}); err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("set inline results err = %v, want %s", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInlineBotArticleTextChannelRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
sessions := &captureSessions{}
|
|
f.router.deps.Sessions = sessions
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
created, err := f.channels.CreateMegagroupFromCreateChat(ctx, f.owner.ID, domain.CreateChannelRequest{
|
|
Title: "Inline Group",
|
|
MemberUserIDs: []int64{f.peer.ID},
|
|
Date: 1700001000,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create inline group: %v", err)
|
|
}
|
|
peer := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash}
|
|
|
|
type getResult struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}
|
|
gotCh := make(chan getResult, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: peer,
|
|
Query: "group",
|
|
})
|
|
gotCh <- getResult{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 30,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineArticleResultWithCallback("group-article", "inline group hello", "Open", []byte{0x00, 0xff, 0x42}),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set channel inline results = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
select {
|
|
case got := <-gotCh:
|
|
if got.err != nil {
|
|
t.Fatalf("get channel inline results: %v", got.err)
|
|
}
|
|
if got.res.QueryID != queryID || len(got.res.Results) != 1 {
|
|
t.Fatalf("channel inline results = query %d len %d, want query %d len 1", got.res.QueryID, len(got.res.Results), queryID)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("channel get inline results did not resolve")
|
|
}
|
|
|
|
updatesClass, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: peer,
|
|
RandomID: 9902,
|
|
QueryID: queryID,
|
|
ID: "group-article",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send channel inline bot result: %v", err)
|
|
}
|
|
sent, full := channelMessageFromUpdates(t, updatesClass)
|
|
if sent.Message != "inline group hello" {
|
|
t.Fatalf("channel inline message = %q, want inline group hello", sent.Message)
|
|
}
|
|
if via, ok := sent.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("channel send via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
assertTGInlineReplyMarkup(t, sent, "Open", []byte{0x00, 0xff, 0x42})
|
|
assertUpdatesContainBotUser(t, full, f.bot.ID)
|
|
|
|
historyList, err := f.router.deps.Channels.GetHistory(ownerCtx, f.owner.ID, domain.ChannelHistoryFilter{
|
|
ChannelID: created.Channel.ID,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("channel history: %v", err)
|
|
}
|
|
history := tgChannelHistoryMessages(f.owner.ID, f.router.enrichChannelHistory(ownerCtx, f.owner.ID, historyList))
|
|
channelHistory, ok := history.(*tg.MessagesChannelMessages)
|
|
if !ok || len(channelHistory.Messages) == 0 {
|
|
t.Fatalf("channel history = %T len %d, want channel messages", history, len(channelMessagesFromClass(history)))
|
|
}
|
|
histMsg, ok := channelHistory.Messages[0].(*tg.Message)
|
|
if !ok {
|
|
t.Fatalf("channel history message = %T, want *tg.Message", channelHistory.Messages[0])
|
|
}
|
|
if via, ok := histMsg.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("channel history via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
assertTGInlineReplyMarkup(t, histMsg, "Open", []byte{0x00, 0xff, 0x42})
|
|
assertChannelMessagesContainBotUser(t, channelHistory, f.bot.ID)
|
|
|
|
diff, err := f.router.deps.Channels.GetDifference(ownerCtx, f.owner.ID, domain.ChannelDifferenceRequest{
|
|
UserID: f.owner.ID,
|
|
ChannelID: created.Channel.ID,
|
|
Pts: created.Channel.Pts,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("channel difference: %v", err)
|
|
}
|
|
if len(diff.NewMessages) != 1 {
|
|
t.Fatalf("channel difference messages = %+v, want one new message", diff.NewMessages)
|
|
}
|
|
assertDomainInlineReplyMarkup(t, diff.NewMessages[0].ReplyMarkup, "Open", []byte{0x00, 0xff, 0x42})
|
|
|
|
feedback := inlineSendFeedbackFromSessions(t, sessions, f.bot.ID)
|
|
msgID, ok := feedback.GetMsgID()
|
|
if !ok {
|
|
t.Fatal("channel inline send feedback missing msg_id")
|
|
}
|
|
msgID64, ok := msgID.(*tg.InputBotInlineMessageID64)
|
|
if !ok || msgID64.OwnerID != created.Channel.ID || msgID64.ID != sent.ID {
|
|
t.Fatalf("channel inline msg_id = %#v, want id64 owner channel %d msg %d", msgID, created.Channel.ID, sent.ID)
|
|
}
|
|
badID := *msgID64
|
|
badID.AccessHash++
|
|
badEdit := &tg.MessagesEditInlineBotMessageRequest{ID: &badID}
|
|
badEdit.SetMessage("bad edit")
|
|
if ok, err := f.router.onMessagesEditInlineBotMessage(botCtx, badEdit); ok || !tgerr.Is(err, "MESSAGE_ID_INVALID") {
|
|
t.Fatalf("bad channel inline edit = %v,%v, want false,MESSAGE_ID_INVALID", ok, err)
|
|
}
|
|
editReq := &tg.MessagesEditInlineBotMessageRequest{ID: msgID}
|
|
editReq.SetMessage("inline group edited")
|
|
editReq.SetReplyMarkup(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{
|
|
Buttons: []tg.KeyboardButtonClass{&tg.KeyboardButtonCallback{Text: "Done", Data: []byte("v2")}},
|
|
}}})
|
|
if ok, err := f.router.onMessagesEditInlineBotMessage(botCtx, editReq); err != nil || !ok {
|
|
t.Fatalf("channel inline edit = %v,%v, want true,nil", ok, err)
|
|
}
|
|
editedHistory, err := f.router.deps.Channels.GetHistory(ownerCtx, f.owner.ID, domain.ChannelHistoryFilter{
|
|
ChannelID: created.Channel.ID,
|
|
Limit: 1,
|
|
})
|
|
if err != nil || len(editedHistory.Messages) != 1 {
|
|
t.Fatalf("edited channel history len = %d err=%v, want 1,nil", len(editedHistory.Messages), err)
|
|
}
|
|
if editedHistory.Messages[0].Body != "inline group edited" || editedHistory.Messages[0].ViaBotID != f.bot.ID {
|
|
t.Fatalf("edited channel message = %+v, want edited via bot", editedHistory.Messages[0])
|
|
}
|
|
assertDomainInlineReplyMarkup(t, editedHistory.Messages[0].ReplyMarkup, "Done", []byte("v2"))
|
|
editDiff, err := f.router.deps.Channels.GetDifference(ownerCtx, f.owner.ID, domain.ChannelDifferenceRequest{
|
|
UserID: f.owner.ID,
|
|
ChannelID: created.Channel.ID,
|
|
Pts: diff.Pts,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("channel edit difference: %v", err)
|
|
}
|
|
if len(editDiff.OtherUpdates) != 1 || editDiff.OtherUpdates[0].Type != domain.ChannelUpdateEditMessage {
|
|
t.Fatalf("channel edit difference updates = %+v, want one edit", editDiff.OtherUpdates)
|
|
}
|
|
assertDomainInlineReplyMarkup(t, editDiff.OtherUpdates[0].Message.ReplyMarkup, "Done", []byte("v2"))
|
|
}
|
|
|
|
func TestInlineBotChannelInlineEditRewritesMedia(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
sessions := &captureSessions{}
|
|
f.router.deps.Sessions = sessions
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
created, err := f.channels.CreateMegagroupFromCreateChat(ctx, f.owner.ID, domain.CreateChannelRequest{
|
|
Title: "Inline Media Edit Group",
|
|
MemberUserIDs: []int64{f.peer.ID},
|
|
Date: 1700002300,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create inline media edit group: %v", err)
|
|
}
|
|
peer := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash}
|
|
queryID, _ := f.router.inlines.registerWithCacheKey(f.router.clock.Now(), f.bot.ID, f.owner.ID, domain.Peer{Type: domain.PeerTypeChannel, ID: created.Channel.ID}, inlineCacheKey{query: "channel-media-edit"})
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineArticleResultWithCallback("channel-media-edit-1", "channel caption before media edit", "Open", []byte("channel-media-v1")),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set channel media edit inline result = %v,%v, want true,nil", ok, err)
|
|
}
|
|
if _, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: peer,
|
|
RandomID: 9907,
|
|
QueryID: queryID,
|
|
ID: "channel-media-edit-1",
|
|
}); err != nil {
|
|
t.Fatalf("send channel media edit inline result: %v", err)
|
|
}
|
|
msgID, ok := inlineSendFeedbackFromSessions(t, sessions, f.bot.ID).GetMsgID()
|
|
if !ok {
|
|
t.Fatal("channel inline media edit feedback missing msg_id")
|
|
}
|
|
|
|
editReq := &tg.MessagesEditInlineBotMessageRequest{ID: msgID}
|
|
editReq.SetMedia(&tg.InputMediaGeoPoint{GeoPoint: &tg.InputGeoPoint{Lat: 40.7128, Long: -74.0060}})
|
|
if ok, err := f.router.onMessagesEditInlineBotMessage(botCtx, editReq); err != nil || !ok {
|
|
t.Fatalf("channel inline media edit = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
history, err := f.router.deps.Channels.GetHistory(ownerCtx, f.owner.ID, domain.ChannelHistoryFilter{
|
|
ChannelID: created.Channel.ID,
|
|
Limit: 1,
|
|
})
|
|
if err != nil || len(history.Messages) != 1 {
|
|
t.Fatalf("channel media edit history len = %d err=%v, want 1,nil", len(history.Messages), err)
|
|
}
|
|
msg := history.Messages[0]
|
|
if msg.Body != "channel caption before media edit" || msg.ViaBotID != f.bot.ID {
|
|
t.Fatalf("channel media edited message = %+v, want original caption via bot", msg)
|
|
}
|
|
if msg.Media == nil || msg.Media.Kind != domain.MessageMediaKindGeo || msg.Media.Geo == nil {
|
|
t.Fatalf("channel media edited media = %+v, want geo", msg.Media)
|
|
}
|
|
assertDomainInlineReplyMarkup(t, msg.ReplyMarkup, "Open", []byte("channel-media-v1"))
|
|
|
|
diff, err := f.router.deps.Channels.GetDifference(ownerCtx, f.owner.ID, domain.ChannelDifferenceRequest{
|
|
UserID: f.owner.ID,
|
|
ChannelID: created.Channel.ID,
|
|
Pts: created.Channel.Pts + 1,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("channel media edit difference: %v", err)
|
|
}
|
|
if len(diff.OtherUpdates) != 1 || diff.OtherUpdates[0].Type != domain.ChannelUpdateEditMessage {
|
|
t.Fatalf("channel media edit difference updates = %+v, want one edit", diff.OtherUpdates)
|
|
}
|
|
if diff.OtherUpdates[0].Message.Media == nil || diff.OtherUpdates[0].Message.Media.Kind != domain.MessageMediaKindGeo {
|
|
t.Fatalf("channel media edit difference message = %+v, want geo media", diff.OtherUpdates[0].Message)
|
|
}
|
|
assertDomainInlineReplyMarkup(t, diff.OtherUpdates[0].Message.ReplyMarkup, "Open", []byte("channel-media-v1"))
|
|
}
|
|
|
|
func TestInlineBotPhotoMediaPrivateRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
type getResult struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}
|
|
gotCh := make(chan getResult, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "photo",
|
|
})
|
|
gotCh <- getResult{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 30,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlinePhotoResult("photo-1", f.photo, "inline photo caption"),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set inline photo result = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
var got getResult
|
|
select {
|
|
case got = <-gotCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("photo get inline results did not resolve")
|
|
}
|
|
if got.err != nil {
|
|
t.Fatalf("get inline photo results: %v", got.err)
|
|
}
|
|
mediaResult, ok := got.res.Results[0].(*tg.BotInlineMediaResult)
|
|
if !ok {
|
|
t.Fatalf("inline photo result = %T, want *tg.BotInlineMediaResult", got.res.Results[0])
|
|
}
|
|
if photo, ok := mediaResult.Photo.(*tg.Photo); !ok || photo.ID != f.photo.ID {
|
|
t.Fatalf("inline result photo = %#v, want photo %d", mediaResult.Photo, f.photo.ID)
|
|
}
|
|
|
|
updatesClass, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: inputPeerUser(f.peer),
|
|
RandomID: 9903,
|
|
QueryID: queryID,
|
|
ID: "photo-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send inline photo result: %v", err)
|
|
}
|
|
sent := messageFromUpdates(t, updatesClass)
|
|
if sent.Message != "inline photo caption" {
|
|
t.Fatalf("sent photo caption = %q, want inline photo caption", sent.Message)
|
|
}
|
|
if via, ok := sent.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("sent photo via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
photoMedia, ok := sent.Media.(*tg.MessageMediaPhoto)
|
|
if !ok {
|
|
t.Fatalf("sent media = %T, want *tg.MessageMediaPhoto", sent.Media)
|
|
}
|
|
if photo, ok := photoMedia.Photo.(*tg.Photo); !ok || photo.ID != f.photo.ID {
|
|
t.Fatalf("sent media photo = %#v, want photo %d", photoMedia.Photo, f.photo.ID)
|
|
}
|
|
}
|
|
|
|
func TestInlineBotDocumentMediaChannelRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
created, err := f.channels.CreateMegagroupFromCreateChat(ctx, f.owner.ID, domain.CreateChannelRequest{
|
|
Title: "Inline Document Group",
|
|
MemberUserIDs: []int64{f.peer.ID},
|
|
Date: 1700002000,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create inline document group: %v", err)
|
|
}
|
|
peer := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash}
|
|
|
|
type getResult struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}
|
|
gotCh := make(chan getResult, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: peer,
|
|
Query: "document",
|
|
})
|
|
gotCh <- getResult{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 30,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineDocumentResult("doc-1", f.document, "inline document caption"),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set inline document result = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
select {
|
|
case got := <-gotCh:
|
|
if got.err != nil {
|
|
t.Fatalf("get inline document results: %v", got.err)
|
|
}
|
|
mediaResult, ok := got.res.Results[0].(*tg.BotInlineMediaResult)
|
|
if !ok {
|
|
t.Fatalf("inline document result = %T, want *tg.BotInlineMediaResult", got.res.Results[0])
|
|
}
|
|
if doc, ok := mediaResult.Document.(*tg.Document); !ok || doc.ID != f.document.ID {
|
|
t.Fatalf("inline result document = %#v, want document %d", mediaResult.Document, f.document.ID)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("document get inline results did not resolve")
|
|
}
|
|
|
|
updatesClass, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: peer,
|
|
RandomID: 9904,
|
|
QueryID: queryID,
|
|
ID: "doc-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send inline document result: %v", err)
|
|
}
|
|
sent, full := channelMessageFromUpdates(t, updatesClass)
|
|
if sent.Message != "inline document caption" {
|
|
t.Fatalf("sent document caption = %q, want inline document caption", sent.Message)
|
|
}
|
|
if via, ok := sent.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("sent document via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
docMedia, ok := sent.Media.(*tg.MessageMediaDocument)
|
|
if !ok {
|
|
t.Fatalf("sent channel media = %T, want *tg.MessageMediaDocument", sent.Media)
|
|
}
|
|
if doc, ok := docMedia.Document.(*tg.Document); !ok || doc.ID != f.document.ID {
|
|
t.Fatalf("sent channel document = %#v, want document %d", docMedia.Document, f.document.ID)
|
|
}
|
|
assertUpdatesContainBotUser(t, full, f.bot.ID)
|
|
|
|
historyList, err := f.router.deps.Channels.GetHistory(ownerCtx, f.owner.ID, domain.ChannelHistoryFilter{
|
|
ChannelID: created.Channel.ID,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("channel document history: %v", err)
|
|
}
|
|
history := tgChannelHistoryMessages(f.owner.ID, f.router.enrichChannelHistory(ownerCtx, f.owner.ID, historyList))
|
|
channelHistory, ok := history.(*tg.MessagesChannelMessages)
|
|
if !ok || len(channelHistory.Messages) == 0 {
|
|
t.Fatalf("channel document history = %T len %d, want channel messages", history, len(channelMessagesFromClass(history)))
|
|
}
|
|
histMsg, ok := channelHistory.Messages[0].(*tg.Message)
|
|
if !ok {
|
|
t.Fatalf("channel document history message = %T, want *tg.Message", channelHistory.Messages[0])
|
|
}
|
|
if _, ok := histMsg.Media.(*tg.MessageMediaDocument); !ok {
|
|
t.Fatalf("channel document history media = %T, want *tg.MessageMediaDocument", histMsg.Media)
|
|
}
|
|
if via, ok := histMsg.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("channel document history via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
}
|
|
|
|
func TestInlineBotContactMediaPrivateRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
type getResult struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}
|
|
gotCh := make(chan getResult, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "contact",
|
|
})
|
|
gotCh <- getResult{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 30,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineContactResult("contact-1", f.peer.Phone, "Peer", "Shared", "BEGIN:VCARD\nFN:Peer Shared\nEND:VCARD"),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set inline contact result = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
var got getResult
|
|
select {
|
|
case got = <-gotCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("contact get inline results did not resolve")
|
|
}
|
|
if got.err != nil {
|
|
t.Fatalf("get inline contact results: %v", got.err)
|
|
}
|
|
result, ok := got.res.Results[0].(*tg.BotInlineResult)
|
|
if !ok {
|
|
t.Fatalf("inline contact result = %T, want *tg.BotInlineResult", got.res.Results[0])
|
|
}
|
|
contactMsg, ok := result.SendMessage.(*tg.BotInlineMessageMediaContact)
|
|
if !ok {
|
|
t.Fatalf("inline contact send_message = %T, want *tg.BotInlineMessageMediaContact", result.SendMessage)
|
|
}
|
|
if contactMsg.PhoneNumber != f.peer.Phone || contactMsg.FirstName != "Peer" || contactMsg.LastName != "Shared" {
|
|
t.Fatalf("inline contact message = %+v, want peer contact", contactMsg)
|
|
}
|
|
|
|
updatesClass, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: inputPeerUser(f.peer),
|
|
RandomID: 9905,
|
|
QueryID: queryID,
|
|
ID: "contact-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send inline contact result: %v", err)
|
|
}
|
|
sent := messageFromUpdates(t, updatesClass)
|
|
media, ok := sent.Media.(*tg.MessageMediaContact)
|
|
if !ok {
|
|
t.Fatalf("sent contact media = %T, want *tg.MessageMediaContact", sent.Media)
|
|
}
|
|
if media.PhoneNumber != f.peer.Phone || media.UserID != f.peer.ID || media.Vcard == "" {
|
|
t.Fatalf("sent contact media = %+v, want peer contact with user_id", media)
|
|
}
|
|
if via, ok := sent.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("sent contact via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
}
|
|
|
|
func TestInlineBotContactMediaChannelRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
created, err := f.channels.CreateMegagroupFromCreateChat(ctx, f.owner.ID, domain.CreateChannelRequest{
|
|
Title: "Inline Contact Group",
|
|
MemberUserIDs: []int64{f.peer.ID},
|
|
Date: 1700002200,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create inline contact group: %v", err)
|
|
}
|
|
peer := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash}
|
|
|
|
type getResult struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}
|
|
gotCh := make(chan getResult, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: peer,
|
|
Query: "contact",
|
|
})
|
|
gotCh <- getResult{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 30,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineContactResultWithCallback("contact-channel", f.peer.Phone, "Peer", "Shared", []byte("contact-data")),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set channel inline contact result = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
select {
|
|
case got := <-gotCh:
|
|
if got.err != nil {
|
|
t.Fatalf("get channel inline contact results: %v", got.err)
|
|
}
|
|
result, ok := got.res.Results[0].(*tg.BotInlineResult)
|
|
if !ok {
|
|
t.Fatalf("channel inline contact result = %T, want *tg.BotInlineResult", got.res.Results[0])
|
|
}
|
|
msg, ok := result.SendMessage.(*tg.BotInlineMessageMediaContact)
|
|
if !ok {
|
|
t.Fatalf("channel inline contact send_message = %T, want contact", result.SendMessage)
|
|
}
|
|
if markup, ok := msg.GetReplyMarkup(); !ok || markup == nil {
|
|
t.Fatalf("channel inline contact missing reply markup")
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("channel contact get inline results did not resolve")
|
|
}
|
|
|
|
updatesClass, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: peer,
|
|
RandomID: 9906,
|
|
QueryID: queryID,
|
|
ID: "contact-channel",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send channel inline contact result: %v", err)
|
|
}
|
|
sent, full := channelMessageFromUpdates(t, updatesClass)
|
|
media, ok := sent.Media.(*tg.MessageMediaContact)
|
|
if !ok {
|
|
t.Fatalf("sent channel contact media = %T, want *tg.MessageMediaContact", sent.Media)
|
|
}
|
|
if media.PhoneNumber != f.peer.Phone || media.UserID != f.peer.ID {
|
|
t.Fatalf("sent channel contact media = %+v, want peer contact with user_id", media)
|
|
}
|
|
if via, ok := sent.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("sent channel contact via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
assertTGInlineReplyMarkup(t, sent, "Contact", []byte("contact-data"))
|
|
assertUpdatesContainBotUser(t, full, f.bot.ID)
|
|
|
|
historyList, err := f.router.deps.Channels.GetHistory(ownerCtx, f.owner.ID, domain.ChannelHistoryFilter{
|
|
ChannelID: created.Channel.ID,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("channel contact history: %v", err)
|
|
}
|
|
history := tgChannelHistoryMessages(f.owner.ID, f.router.enrichChannelHistory(ownerCtx, f.owner.ID, historyList))
|
|
channelHistory, ok := history.(*tg.MessagesChannelMessages)
|
|
if !ok || len(channelHistory.Messages) == 0 {
|
|
t.Fatalf("channel contact history = %T len %d, want channel messages", history, len(channelMessagesFromClass(history)))
|
|
}
|
|
histMsg, ok := channelHistory.Messages[0].(*tg.Message)
|
|
if !ok {
|
|
t.Fatalf("channel contact history message = %T, want *tg.Message", channelHistory.Messages[0])
|
|
}
|
|
if _, ok := histMsg.Media.(*tg.MessageMediaContact); !ok {
|
|
t.Fatalf("channel contact history media = %T, want *tg.MessageMediaContact", histMsg.Media)
|
|
}
|
|
assertTGInlineReplyMarkup(t, histMsg, "Contact", []byte("contact-data"))
|
|
|
|
diff, err := f.router.deps.Channels.GetDifference(ownerCtx, f.owner.ID, domain.ChannelDifferenceRequest{
|
|
UserID: f.owner.ID,
|
|
ChannelID: created.Channel.ID,
|
|
Pts: created.Channel.Pts,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("channel contact difference: %v", err)
|
|
}
|
|
if len(diff.NewMessages) != 1 || diff.NewMessages[0].Media == nil || diff.NewMessages[0].Media.Kind != domain.MessageMediaKindContact {
|
|
t.Fatalf("channel contact difference messages = %+v, want one contact", diff.NewMessages)
|
|
}
|
|
assertDomainInlineReplyMarkup(t, diff.NewMessages[0].ReplyMarkup, "Contact", []byte("contact-data"))
|
|
}
|
|
|
|
func TestInlineBotContactValidation(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
queryID, _ := f.router.inlines.register(f.router.clock.Now(), f.bot.ID, f.owner.ID, domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID})
|
|
defer f.router.inlines.consume(queryID)
|
|
|
|
longVcard := strings.Repeat("x", maxContactVcardLength+1)
|
|
withThumb := inlineContactResult("contact-thumb", f.peer.Phone, "Peer", "Shared", "")
|
|
withThumb.SetThumb(tg.InputWebDocument{URL: "https://example.test/thumb.jpg", Size: 128, MimeType: "image/jpeg"})
|
|
cases := []struct {
|
|
name string
|
|
result tg.InputBotInlineResultClass
|
|
want string
|
|
}{
|
|
{
|
|
name: "type mismatch",
|
|
result: func() tg.InputBotInlineResultClass {
|
|
r := inlineContactResult("bad-type", f.peer.Phone, "Peer", "", "")
|
|
r.Type = "article"
|
|
return r
|
|
}(),
|
|
want: "RESULT_TYPE_INVALID",
|
|
},
|
|
{
|
|
name: "empty contact",
|
|
result: inlineContactResult("empty-contact", "", "", "", ""),
|
|
want: "MEDIA_EMPTY",
|
|
},
|
|
{
|
|
name: "vcard too long",
|
|
result: inlineContactResult("long-vcard", f.peer.Phone, "Peer", "", longVcard),
|
|
want: "MEDIA_INVALID",
|
|
},
|
|
{
|
|
name: "web preview unsupported",
|
|
result: withThumb,
|
|
want: "WEBDOCUMENT_INVALID",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
tc.result,
|
|
},
|
|
}); err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("set inline contact err = %v, want %s", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInlineBotGeoMediaPrivateRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
req := &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "geo",
|
|
}
|
|
req.SetGeoPoint(&tg.InputGeoPoint{Lat: 39.9042, Long: 116.4074})
|
|
if _, err := f.router.onMessagesGetInlineBotResults(ownerCtx, req); err == nil || !strings.Contains(err.Error(), "BOT_INLINE_GEO_NOT_ALLOWED") {
|
|
t.Fatalf("inline geo disabled err = %v, want BOT_INLINE_GEO_NOT_ALLOWED", err)
|
|
}
|
|
if _, err := f.bots.SetInlineGeo(ctx, f.bot.ID, true); err != nil {
|
|
t.Fatalf("enable inline geo: %v", err)
|
|
}
|
|
|
|
type getResult struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}
|
|
gotCh := make(chan getResult, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, req)
|
|
gotCh <- getResult{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 30,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineGeoResult("geo-1", 31.2304, 121.4737),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set inline geo result = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
var got getResult
|
|
select {
|
|
case got = <-gotCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("geo get inline results did not resolve")
|
|
}
|
|
if got.err != nil {
|
|
t.Fatalf("get inline geo results: %v", got.err)
|
|
}
|
|
if len(got.res.Users) != 1 {
|
|
t.Fatalf("geo inline users = %d, want bot user", len(got.res.Users))
|
|
}
|
|
botUser, ok := got.res.Users[0].(*tg.User)
|
|
if !ok || !botUser.BotInlineGeo {
|
|
t.Fatalf("geo inline bot user = %#v, want bot_inline_geo", got.res.Users[0])
|
|
}
|
|
result, ok := got.res.Results[0].(*tg.BotInlineResult)
|
|
if !ok {
|
|
t.Fatalf("geo inline result = %T, want *tg.BotInlineResult", got.res.Results[0])
|
|
}
|
|
if _, ok := result.SendMessage.(*tg.BotInlineMessageMediaGeo); !ok {
|
|
t.Fatalf("geo send_message = %T, want *tg.BotInlineMessageMediaGeo", result.SendMessage)
|
|
}
|
|
|
|
updatesClass, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: inputPeerUser(f.peer),
|
|
RandomID: 9905,
|
|
QueryID: queryID,
|
|
ID: "geo-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send inline geo result: %v", err)
|
|
}
|
|
sent := messageFromUpdates(t, updatesClass)
|
|
if via, ok := sent.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("sent geo via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
media, ok := sent.Media.(*tg.MessageMediaGeo)
|
|
if !ok {
|
|
t.Fatalf("sent geo media = %T, want *tg.MessageMediaGeo", sent.Media)
|
|
}
|
|
point, ok := media.Geo.(*tg.GeoPoint)
|
|
if !ok || point.Lat != 31.2304 || point.Long != 121.4737 {
|
|
t.Fatalf("sent geo point = %#v, want 31.2304,121.4737", media.Geo)
|
|
}
|
|
}
|
|
|
|
func TestInlineBotVenueMediaChannelRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
if _, err := f.bots.SetInlineGeo(ctx, f.bot.ID, true); err != nil {
|
|
t.Fatalf("enable inline geo: %v", err)
|
|
}
|
|
created, err := f.channels.CreateMegagroupFromCreateChat(ctx, f.owner.ID, domain.CreateChannelRequest{
|
|
Title: "Inline Venue Group",
|
|
MemberUserIDs: []int64{f.peer.ID},
|
|
Date: 1700003000,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create inline venue group: %v", err)
|
|
}
|
|
peer := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash}
|
|
req := &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: peer,
|
|
Query: "venue",
|
|
}
|
|
req.SetGeoPoint(&tg.InputGeoPoint{Lat: 39.9042, Long: 116.4074})
|
|
|
|
type getResult struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}
|
|
gotCh := make(chan getResult, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, req)
|
|
gotCh <- getResult{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 30,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineVenueResult("venue-1", "Cafe Inline", 30.2672, -97.7431),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set inline venue result = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
select {
|
|
case got := <-gotCh:
|
|
if got.err != nil {
|
|
t.Fatalf("get inline venue results: %v", got.err)
|
|
}
|
|
result, ok := got.res.Results[0].(*tg.BotInlineResult)
|
|
if !ok {
|
|
t.Fatalf("venue inline result = %T, want *tg.BotInlineResult", got.res.Results[0])
|
|
}
|
|
if _, ok := result.SendMessage.(*tg.BotInlineMessageMediaVenue); !ok {
|
|
t.Fatalf("venue send_message = %T, want *tg.BotInlineMessageMediaVenue", result.SendMessage)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("venue get inline results did not resolve")
|
|
}
|
|
|
|
updatesClass, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: peer,
|
|
RandomID: 9906,
|
|
QueryID: queryID,
|
|
ID: "venue-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send inline venue result: %v", err)
|
|
}
|
|
sent, full := channelMessageFromUpdates(t, updatesClass)
|
|
venueMedia, ok := sent.Media.(*tg.MessageMediaVenue)
|
|
if !ok {
|
|
t.Fatalf("sent venue media = %T, want *tg.MessageMediaVenue", sent.Media)
|
|
}
|
|
if venueMedia.Title != "Cafe Inline" {
|
|
t.Fatalf("sent venue title = %q, want Cafe Inline", venueMedia.Title)
|
|
}
|
|
if via, ok := sent.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("sent venue via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
assertUpdatesContainBotUser(t, full, f.bot.ID)
|
|
|
|
historyList, err := f.router.deps.Channels.GetHistory(ownerCtx, f.owner.ID, domain.ChannelHistoryFilter{
|
|
ChannelID: created.Channel.ID,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("channel venue history: %v", err)
|
|
}
|
|
history := tgChannelHistoryMessages(f.owner.ID, f.router.enrichChannelHistory(ownerCtx, f.owner.ID, historyList))
|
|
channelHistory, ok := history.(*tg.MessagesChannelMessages)
|
|
if !ok || len(channelHistory.Messages) == 0 {
|
|
t.Fatalf("channel venue history = %T len %d, want channel messages", history, len(channelMessagesFromClass(history)))
|
|
}
|
|
histMsg, ok := channelHistory.Messages[0].(*tg.Message)
|
|
if !ok {
|
|
t.Fatalf("channel venue history message = %T, want *tg.Message", channelHistory.Messages[0])
|
|
}
|
|
if _, ok := histMsg.Media.(*tg.MessageMediaVenue); !ok {
|
|
t.Fatalf("channel venue history media = %T, want *tg.MessageMediaVenue", histMsg.Media)
|
|
}
|
|
if via, ok := histMsg.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("channel venue history via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
}
|
|
|
|
func TestInlineBotSwitchPMRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
type getResult struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}
|
|
gotCh := make(chan getResult, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "connect",
|
|
})
|
|
gotCh <- getResult{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
req := &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 30,
|
|
Results: []tg.InputBotInlineResultClass{},
|
|
}
|
|
req.SetSwitchPm(tg.InlineBotSwitchPM{Text: "Connect account", StartParam: "connect_123"})
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, req); err != nil || !ok {
|
|
t.Fatalf("set switch_pm inline results = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
var got getResult
|
|
select {
|
|
case got = <-gotCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("switch_pm get inline results did not resolve")
|
|
}
|
|
if got.err != nil {
|
|
t.Fatalf("get switch_pm inline results: %v", got.err)
|
|
}
|
|
if got.res.QueryID != queryID {
|
|
t.Fatalf("switch_pm query_id = %d, want %d", got.res.QueryID, queryID)
|
|
}
|
|
if len(got.res.Results) != 0 {
|
|
t.Fatalf("switch_pm results len = %d, want 0", len(got.res.Results))
|
|
}
|
|
switchPM, ok := got.res.GetSwitchPm()
|
|
if !ok {
|
|
t.Fatal("switch_pm missing in bot results")
|
|
}
|
|
if switchPM.Text != "Connect account" || switchPM.StartParam != "connect_123" {
|
|
t.Fatalf("switch_pm = %#v, want text/start_param", switchPM)
|
|
}
|
|
if len(got.res.Users) != 1 {
|
|
t.Fatalf("switch_pm users = %d, want bot user", len(got.res.Users))
|
|
}
|
|
}
|
|
|
|
func TestInlineBotServerCacheHitUsesFreshQueryID(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
req := &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "cached",
|
|
}
|
|
|
|
gotCh := make(chan struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}, 1)
|
|
go func() {
|
|
res, err := f.router.onMessagesGetInlineBotResults(ownerCtx, req)
|
|
gotCh <- struct {
|
|
res *tg.MessagesBotResults
|
|
err error
|
|
}{res: res, err: err}
|
|
}()
|
|
|
|
queryID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
CacheTime: 60,
|
|
NextOffset: "page2",
|
|
Results: []tg.InputBotInlineResultClass{inlineArticleResult("cached-1", "cached hello")},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set cached inline results = %v,%v, want true,nil", ok, err)
|
|
}
|
|
select {
|
|
case got := <-gotCh:
|
|
if got.err != nil {
|
|
t.Fatalf("first cached get: %v", got.err)
|
|
}
|
|
if got.res.QueryID != queryID {
|
|
t.Fatalf("first query_id = %d, want %d", got.res.QueryID, queryID)
|
|
}
|
|
if next, ok := got.res.GetNextOffset(); !ok || next != "page2" {
|
|
t.Fatalf("first next_offset = %q,%v, want page2,true", next, ok)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("first cached get did not resolve")
|
|
}
|
|
if f.router.inlines.unansweredSize() != 0 {
|
|
t.Fatalf("unanswered inline queries after first answer = %d, want 0", f.router.inlines.unansweredSize())
|
|
}
|
|
|
|
fastCtx, cancel := context.WithTimeout(ownerCtx, 200*time.Millisecond)
|
|
defer cancel()
|
|
cached, err := f.router.onMessagesGetInlineBotResults(fastCtx, req)
|
|
if err != nil {
|
|
t.Fatalf("cached get: %v", err)
|
|
}
|
|
if cached.QueryID == queryID {
|
|
t.Fatalf("cached query_id reused old value %d", queryID)
|
|
}
|
|
if f.router.inlines.unansweredSize() != 0 {
|
|
t.Fatalf("unanswered inline queries after cache hit = %d, want 0", f.router.inlines.unansweredSize())
|
|
}
|
|
if next, ok := cached.GetNextOffset(); !ok || next != "page2" {
|
|
t.Fatalf("cached next_offset = %q,%v, want page2,true", next, ok)
|
|
}
|
|
|
|
updatesClass, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: inputPeerUser(f.peer),
|
|
RandomID: 9910,
|
|
QueryID: cached.QueryID,
|
|
ID: "cached-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send cached inline result: %v", err)
|
|
}
|
|
sent := messageFromUpdates(t, updatesClass)
|
|
if sent.Message != "cached hello" {
|
|
t.Fatalf("cached sent message = %q, want cached hello", sent.Message)
|
|
}
|
|
if via, ok := sent.GetViaBotID(); !ok || via != f.bot.ID {
|
|
t.Fatalf("cached via_bot_id = %d,%v, want %d,true", via, ok, f.bot.ID)
|
|
}
|
|
}
|
|
|
|
func TestInlineBotServerCacheSkipsPrivateAndZeroCache(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
private bool
|
|
cacheTime int
|
|
}{
|
|
{name: "private", private: true, cacheTime: 60},
|
|
{name: "zero cache", private: false, cacheTime: 0},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
req := &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: tc.name,
|
|
}
|
|
|
|
firstCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := f.router.onMessagesGetInlineBotResults(ownerCtx, req)
|
|
firstCh <- err
|
|
}()
|
|
firstID := waitInlineBotQuery(t, f.router)
|
|
setReq := &tg.MessagesSetInlineBotResultsRequest{
|
|
Private: tc.private,
|
|
QueryID: firstID,
|
|
CacheTime: tc.cacheTime,
|
|
Results: []tg.InputBotInlineResultClass{inlineArticleResult("miss-1", "first")},
|
|
}
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, setReq); err != nil || !ok {
|
|
t.Fatalf("set first no-cache results = %v,%v, want true,nil", ok, err)
|
|
}
|
|
select {
|
|
case err := <-firstCh:
|
|
if err != nil {
|
|
t.Fatalf("first no-cache get: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("first no-cache get did not resolve")
|
|
}
|
|
|
|
secondCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := f.router.onMessagesGetInlineBotResults(ownerCtx, req)
|
|
secondCh <- err
|
|
}()
|
|
secondID := waitInlineBotQuery(t, f.router)
|
|
if secondID == firstID {
|
|
t.Fatalf("second query reused first id %d", firstID)
|
|
}
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: secondID,
|
|
CacheTime: 1,
|
|
Results: []tg.InputBotInlineResultClass{inlineArticleResult("miss-2", "second")},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set second no-cache results = %v,%v, want true,nil", ok, err)
|
|
}
|
|
select {
|
|
case err := <-secondCh:
|
|
if err != nil {
|
|
t.Fatalf("second no-cache get: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("second no-cache get did not resolve")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInlineBotServerCacheSeparatesOffsets(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
firstReq := &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "pages",
|
|
}
|
|
firstCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := f.router.onMessagesGetInlineBotResults(ownerCtx, firstReq)
|
|
firstCh <- err
|
|
}()
|
|
firstID := waitInlineBotQuery(t, f.router)
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: firstID,
|
|
CacheTime: 60,
|
|
NextOffset: "page2",
|
|
Results: []tg.InputBotInlineResultClass{inlineArticleResult("page-1", "page one")},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set first page results = %v,%v, want true,nil", ok, err)
|
|
}
|
|
select {
|
|
case err := <-firstCh:
|
|
if err != nil {
|
|
t.Fatalf("first page get: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("first page get did not resolve")
|
|
}
|
|
|
|
secondReq := &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "pages",
|
|
Offset: "page2",
|
|
}
|
|
secondCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := f.router.onMessagesGetInlineBotResults(ownerCtx, secondReq)
|
|
secondCh <- err
|
|
}()
|
|
secondID := waitInlineBotQuery(t, f.router)
|
|
if secondID == firstID {
|
|
t.Fatalf("second page reused first id %d", firstID)
|
|
}
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: secondID,
|
|
CacheTime: 60,
|
|
Results: []tg.InputBotInlineResultClass{inlineArticleResult("page-2", "page two")},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set second page results = %v,%v, want true,nil", ok, err)
|
|
}
|
|
select {
|
|
case err := <-secondCh:
|
|
if err != nil {
|
|
t.Fatalf("second page get: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("second page get did not resolve")
|
|
}
|
|
}
|
|
|
|
func TestInlineBotValidation(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
queryID, _ := f.router.inlines.register(time.Now(), f.bot.ID, f.owner.ID, domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID})
|
|
|
|
if _, err := f.router.onMessagesSetInlineBotResults(WithUserID(ctx, f.owner.ID), &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
}); err == nil || !strings.Contains(err.Error(), "USER_BOT_REQUIRED") {
|
|
t.Fatalf("non-bot set inline err = %v, want USER_BOT_REQUIRED", err)
|
|
}
|
|
|
|
if _, err := f.router.onMessagesSetInlineBotResults(WithUserID(ctx, f.bot.ID), &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineArticleResult("dup", "one"),
|
|
inlineArticleResult("dup", "two"),
|
|
},
|
|
}); err == nil || !strings.Contains(err.Error(), "RESULT_ID_DUPLICATE") {
|
|
t.Fatalf("duplicate inline result err = %v, want RESULT_ID_DUPLICATE", err)
|
|
}
|
|
|
|
disabledBot, _, err := f.bots.CreateBot(ctx, f.owner.ID, "Disabled Inline Bot", "disabled_inline_bot")
|
|
if err != nil {
|
|
t.Fatalf("create disabled bot: %v", err)
|
|
}
|
|
if _, err := f.router.onMessagesGetInlineBotResults(WithUserID(ctx, f.owner.ID), &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(disabledBot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "hello",
|
|
}); err == nil || !strings.Contains(err.Error(), "BOT_INLINE_DISABLED") {
|
|
t.Fatalf("disabled inline err = %v, want BOT_INLINE_DISABLED", err)
|
|
}
|
|
|
|
if _, err := f.bots.SetInlineGeo(ctx, f.bot.ID, true); err != nil {
|
|
t.Fatalf("enable inline geo: %v", err)
|
|
}
|
|
badGeoReq := &tg.MessagesGetInlineBotResultsRequest{
|
|
Bot: inputUser(f.bot),
|
|
Peer: inputPeerUser(f.peer),
|
|
Query: "bad geo",
|
|
}
|
|
badGeoReq.SetGeoPoint(&tg.InputGeoPoint{Lat: 91, Long: 0})
|
|
if _, err := f.router.onMessagesGetInlineBotResults(WithUserID(ctx, f.owner.ID), badGeoReq); err == nil || !strings.Contains(err.Error(), "MEDIA_INVALID") {
|
|
t.Fatalf("bad inline geo query err = %v, want MEDIA_INVALID", err)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
result tg.InputBotInlineResultClass
|
|
want string
|
|
}{
|
|
{name: "geo live fields", result: inlineGeoResultWithPeriod("geo-live", 1, 2), want: "MEDIA_INVALID"},
|
|
{name: "empty venue title", result: inlineVenueResult("venue-empty", "", 1, 2), want: "MEDIA_EMPTY"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
queryID, _ := f.router.inlines.register(time.Now(), f.bot.ID, f.owner.ID, domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID})
|
|
if _, err := f.router.onMessagesSetInlineBotResults(WithUserID(ctx, f.bot.ID), &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
tc.result,
|
|
},
|
|
}); err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("inline media validation err = %v, want %s", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
switchPMCases := []struct {
|
|
name string
|
|
pm tg.InlineBotSwitchPM
|
|
want string
|
|
}{
|
|
{name: "empty text", pm: tg.InlineBotSwitchPM{Text: "", StartParam: "start"}, want: "SWITCH_PM_TEXT_EMPTY"},
|
|
{name: "empty start", pm: tg.InlineBotSwitchPM{Text: "Start", StartParam: ""}, want: "START_PARAM_EMPTY"},
|
|
{name: "invalid start", pm: tg.InlineBotSwitchPM{Text: "Start", StartParam: "bad space"}, want: "START_PARAM_INVALID"},
|
|
{name: "long start", pm: tg.InlineBotSwitchPM{Text: "Start", StartParam: strings.Repeat("a", domain.MaxStartParamLen+1)}, want: "START_PARAM_INVALID"},
|
|
{name: "long text", pm: tg.InlineBotSwitchPM{Text: strings.Repeat("x", domain.MaxBotInlineSwitchTextLen+1), StartParam: "start"}, want: "BUTTON_INVALID"},
|
|
}
|
|
for _, tc := range switchPMCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
queryID, _ := f.router.inlines.register(time.Now(), f.bot.ID, f.owner.ID, domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID})
|
|
req := &tg.MessagesSetInlineBotResultsRequest{QueryID: queryID}
|
|
req.SetSwitchPm(tc.pm)
|
|
if _, err := f.router.onMessagesSetInlineBotResults(WithUserID(ctx, f.bot.ID), req); err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("switch_pm err = %v, want %s", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
webviewQueryID, _ := f.router.inlines.register(time.Now(), f.bot.ID, f.owner.ID, domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID})
|
|
webviewReq := &tg.MessagesSetInlineBotResultsRequest{QueryID: webviewQueryID}
|
|
webviewReq.SetSwitchWebview(tg.InlineBotWebView{Text: "Open", URL: "https://example.test/app"})
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(WithUserID(ctx, f.bot.ID), webviewReq); err != nil || !ok {
|
|
t.Fatalf("switch_webview set = %v,%v, want true,nil", ok, err)
|
|
}
|
|
results, ok := f.router.inlines.resultsForQueryContext(ctx, time.Now(), f.owner.ID, webviewQueryID)
|
|
if !ok || results.SwitchWeb == nil || results.SwitchWeb.Text != "Open" || results.SwitchWeb.URL != "https://example.test/app" {
|
|
t.Fatalf("switch_webview results = %+v ok=%v", results.SwitchWeb, ok)
|
|
}
|
|
tgResults := f.router.tgBotInlineResults(ctx, f.owner.ID, results)
|
|
gotSwitchWeb, ok := tgResults.GetSwitchWebview()
|
|
if !ok || gotSwitchWeb.Text != "Open" || gotSwitchWeb.URL != "https://example.test/app" {
|
|
t.Fatalf("tg switch_webview = %+v,%v", gotSwitchWeb, ok)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
web tg.InlineBotWebView
|
|
want string
|
|
}{
|
|
{name: "empty text", web: tg.InlineBotWebView{Text: "", URL: "https://example.test/app"}, want: "BUTTON_INVALID"},
|
|
{name: "bad url", web: tg.InlineBotWebView{Text: "Open", URL: "http://example.test/app"}, want: "SWITCH_WEBVIEW_URL_INVALID"},
|
|
} {
|
|
t.Run("switch_webview "+tc.name, func(t *testing.T) {
|
|
queryID, _ := f.router.inlines.register(time.Now(), f.bot.ID, f.owner.ID, domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID})
|
|
req := &tg.MessagesSetInlineBotResultsRequest{QueryID: queryID}
|
|
req.SetSwitchWebview(tc.web)
|
|
if _, err := f.router.onMessagesSetInlineBotResults(WithUserID(ctx, f.bot.ID), req); err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("switch_webview err = %v, want %s", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMessagesRequestSimpleWebViewURLOnly(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
req := &tg.MessagesRequestSimpleWebViewRequest{
|
|
Bot: inputUser(f.bot),
|
|
Platform: "tdesktop",
|
|
}
|
|
req.SetFromSwitchWebview(true)
|
|
req.SetURL("https://example.test/app")
|
|
req.SetThemeParams(tg.DataJSON{Data: "{}"})
|
|
|
|
got, err := f.router.onMessagesRequestSimpleWebView(WithUserID(ctx, f.owner.ID), req)
|
|
if err != nil {
|
|
t.Fatalf("request simple webview: %v", err)
|
|
}
|
|
if got.URL != "https://example.test/app" {
|
|
t.Fatalf("webview url = %q, want request url", got.URL)
|
|
}
|
|
if _, ok := got.GetQueryID(); ok {
|
|
t.Fatal("webview query_id set, want URL-only result")
|
|
}
|
|
|
|
req.SetURL("http://example.test/app")
|
|
if _, err := f.router.onMessagesRequestSimpleWebView(WithUserID(ctx, f.owner.ID), req); err == nil || !strings.Contains(err.Error(), "URL_INVALID") {
|
|
t.Fatalf("bad webview url err = %v, want URL_INVALID", err)
|
|
}
|
|
req.SetURL("https://example.test/app")
|
|
req.Bot = inputUser(f.owner)
|
|
if _, err := f.router.onMessagesRequestSimpleWebView(WithUserID(ctx, f.owner.ID), req); err == nil || !strings.Contains(err.Error(), "BOT_INVALID") {
|
|
t.Fatalf("non-bot webview err = %v, want BOT_INVALID", err)
|
|
}
|
|
}
|
|
|
|
func channelMessageFromUpdates(t *testing.T, updates tg.UpdatesClass) (*tg.Message, *tg.Updates) {
|
|
t.Helper()
|
|
full, ok := updates.(*tg.Updates)
|
|
if !ok {
|
|
t.Fatalf("updates = %T, want *tg.Updates", updates)
|
|
}
|
|
for _, update := range full.Updates {
|
|
if newMessage, ok := update.(*tg.UpdateNewChannelMessage); ok {
|
|
if msg, ok := newMessage.Message.(*tg.Message); ok {
|
|
return msg, full
|
|
}
|
|
}
|
|
}
|
|
t.Fatalf("updates missing new channel message: %+v", full.Updates)
|
|
return nil, nil
|
|
}
|
|
|
|
func assertUpdatesContainBotUser(t *testing.T, updates *tg.Updates, botID int64) {
|
|
t.Helper()
|
|
for _, u := range updates.Users {
|
|
if user, ok := u.(*tg.User); ok && user.ID == botID && user.Bot {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("updates users missing via bot %d: %+v", botID, updates.Users)
|
|
}
|
|
|
|
func assertChannelMessagesContainBotUser(t *testing.T, messages *tg.MessagesChannelMessages, botID int64) {
|
|
t.Helper()
|
|
for _, u := range messages.Users {
|
|
if user, ok := u.(*tg.User); ok && user.ID == botID && user.Bot {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("channel messages users missing via bot %d: %+v", botID, messages.Users)
|
|
}
|
|
|
|
func assertTGInlineReplyMarkup(t *testing.T, msg *tg.Message, wantText string, wantData []byte) {
|
|
t.Helper()
|
|
markupClass, ok := msg.GetReplyMarkup()
|
|
if !ok {
|
|
t.Fatalf("message %d missing reply_markup", msg.ID)
|
|
}
|
|
markup, ok := markupClass.(*tg.ReplyInlineMarkup)
|
|
if !ok {
|
|
t.Fatalf("reply_markup = %T, want *tg.ReplyInlineMarkup", markupClass)
|
|
}
|
|
if len(markup.Rows) != 1 || len(markup.Rows[0].Buttons) != 1 {
|
|
t.Fatalf("reply_markup rows = %+v, want one callback button", markup.Rows)
|
|
}
|
|
button, ok := markup.Rows[0].Buttons[0].(*tg.KeyboardButtonCallback)
|
|
if !ok {
|
|
t.Fatalf("reply_markup button = %T, want callback", markup.Rows[0].Buttons[0])
|
|
}
|
|
if button.Text != wantText || !bytes.Equal(button.Data, wantData) {
|
|
t.Fatalf("reply_markup button = %q/%v, want %q/%v", button.Text, button.Data, wantText, wantData)
|
|
}
|
|
}
|
|
|
|
func assertDomainInlineReplyMarkup(t *testing.T, markup *domain.MessageReplyMarkup, wantText string, wantData []byte) {
|
|
t.Helper()
|
|
if markup == nil || len(markup.Inline) != 1 || len(markup.Inline[0]) != 1 {
|
|
t.Fatalf("domain reply markup = %+v, want one callback button", markup)
|
|
}
|
|
button := markup.Inline[0][0]
|
|
if button.Type != domain.MarkupButtonCallback || button.Text != wantText || !bytes.Equal(button.Data, wantData) {
|
|
t.Fatalf("domain reply markup button = %+v, want callback %q/%v", button, wantText, wantData)
|
|
}
|
|
}
|
|
|
|
func waitInlineBotQuery(t *testing.T, r *Router) int64 {
|
|
t.Helper()
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
r.inlines.mu.Lock()
|
|
for queryID, pending := range r.inlines.pending {
|
|
if pending.results == nil {
|
|
r.inlines.mu.Unlock()
|
|
return queryID
|
|
}
|
|
}
|
|
r.inlines.mu.Unlock()
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
t.Fatal("inline query was not registered")
|
|
return 0
|
|
}
|
|
|
|
func inputPeerUser(user domain.User) *tg.InputPeerUser {
|
|
return &tg.InputPeerUser{UserID: user.ID, AccessHash: user.AccessHash}
|
|
}
|
|
|
|
func inlineArticleResult(id, message string) tg.InputBotInlineResultClass {
|
|
return &tg.InputBotInlineResult{
|
|
ID: id,
|
|
Type: "article",
|
|
Title: id,
|
|
SendMessage: &tg.InputBotInlineMessageText{Message: message},
|
|
}
|
|
}
|
|
|
|
func inlineArticleResultWithCallback(id, message, button string, data []byte) tg.InputBotInlineResultClass {
|
|
result := inlineArticleResult(id, message).(*tg.InputBotInlineResult)
|
|
msg := result.SendMessage.(*tg.InputBotInlineMessageText)
|
|
msg.SetReplyMarkup(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{
|
|
Buttons: []tg.KeyboardButtonClass{&tg.KeyboardButtonCallback{Text: button, Data: data}},
|
|
}}})
|
|
return result
|
|
}
|
|
|
|
func inlineArticleResultWithWebThumb(id, message string) *tg.InputBotInlineResult {
|
|
result := &tg.InputBotInlineResult{
|
|
ID: id,
|
|
Type: "article",
|
|
Title: "Preview",
|
|
Description: "External preview",
|
|
SendMessage: &tg.InputBotInlineMessageText{Message: message},
|
|
}
|
|
result.SetURL("https://example.test/result")
|
|
result.SetThumb(tg.InputWebDocument{
|
|
URL: "https://example.test/thumb.jpg",
|
|
Size: 1234,
|
|
MimeType: "image/jpeg",
|
|
Attributes: []tg.DocumentAttributeClass{
|
|
&tg.DocumentAttributeImageSize{W: 96, H: 96},
|
|
},
|
|
})
|
|
return result
|
|
}
|
|
|
|
func inlineArticleResultWithWebContent(id, message string) *tg.InputBotInlineResult {
|
|
result := inlineArticleResultWithWebThumb(id, message)
|
|
result.SetContent(tg.InputWebDocument{
|
|
URL: "https://example.test/content.png",
|
|
Size: 4096,
|
|
MimeType: "image/png",
|
|
Attributes: []tg.DocumentAttributeClass{
|
|
&tg.DocumentAttributeImageSize{W: 320, H: 200},
|
|
},
|
|
})
|
|
return result
|
|
}
|
|
|
|
func inlineExternalPhotoContentResult(id, caption string) *tg.InputBotInlineResult {
|
|
result := &tg.InputBotInlineResult{
|
|
ID: id,
|
|
Type: "photo",
|
|
Title: "External Photo",
|
|
Description: "External photo content",
|
|
SendMessage: &tg.InputBotInlineMessageMediaAuto{
|
|
Message: caption,
|
|
},
|
|
}
|
|
result.SetThumb(tg.InputWebDocument{
|
|
URL: "https://example.test/photo-thumb.jpg",
|
|
Size: 512,
|
|
MimeType: "image/jpeg",
|
|
Attributes: []tg.DocumentAttributeClass{
|
|
&tg.DocumentAttributeImageSize{W: 90, H: 90},
|
|
},
|
|
})
|
|
result.SetContent(tg.InputWebDocument{
|
|
URL: "https://example.test/photo.png",
|
|
Size: 4096,
|
|
MimeType: "image/png",
|
|
Attributes: []tg.DocumentAttributeClass{
|
|
&tg.DocumentAttributeImageSize{W: 320, H: 200},
|
|
},
|
|
})
|
|
return result
|
|
}
|
|
|
|
func assertInlineArticleWebPreview(t *testing.T, res *tg.MessagesBotResults, queryID int64) {
|
|
t.Helper()
|
|
if res.QueryID != queryID || len(res.Results) != 1 {
|
|
t.Fatalf("inline results = query %d len %d, want query %d len 1", res.QueryID, len(res.Results), queryID)
|
|
}
|
|
result, ok := res.Results[0].(*tg.BotInlineResult)
|
|
if !ok {
|
|
t.Fatalf("inline result = %T, want *tg.BotInlineResult", res.Results[0])
|
|
}
|
|
if result.ID != "article-web" || result.Type != "article" {
|
|
t.Fatalf("inline result id/type = %q/%q, want article-web/article", result.ID, result.Type)
|
|
}
|
|
if gotURL, ok := result.GetURL(); !ok || gotURL != "https://example.test/result" {
|
|
t.Fatalf("inline result url = %q,%v, want https://example.test/result,true", gotURL, ok)
|
|
}
|
|
rawThumb, ok := result.GetThumb()
|
|
if !ok {
|
|
t.Fatal("inline result missing thumb")
|
|
}
|
|
thumb, ok := rawThumb.(*tg.WebDocument)
|
|
if !ok {
|
|
t.Fatalf("inline result thumb = %T, want *tg.WebDocument", rawThumb)
|
|
}
|
|
if thumb.URL != "https://example.test/thumb.jpg" || thumb.AccessHash == 0 || thumb.Size != 1234 || thumb.MimeType != "image/jpeg" {
|
|
t.Fatalf("thumb = url %q size %d mime %q", thumb.URL, thumb.Size, thumb.MimeType)
|
|
}
|
|
if len(thumb.Attributes) != 1 {
|
|
t.Fatalf("thumb attributes = %d, want 1", len(thumb.Attributes))
|
|
}
|
|
size, ok := thumb.Attributes[0].(*tg.DocumentAttributeImageSize)
|
|
if !ok || size.W != 96 || size.H != 96 {
|
|
t.Fatalf("thumb size attr = %#v, want 96x96", thumb.Attributes[0])
|
|
}
|
|
msg, ok := result.SendMessage.(*tg.BotInlineMessageText)
|
|
if !ok || msg.Message != "preview text" {
|
|
t.Fatalf("inline send message = %T/%#v, want text preview text", result.SendMessage, result.SendMessage)
|
|
}
|
|
}
|
|
|
|
func inlinePhotoResult(id string, photo domain.Photo, caption string) tg.InputBotInlineResultClass {
|
|
return &tg.InputBotInlineResultPhoto{
|
|
ID: id,
|
|
Type: "photo",
|
|
Photo: &tg.InputPhoto{ID: photo.ID, AccessHash: photo.AccessHash},
|
|
SendMessage: &tg.InputBotInlineMessageMediaAuto{
|
|
Message: caption,
|
|
},
|
|
}
|
|
}
|
|
|
|
func inlineDocumentResult(id string, doc domain.Document, caption string) tg.InputBotInlineResultClass {
|
|
return &tg.InputBotInlineResultDocument{
|
|
ID: id,
|
|
Type: "file",
|
|
Title: id,
|
|
Description: doc.MimeType,
|
|
Document: &tg.InputDocument{ID: doc.ID, AccessHash: doc.AccessHash},
|
|
SendMessage: &tg.InputBotInlineMessageMediaAuto{
|
|
Message: caption,
|
|
},
|
|
}
|
|
}
|
|
|
|
func inlineGeoResult(id string, lat, long float64) tg.InputBotInlineResultClass {
|
|
return &tg.InputBotInlineResult{
|
|
ID: id,
|
|
Type: "geo",
|
|
Title: id,
|
|
Description: "Location",
|
|
SendMessage: &tg.InputBotInlineMessageMediaGeo{
|
|
GeoPoint: &tg.InputGeoPoint{Lat: lat, Long: long},
|
|
},
|
|
}
|
|
}
|
|
|
|
func inlineGeoResultWithPeriod(id string, lat, long float64) tg.InputBotInlineResultClass {
|
|
msg := &tg.InputBotInlineMessageMediaGeo{GeoPoint: &tg.InputGeoPoint{Lat: lat, Long: long}}
|
|
msg.SetPeriod(60)
|
|
return &tg.InputBotInlineResult{
|
|
ID: id,
|
|
Type: "geo",
|
|
Title: id,
|
|
SendMessage: msg,
|
|
}
|
|
}
|
|
|
|
func inlineVenueResult(id, title string, lat, long float64) tg.InputBotInlineResultClass {
|
|
return &tg.InputBotInlineResult{
|
|
ID: id,
|
|
Type: "venue",
|
|
Title: title,
|
|
Description: "Venue",
|
|
SendMessage: &tg.InputBotInlineMessageMediaVenue{
|
|
GeoPoint: &tg.InputGeoPoint{Lat: lat, Long: long},
|
|
Title: title,
|
|
Address: "Inline Street",
|
|
Provider: "gplaces",
|
|
VenueID: "venue-id",
|
|
VenueType: "cafe",
|
|
},
|
|
}
|
|
}
|
|
|
|
func inlineContactResult(id, phone, first, last, vcard string) *tg.InputBotInlineResult {
|
|
return &tg.InputBotInlineResult{
|
|
ID: id,
|
|
Type: "contact",
|
|
Title: first,
|
|
Description: phone,
|
|
SendMessage: &tg.InputBotInlineMessageMediaContact{
|
|
PhoneNumber: phone,
|
|
FirstName: first,
|
|
LastName: last,
|
|
Vcard: vcard,
|
|
},
|
|
}
|
|
}
|
|
|
|
func inlineContactResultWithCallback(id, phone, first, last string, data []byte) tg.InputBotInlineResultClass {
|
|
result := inlineContactResult(id, phone, first, last, "BEGIN:VCARD\nFN:"+first+" "+last+"\nEND:VCARD")
|
|
msg := result.SendMessage.(*tg.InputBotInlineMessageMediaContact)
|
|
msg.SetReplyMarkup(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{
|
|
Buttons: []tg.KeyboardButtonClass{&tg.KeyboardButtonCallback{Text: "Contact", Data: data}},
|
|
}}})
|
|
return result
|
|
}
|
|
|
|
func messageFromUpdates(t *testing.T, updates tg.UpdatesClass) *tg.Message {
|
|
t.Helper()
|
|
full, ok := updates.(*tg.Updates)
|
|
if !ok {
|
|
t.Fatalf("updates = %T, want *tg.Updates", updates)
|
|
}
|
|
for _, update := range full.Updates {
|
|
if newMessage, ok := update.(*tg.UpdateNewMessage); ok {
|
|
if msg, ok := newMessage.Message.(*tg.Message); ok {
|
|
return msg
|
|
}
|
|
}
|
|
}
|
|
t.Fatalf("updates missing new message: %+v", full.Updates)
|
|
return nil
|
|
}
|
|
|
|
func messagesFromClass(in tg.MessagesMessagesClass) []tg.MessageClass {
|
|
if out, ok := in.(*tg.MessagesMessages); ok {
|
|
return out.Messages
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func channelMessagesFromClass(in tg.MessagesMessagesClass) []tg.MessageClass {
|
|
if out, ok := in.(*tg.MessagesChannelMessages); ok {
|
|
return out.Messages
|
|
}
|
|
return nil
|
|
}
|