879 lines
33 KiB
Go
879 lines
33 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"github.com/iamxvbaba/td/bin"
|
|
"github.com/iamxvbaba/td/clock"
|
|
"github.com/iamxvbaba/td/proto"
|
|
"github.com/iamxvbaba/td/tg"
|
|
"github.com/iamxvbaba/td/tgerr"
|
|
"go.uber.org/zap/zaptest"
|
|
"strings"
|
|
appchannels "telesrv/internal/app/channels"
|
|
appcommunities "telesrv/internal/app/communities"
|
|
appusers "telesrv/internal/app/users"
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store/memory"
|
|
"testing"
|
|
)
|
|
|
|
func TestMessagesSearchGlobalRestrictsCommunityScope(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
owner, err := users.Create(ctx, domain.User{AccessHash: 84, Phone: "15550000084", FirstName: "Owner"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
viewer, err := users.Create(ctx, domain.User{AccessHash: 85, Phone: "15550000085", FirstName: "Viewer"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
channels := memory.NewChannelStore()
|
|
channelService := appchannels.NewService(channels)
|
|
linked, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{CreatorUserID: owner.ID, Title: "Linked", Megagroup: true, MemberUserIDs: []int64{viewer.ID}, Date: 100})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
publicPreview, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{CreatorUserID: owner.ID, Title: "Public Preview", Megagroup: true, Date: 101})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
publicPreview.Channel, err = channelService.UpdateUsername(ctx, owner.ID, domain.UpdateChannelUsernameRequest{
|
|
UserID: owner.ID, ChannelID: publicPreview.Channel.ID, Username: "community_public_preview",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
outside, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{CreatorUserID: owner.ID, Title: "Outside", Megagroup: true, Date: 101})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
communityService := appcommunities.NewService(memory.NewCommunityStore(users, channels, nil, nil))
|
|
community, err := communityService.Create(ctx, owner.ID, domain.CreateCommunityRequest{
|
|
Title: "Scope", InitialPeer: domain.Peer{Type: domain.PeerTypeChannel, ID: linked.Channel.ID},
|
|
Visibility: domain.CommunityPeerVisible, Date: 102,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := communityService.TogglePeerLink(ctx, owner.ID, domain.CommunityTogglePeerLinkRequest{
|
|
CommunityID: community.Community.ID,
|
|
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: publicPreview.Channel.ID},
|
|
Visibility: domain.CommunityPeerVisible,
|
|
Date: 103,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := New(Config{}, Deps{Users: appusers.NewService(users), Channels: channelService, Communities: communityService}, zaptest.NewLogger(t), clock.System)
|
|
for i, channel := range []domain.Channel{linked.Channel, publicPreview.Channel, outside.Channel} {
|
|
_, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{
|
|
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}, Message: "scoped result", RandomID: int64(9000 + i),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send channel %d: %v", channel.ID, err)
|
|
}
|
|
}
|
|
req := &tg.MessagesSearchGlobalRequest{
|
|
Q: "scoped",
|
|
Filter: &tg.InputMessagesFilterEmpty{},
|
|
OffsetPeer: &tg.InputPeerEmpty{},
|
|
Limit: 20,
|
|
}
|
|
req.SetCommunity(&tg.InputChannel{ChannelID: community.Community.ID, AccessHash: community.Community.AccessHash})
|
|
|
|
result, err := r.onMessagesSearchGlobal(WithUserID(ctx, viewer.ID), req)
|
|
if err != nil {
|
|
t.Fatalf("community-scoped messages.searchGlobal: %v", err)
|
|
}
|
|
response, ok := result.(*tg.MessagesMessages)
|
|
if !ok || len(response.Messages) != 2 {
|
|
t.Fatalf("community search result = %#v, want joined and public-preview linked messages", result)
|
|
}
|
|
gotChannels := map[int64]bool{}
|
|
for _, item := range response.Messages {
|
|
message, ok := item.(*tg.Message)
|
|
if !ok {
|
|
t.Fatalf("community search message = %#v, want channel message", item)
|
|
}
|
|
peer, ok := message.PeerID.(*tg.PeerChannel)
|
|
if !ok {
|
|
t.Fatalf("community search message peer = %#v", message.PeerID)
|
|
}
|
|
gotChannels[peer.ChannelID] = true
|
|
}
|
|
if !gotChannels[linked.Channel.ID] || !gotChannels[publicPreview.Channel.ID] || gotChannels[outside.Channel.ID] {
|
|
t.Fatalf("community search channels = %+v", gotChannels)
|
|
}
|
|
|
|
emptyReq := &tg.MessagesSearchGlobalRequest{
|
|
Filter: &tg.InputMessagesFilterEmpty{},
|
|
OffsetPeer: &tg.InputPeerEmpty{},
|
|
Limit: 20,
|
|
}
|
|
emptyReq.SetCommunity(&tg.InputChannel{ChannelID: community.Community.ID, AccessHash: community.Community.AccessHash})
|
|
emptyResult, err := r.onMessagesSearchGlobal(WithUserID(ctx, viewer.ID), emptyReq)
|
|
if err != nil {
|
|
t.Fatalf("empty community-scoped messages.searchGlobal: %v", err)
|
|
}
|
|
emptyResponse, ok := emptyResult.(*tg.MessagesMessages)
|
|
if !ok || len(emptyResponse.Messages) != 0 || len(emptyResponse.Chats) != 1 {
|
|
t.Fatalf("empty community search result = %#v, want empty messages with validated Community chat", emptyResult)
|
|
}
|
|
if got, ok := emptyResponse.Chats[0].(*tg.Community); !ok || got.ID != community.Community.ID {
|
|
t.Fatalf("empty community search chat = %#v, want Community %d", emptyResponse.Chats[0], community.Community.ID)
|
|
}
|
|
|
|
badHashReq := &tg.MessagesSearchGlobalRequest{
|
|
Filter: &tg.InputMessagesFilterEmpty{},
|
|
OffsetPeer: &tg.InputPeerEmpty{},
|
|
Limit: 20,
|
|
}
|
|
badHashReq.SetCommunity(&tg.InputChannel{ChannelID: community.Community.ID, AccessHash: community.Community.AccessHash + 1})
|
|
if _, err := r.onMessagesSearchGlobal(WithUserID(ctx, viewer.ID), badHashReq); err == nil || !tgerr.Is(err, "CHANNEL_PRIVATE") {
|
|
t.Fatalf("empty community search wrong access hash err = %v, want CHANNEL_PRIVATE", err)
|
|
}
|
|
}
|
|
|
|
func TestMessagesSearchChannelPeerReturnsSingleCopyMessages(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 35, Phone: "15550002035", FirstName: "Owner"})
|
|
friend, _ := userStore.Create(ctx, domain.User{AccessHash: 36, Phone: "15550002036", FirstName: "Friend"})
|
|
channelStore := memory.NewChannelStore()
|
|
r := New(Config{}, Deps{
|
|
Users: appusers.NewService(userStore),
|
|
Channels: appchannels.NewService(channelStore),
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
created, err := r.onMessagesCreateChat(WithUserID(ctx, owner.ID), &tg.MessagesCreateChatRequest{
|
|
Users: []tg.InputUserClass{&tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash}},
|
|
Title: "RPC Search Group",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create chat: %v", err)
|
|
}
|
|
channel := created.Updates.(*tg.Updates).Chats[0].(*tg.Channel)
|
|
pinnedMsgID := 0
|
|
for _, item := range []struct {
|
|
userID int64
|
|
text string
|
|
random int64
|
|
}{
|
|
{owner.ID, "needle from owner", 5001},
|
|
{friend.ID, "not this one", 5002},
|
|
{friend.ID, "needle from friend", 5003},
|
|
} {
|
|
sent, err := r.onMessagesSendMessage(WithUserID(ctx, item.userID), &tg.MessagesSendMessageRequest{
|
|
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
|
Message: item.text,
|
|
RandomID: item.random,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send %q: %v", item.text, err)
|
|
}
|
|
if item.text == "not this one" {
|
|
channelUpdates := sent.(*tg.Updates)
|
|
if len(channelUpdates.Updates) == 0 {
|
|
t.Fatalf("send %q updates = %+v, want updateMessageID", item.text, channelUpdates.Updates)
|
|
}
|
|
pinnedMsgID = channelUpdates.Updates[0].(*tg.UpdateMessageID).ID
|
|
}
|
|
}
|
|
|
|
req := &tg.MessagesSearchRequest{
|
|
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
|
Q: "needle",
|
|
Filter: &tg.InputMessagesFilterEmpty{},
|
|
Limit: 10,
|
|
}
|
|
var in bin.Buffer
|
|
if err := req.Encode(&in); err != nil {
|
|
t.Fatalf("encode search: %v", err)
|
|
}
|
|
enc, err := r.Dispatch(WithUserID(ctx, friend.ID), [8]byte{}, 0, &in)
|
|
if err != nil {
|
|
t.Fatalf("dispatch search: %v", err)
|
|
}
|
|
messages, chats, users := searchMessagesPayload(t, enc)
|
|
if len(messages) != 2 || len(chats) != 1 || len(users) < 2 {
|
|
t.Fatalf("search payload sizes = messages %d chats %d users %d, want 2/1/2+", len(messages), len(chats), len(users))
|
|
}
|
|
for _, msg := range messages {
|
|
item := msg.(*tg.Message)
|
|
if !strings.Contains(item.Message, "needle") {
|
|
t.Fatalf("search result message = %q, want only needle hits", item.Message)
|
|
}
|
|
}
|
|
|
|
fromReq := *req
|
|
fromReq.SetFromID(&tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash})
|
|
in.Reset()
|
|
if err := fromReq.Encode(&in); err != nil {
|
|
t.Fatalf("encode from search: %v", err)
|
|
}
|
|
enc, err = r.Dispatch(WithUserID(ctx, owner.ID), [8]byte{}, 0, &in)
|
|
if err != nil {
|
|
t.Fatalf("dispatch from search: %v", err)
|
|
}
|
|
messages, _, _ = searchMessagesPayload(t, enc)
|
|
if len(messages) != 1 || messages[0].(*tg.Message).Message != "needle from friend" {
|
|
t.Fatalf("from search messages = %+v, want friend needle only", messages)
|
|
}
|
|
|
|
mediaCountReq := &tg.MessagesSearchRequest{
|
|
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
|
Filter: &tg.InputMessagesFilterPhotos{},
|
|
Limit: 0,
|
|
}
|
|
// Match DrKLO's ordinary messages.search wire shape: flags.3 is present
|
|
// even though the saved_reaction vector is empty.
|
|
mediaCountReq.SetSavedReaction([]tg.ReactionClass{})
|
|
in.Reset()
|
|
if err := mediaCountReq.Encode(&in); err != nil {
|
|
t.Fatalf("encode shared media count search: %v", err)
|
|
}
|
|
enc, err = r.Dispatch(WithUserID(ctx, friend.ID), [8]byte{}, 0, &in)
|
|
if err != nil {
|
|
t.Fatalf("dispatch shared media count search: %v", err)
|
|
}
|
|
channelMessages, ok := enc.(*tg.MessagesChannelMessages)
|
|
if !ok {
|
|
t.Fatalf("shared media count search result = %T, want messages.channelMessages", enc)
|
|
}
|
|
if channelMessages.Count != 0 || len(channelMessages.Messages) != 0 {
|
|
t.Fatalf("shared media count search = count %d messages %d, want empty without media store", channelMessages.Count, len(channelMessages.Messages))
|
|
}
|
|
|
|
if _, err := r.onMessagesUpdatePinnedMessage(WithUserID(ctx, owner.ID), &tg.MessagesUpdatePinnedMessageRequest{
|
|
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
|
ID: pinnedMsgID,
|
|
}); err != nil {
|
|
t.Fatalf("pin channel message: %v", err)
|
|
}
|
|
pinnedReq := &tg.MessagesSearchRequest{
|
|
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
|
Filter: &tg.InputMessagesFilterPinned{},
|
|
Limit: 40,
|
|
}
|
|
in.Reset()
|
|
if err := pinnedReq.Encode(&in); err != nil {
|
|
t.Fatalf("encode pinned search: %v", err)
|
|
}
|
|
enc, err = r.Dispatch(WithUserID(ctx, friend.ID), [8]byte{}, 0, &in)
|
|
if err != nil {
|
|
t.Fatalf("dispatch pinned search: %v", err)
|
|
}
|
|
pinnedMessages, _, _ := searchMessagesPayload(t, enc)
|
|
if len(pinnedMessages) != 1 {
|
|
t.Fatalf("pinned search returned %d messages, want 1", len(pinnedMessages))
|
|
}
|
|
pinnedMessage, ok := pinnedMessages[0].(*tg.Message)
|
|
if !ok || pinnedMessage.ID != pinnedMsgID || !pinnedMessage.GetPinned() {
|
|
t.Fatalf("pinned search message = %#v, want pinned message id=%d", pinnedMessages[0], pinnedMsgID)
|
|
}
|
|
}
|
|
|
|
func TestMessagesSearchChatPhotosDoesNotReturnOrdinaryChannelHistory(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 93500, Phone: "15550093500", FirstName: "Owner"})
|
|
channelStore := memory.NewChannelStore()
|
|
channelService := appchannels.NewService(channelStore)
|
|
r := New(Config{}, Deps{
|
|
Channels: channelService,
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
created, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
|
|
Title: "Chat Photos Count Guard", Megagroup: true, Date: 1700034000,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channel := created.Channel
|
|
for i := 0; i < 7; i++ {
|
|
if _, err := channelService.SendMessage(ctx, owner.ID, domain.SendChannelMessageRequest{
|
|
ChannelID: channel.ID,
|
|
RandomID: int64(1700034000 + i),
|
|
Message: "ordinary channel message",
|
|
Date: 1700034001 + i,
|
|
}); err != nil {
|
|
t.Fatalf("send ordinary message %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
req := &tg.MessagesSearchRequest{
|
|
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
|
Filter: &tg.InputMessagesFilterChatPhotos{},
|
|
Limit: 80,
|
|
}
|
|
var in bin.Buffer
|
|
if err := req.Encode(&in); err != nil {
|
|
t.Fatalf("encode chat photos search: %v", err)
|
|
}
|
|
enc, err := r.Dispatch(WithUserID(ctx, owner.ID), [8]byte{}, 0, &in)
|
|
if err != nil {
|
|
t.Fatalf("dispatch chat photos search: %v", err)
|
|
}
|
|
got, ok := enc.(*tg.MessagesChannelMessages)
|
|
if !ok {
|
|
t.Fatalf("chat photos search result = %T, want messages.channelMessages", enc)
|
|
}
|
|
if got.Count != 0 || len(got.Messages) != 0 {
|
|
t.Fatalf("chat photos search = count %d messages %d, want explicit empty stub", got.Count, len(got.Messages))
|
|
}
|
|
if len(got.Chats) != 1 {
|
|
t.Fatalf("chat photos search chats = %d, want current channel projection", len(got.Chats))
|
|
}
|
|
}
|
|
|
|
func TestMessagesGetSearchCountersUsesMediaCategoryCounts(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 93501, Phone: "15550093501", FirstName: "Owner"})
|
|
channelStore := memory.NewChannelStore()
|
|
channelService := appchannels.NewService(channelStore)
|
|
r := New(Config{}, Deps{
|
|
Channels: channelService,
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
created, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
|
|
Title: "Media Counters", Megagroup: true, Date: 1700035000,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channel := created.Channel
|
|
docMedia := func(id int64, attrs ...domain.DocumentAttribute) *domain.MessageMedia {
|
|
return &domain.MessageMedia{Kind: domain.MessageMediaKindDocument, Document: &domain.Document{ID: id, AccessHash: id + 10, Attributes: attrs}}
|
|
}
|
|
send := func(randomID int64, msg string, media *domain.MessageMedia, entities []domain.MessageEntity) {
|
|
if _, err := channelService.SendMessage(ctx, owner.ID, domain.SendChannelMessageRequest{
|
|
ChannelID: channel.ID,
|
|
RandomID: randomID,
|
|
Message: msg,
|
|
Media: media,
|
|
Entities: entities,
|
|
Date: 1700035000 + int(randomID),
|
|
}); err != nil {
|
|
t.Fatalf("send %d: %v", randomID, err)
|
|
}
|
|
}
|
|
send(1, "photo https://x.test", &domain.MessageMedia{Kind: domain.MessageMediaKindPhoto, Photo: &domain.Photo{ID: 1, AccessHash: 11}}, []domain.MessageEntity{{Type: domain.MessageEntityURL, Offset: 6, Length: 14}})
|
|
send(2, "video", docMedia(2, domain.DocumentAttribute{Kind: domain.DocAttrVideo, W: 1, H: 1}), nil)
|
|
send(3, "file", docMedia(3, domain.DocumentAttribute{Kind: domain.DocAttrFilename, FileName: "a.bin"}), nil)
|
|
send(4, "music", docMedia(4, domain.DocumentAttribute{Kind: domain.DocAttrAudio, Title: "song"}), nil)
|
|
send(5, "voice", docMedia(5, domain.DocumentAttribute{Kind: domain.DocAttrAudio, Voice: true}), nil)
|
|
send(6, "round", docMedia(6, domain.DocumentAttribute{Kind: domain.DocAttrVideo, RoundMessage: true}), nil)
|
|
gif := docMedia(7,
|
|
domain.DocumentAttribute{Kind: domain.DocAttrAnimated},
|
|
domain.DocumentAttribute{Kind: domain.DocAttrVideo, W: 320, H: 240, Duration: 1})
|
|
gif.Document.MimeType = "video/mp4"
|
|
send(7, "gif", gif, nil)
|
|
send(8, "poll", &domain.MessageMedia{Kind: domain.MessageMediaKindPoll}, nil)
|
|
|
|
filters := []tg.MessagesFilterClass{
|
|
&tg.InputMessagesFilterPhotos{},
|
|
&tg.InputMessagesFilterPhotoVideo{},
|
|
&tg.InputMessagesFilterDocument{},
|
|
&tg.InputMessagesFilterMusic{},
|
|
&tg.InputMessagesFilterURL{},
|
|
&tg.InputMessagesFilterGif{},
|
|
&tg.InputMessagesFilterVoice{},
|
|
&tg.InputMessagesFilterRoundVideo{},
|
|
&tg.InputMessagesFilterRoundVoice{},
|
|
&tg.InputMessagesFilterPoll{},
|
|
&tg.InputMessagesFilterChatPhotos{},
|
|
}
|
|
counters, err := r.onMessagesGetSearchCounters(WithUserID(ctx, owner.ID), &tg.MessagesGetSearchCountersRequest{
|
|
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
|
Filters: filters,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("messages.getSearchCounters: %v", err)
|
|
}
|
|
want := []int{1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 0}
|
|
if len(counters) != len(want) {
|
|
t.Fatalf("got %d counters, want %d", len(counters), len(want))
|
|
}
|
|
for i, counter := range counters {
|
|
if counter.Count != want[i] {
|
|
t.Fatalf("counter[%d] %T = %d, want %d", i, counter.Filter, counter.Count, want[i])
|
|
}
|
|
if counter.Inexact {
|
|
t.Fatalf("counter[%d] is inexact, want exact", i)
|
|
}
|
|
}
|
|
|
|
searchReq := &tg.MessagesSearchRequest{
|
|
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
|
Filter: &tg.InputMessagesFilterPhotoVideo{},
|
|
Limit: 0,
|
|
}
|
|
var in bin.Buffer
|
|
if err := searchReq.Encode(&in); err != nil {
|
|
t.Fatalf("encode shared media count search: %v", err)
|
|
}
|
|
enc, err := r.Dispatch(WithUserID(ctx, owner.ID), [8]byte{}, 0, &in)
|
|
if err != nil {
|
|
t.Fatalf("dispatch shared media count search: %v", err)
|
|
}
|
|
channelMessages, ok := enc.(*tg.MessagesChannelMessages)
|
|
if !ok {
|
|
t.Fatalf("shared media count search result = %T, want messages.channelMessages", enc)
|
|
}
|
|
if channelMessages.Count != 2 || len(channelMessages.Messages) != 0 {
|
|
t.Fatalf("shared media count search = count %d messages %d, want count 2 and no messages", channelMessages.Count, len(channelMessages.Messages))
|
|
}
|
|
}
|
|
|
|
func TestMessagesGetSearchCountersPropagatesMediaCountError(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 93502, Phone: "15550093502", FirstName: "Owner"})
|
|
channelStore := memory.NewChannelStore()
|
|
channelService := appchannels.NewService(channelStore)
|
|
created, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
|
|
Title: "Media Counter Error", Megagroup: true, Date: 1700035100,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
wantErr := errors.New("media count store down")
|
|
r := New(Config{}, Deps{
|
|
Channels: &failingMediaCountChannelsService{Service: channelService, err: wantErr},
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
|
|
_, err = r.onMessagesGetSearchCounters(WithUserID(ctx, owner.ID), &tg.MessagesGetSearchCountersRequest{
|
|
Peer: &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash},
|
|
Filters: []tg.MessagesFilterClass{&tg.InputMessagesFilterURL{}},
|
|
})
|
|
if !errors.Is(err, wantErr) {
|
|
t.Fatalf("messages.getSearchCounters error = %v, want %v", err, wantErr)
|
|
}
|
|
}
|
|
|
|
type failingMediaCountChannelsService struct {
|
|
*appchannels.Service
|
|
err error
|
|
}
|
|
|
|
func (s *failingMediaCountChannelsService) CountChannelMediaCategories(context.Context, int64, int64) (domain.MediaCategoryCounts, error) {
|
|
return nil, s.err
|
|
}
|
|
|
|
func TestMessagesSearchGlobalReturnsJoinedChannelMessages(t *testing.T) {
|
|
ctx := context.Background()
|
|
userStore := memory.NewUserStore()
|
|
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 91101, Phone: "15550091101", FirstName: "Owner"})
|
|
viewer, _ := userStore.Create(ctx, domain.User{AccessHash: 91102, Phone: "15550091102", FirstName: "Viewer"})
|
|
channelStore := memory.NewChannelStore()
|
|
channelService := appchannels.NewService(channelStore)
|
|
r := New(Config{}, Deps{
|
|
Users: appusers.NewService(userStore),
|
|
Channels: channelService,
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
joined, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
|
|
Title: "Joined Broadcast",
|
|
Broadcast: true,
|
|
Date: 1700020000,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create joined channel: %v", err)
|
|
}
|
|
if _, err := channelService.InviteToChannel(ctx, owner.ID, joined.Channel.ID, []int64{viewer.ID}, 1700020001); err != nil {
|
|
t.Fatalf("invite viewer: %v", err)
|
|
}
|
|
hidden, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
|
|
Title: "Hidden Broadcast",
|
|
Broadcast: true,
|
|
Date: 1700020002,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create hidden channel: %v", err)
|
|
}
|
|
for i, body := range []string{"global needle older", "global needle newer"} {
|
|
if _, err := channelService.SendMessage(ctx, owner.ID, domain.SendChannelMessageRequest{
|
|
ChannelID: joined.Channel.ID,
|
|
RandomID: int64(201 + i),
|
|
Message: body,
|
|
Date: 1700020010 + i*10,
|
|
}); err != nil {
|
|
t.Fatalf("send joined %d: %v", i, err)
|
|
}
|
|
}
|
|
if _, err := channelService.SendMessage(ctx, owner.ID, domain.SendChannelMessageRequest{
|
|
ChannelID: hidden.Channel.ID,
|
|
RandomID: 301,
|
|
Message: "global needle hidden",
|
|
Date: 1700020030,
|
|
}); err != nil {
|
|
t.Fatalf("send hidden: %v", err)
|
|
}
|
|
|
|
req := &tg.MessagesSearchGlobalRequest{
|
|
BroadcastsOnly: true,
|
|
Q: "global needle",
|
|
Filter: &tg.InputMessagesFilterEmpty{},
|
|
OffsetPeer: &tg.InputPeerEmpty{},
|
|
Limit: 1,
|
|
}
|
|
got, err := r.onMessagesSearchGlobal(WithUserID(ctx, viewer.ID), req)
|
|
if err != nil {
|
|
t.Fatalf("messages.searchGlobal first page: %v", err)
|
|
}
|
|
slice, ok := got.(*tg.MessagesMessagesSlice)
|
|
if !ok {
|
|
t.Fatalf("first page = %T %+v, want messagesSlice", got, got)
|
|
}
|
|
if nextRate, ok := slice.GetNextRate(); !ok || nextRate != 1700020020 {
|
|
t.Fatalf("first page next_rate = %d ok %v, want newest date", nextRate, ok)
|
|
}
|
|
messages, chats, users := searchMessagesPayload(t, got)
|
|
if len(messages) != 1 || len(chats) != 1 || len(users) != 1 {
|
|
t.Fatalf("first payload messages=%d chats=%d users=%d, want 1/1/1", len(messages), len(chats), len(users))
|
|
}
|
|
first := messages[0].(*tg.Message)
|
|
if first.Message != "global needle newer" {
|
|
t.Fatalf("first message = %q, want newest joined channel hit", first.Message)
|
|
}
|
|
if peer, ok := first.PeerID.(*tg.PeerChannel); !ok || peer.ChannelID != joined.Channel.ID {
|
|
t.Fatalf("first peer = %#v, want joined channel %d", first.PeerID, joined.Channel.ID)
|
|
}
|
|
|
|
page2 := &tg.MessagesSearchGlobalRequest{
|
|
BroadcastsOnly: true,
|
|
Q: "global needle",
|
|
Filter: &tg.InputMessagesFilterEmpty{},
|
|
OffsetRate: slice.NextRate,
|
|
OffsetPeer: &tg.InputPeerChannel{
|
|
ChannelID: joined.Channel.ID,
|
|
AccessHash: joined.Channel.AccessHash,
|
|
},
|
|
OffsetID: first.ID,
|
|
Limit: 10,
|
|
}
|
|
got, err = r.onMessagesSearchGlobal(WithUserID(ctx, viewer.ID), page2)
|
|
if err != nil {
|
|
t.Fatalf("messages.searchGlobal second page: %v", err)
|
|
}
|
|
messages, chats, _ = searchMessagesPayload(t, got)
|
|
if len(messages) != 1 || len(chats) != 1 {
|
|
t.Fatalf("second payload messages=%d chats=%d, want older joined hit only", len(messages), len(chats))
|
|
}
|
|
if msg := messages[0].(*tg.Message); msg.Message != "global needle older" {
|
|
t.Fatalf("second message = %q, want older joined hit", msg.Message)
|
|
}
|
|
}
|
|
|
|
func TestMessagesGetHistoryReturnsStoredMessages(t *testing.T) {
|
|
msg := domain.Message{
|
|
ID: 1,
|
|
OwnerUserID: 1000000001,
|
|
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
|
|
From: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
|
|
Date: 1700000100,
|
|
Body: "Login code: 12345",
|
|
}
|
|
messages := &captureMessages{
|
|
list: domain.MessageList{
|
|
Messages: []domain.Message{msg},
|
|
Users: []domain.User{domain.OfficialSystemUser()},
|
|
Count: 1,
|
|
Hash: 99,
|
|
},
|
|
}
|
|
r := New(Config{}, Deps{Messages: messages, Users: mapUsersService{users: map[int64]domain.User{domain.OfficialSystemUserID: domain.OfficialSystemUser()}}}, zaptest.NewLogger(t), clock.System)
|
|
req := &tg.MessagesGetHistoryRequest{
|
|
Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash},
|
|
Limit: 20,
|
|
AddOffset: 1 << 30,
|
|
}
|
|
var in bin.Buffer
|
|
if err := req.Encode(&in); err != nil {
|
|
t.Fatalf("encode request: %v", err)
|
|
}
|
|
|
|
enc, err := r.Dispatch(WithUserID(context.Background(), 1000000001), [8]byte{}, 0, &in)
|
|
if err != nil {
|
|
t.Fatalf("dispatch: %v", err)
|
|
}
|
|
got, ok := enc.(*tg.MessagesMessages)
|
|
if !ok {
|
|
t.Fatalf("response = %T, want *tg.MessagesMessages", enc)
|
|
}
|
|
if len(got.Messages) != 1 || len(got.Users) != 1 {
|
|
t.Fatalf("history = %+v, want one message and one user", got)
|
|
}
|
|
if messages.filter.Peer.ID != domain.OfficialSystemUserID || messages.filter.Limit != 20 || messages.filter.AddOffset != domain.MaxMessageHistoryAddOffset {
|
|
t.Fatalf("filter = %+v, want official peer limit 20 and clamped add_offset", messages.filter)
|
|
}
|
|
}
|
|
|
|
func TestMessagesSearchMediaPreservesCombinedFilters(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
alice, err := users.Create(ctx, domain.User{AccessHash: 511, Phone: "15550000511", FirstName: "Alice"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bob, err := users.Create(ctx, domain.User{AccessHash: 512, Phone: "15550000512", FirstName: "Bob"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
messages := &captureMessages{}
|
|
r := New(Config{}, Deps{Messages: messages, Users: appusers.NewService(users)}, zaptest.NewLogger(t), clock.System)
|
|
req := &tg.MessagesSearchRequest{
|
|
Peer: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
|
|
Q: "invoice", FromID: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
|
|
Filter: &tg.InputMessagesFilterPhotos{}, MinDate: 100, MaxDate: 200,
|
|
OffsetID: 90, AddOffset: 3, Limit: 20, MaxID: 80, MinID: 10,
|
|
}
|
|
req.SetTopMsgID(7)
|
|
var in bin.Buffer
|
|
if err := req.Encode(&in); err != nil {
|
|
t.Fatalf("encode request: %v", err)
|
|
}
|
|
if _, err := r.Dispatch(WithUserID(ctx, bob.ID), [8]byte{}, 0, &in); err != nil {
|
|
t.Fatalf("messages.search media: %v", err)
|
|
}
|
|
got := messages.mediaReq
|
|
if got.Query != "invoice" || got.SenderUserID != alice.ID || got.MinDate != 100 || got.MaxDate != 200 ||
|
|
got.TopMsgID != 7 || got.OffsetID != 90 || got.AddOffset != 3 || got.Limit != 20 || got.MaxID != 80 || got.MinID != 10 {
|
|
t.Fatalf("media request = %+v", got)
|
|
}
|
|
if len(got.Categories) != 1 || got.Categories[0] != domain.MediaCategoryPhoto {
|
|
t.Fatalf("media categories = %v", got.Categories)
|
|
}
|
|
}
|
|
|
|
func TestMessagesSetTypingPushesUserTypingUpdate(t *testing.T) {
|
|
sessions := &captureScopedSessions{captureSessions: &captureSessions{}}
|
|
r := New(Config{}, Deps{Sessions: sessions}, zaptest.NewLogger(t), clock.System)
|
|
var authKeyID [8]byte
|
|
authKeyID[0] = 7
|
|
|
|
ctx := WithSessionID(WithAuthKeyID(WithUserID(context.Background(), 1000000001), authKeyID), 55)
|
|
ok, err := r.onMessagesSetTyping(ctx, &tg.MessagesSetTypingRequest{
|
|
Peer: &tg.InputPeerUser{UserID: 1000000002, AccessHash: 22},
|
|
Action: &tg.SendMessageTypingAction{},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("set typing: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatalf("set typing = false, want true")
|
|
}
|
|
|
|
got := sessions.snapshot()
|
|
if got.userID != 1000000002 || got.sessionID != 55 || got.messageType != proto.MessageFromServer {
|
|
t.Fatalf("push = user %d exclude session %d type %v, want target/exclude/from_server", got.userID, got.sessionID, got.messageType)
|
|
}
|
|
if gotAuthKeyID := sessions.scopedAuthKey(); gotAuthKeyID != authKeyID {
|
|
t.Fatalf("exclude auth_key_id = %x, want %x", gotAuthKeyID, authKeyID)
|
|
}
|
|
updateShort, ok := got.message.(*tg.UpdateShort)
|
|
if !ok {
|
|
t.Fatalf("pushed message = %T, want *tg.UpdateShort", got.message)
|
|
}
|
|
typing, ok := updateShort.Update.(*tg.UpdateUserTyping)
|
|
if !ok {
|
|
t.Fatalf("short update = %T, want *tg.UpdateUserTyping", updateShort.Update)
|
|
}
|
|
if typing.UserID != 1000000001 {
|
|
t.Fatalf("typing user_id = %d, want sender", typing.UserID)
|
|
}
|
|
if _, ok := typing.Action.(*tg.SendMessageTypingAction); !ok {
|
|
t.Fatalf("typing action = %T, want *tg.SendMessageTypingAction", typing.Action)
|
|
}
|
|
}
|
|
|
|
func TestMessagesSetTypingRejectsInvalidTopMsgID(t *testing.T) {
|
|
r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
|
|
req := &tg.MessagesSetTypingRequest{
|
|
Peer: &tg.InputPeerUser{UserID: 1000000002, AccessHash: 22},
|
|
Action: &tg.SendMessageTypingAction{},
|
|
}
|
|
req.SetTopMsgID(domain.MaxMessageBoxID + 1)
|
|
|
|
ok, err := r.onMessagesSetTyping(WithUserID(context.Background(), 1000000001), req)
|
|
if ok || err == nil || !strings.Contains(err.Error(), "MSG_ID_INVALID") {
|
|
t.Fatalf("set typing invalid top_msg_id = ok %v err %v, want MSG_ID_INVALID", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestMessagesSetTypingTreatsWebAMainThreadSentinelAsUnset(t *testing.T) {
|
|
sessions := &captureScopedSessions{captureSessions: &captureSessions{}}
|
|
r := New(Config{}, Deps{Sessions: sessions}, zaptest.NewLogger(t), clock.System)
|
|
req := &tg.MessagesSetTypingRequest{
|
|
Peer: &tg.InputPeerUser{UserID: 1000000002, AccessHash: 22},
|
|
Action: &tg.SendMessageTypingAction{},
|
|
}
|
|
req.SetTopMsgID(-1)
|
|
|
|
ok, err := r.onMessagesSetTyping(WithUserID(context.Background(), 1000000001), req)
|
|
if err != nil {
|
|
t.Fatalf("set typing with WebA main thread sentinel: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatalf("set typing = false, want true")
|
|
}
|
|
got := sessions.snapshot()
|
|
updateShort, ok := got.message.(*tg.UpdateShort)
|
|
if !ok {
|
|
t.Fatalf("pushed message = %T, want *tg.UpdateShort", got.message)
|
|
}
|
|
typing, ok := updateShort.Update.(*tg.UpdateUserTyping)
|
|
if !ok {
|
|
t.Fatalf("short update = %T, want *tg.UpdateUserTyping", updateShort.Update)
|
|
}
|
|
if typing.TopMsgID != 0 {
|
|
t.Fatalf("typing top_msg_id = %d, want unset/0", typing.TopMsgID)
|
|
}
|
|
}
|
|
|
|
func TestMessagesSetTypingPushesChannelTypingTopMsgID(t *testing.T) {
|
|
const (
|
|
ownerID = int64(1000000001)
|
|
memberID = int64(1000000002)
|
|
topicID = 7
|
|
)
|
|
channels := appchannels.NewService(memory.NewChannelStore())
|
|
created, err := channels.CreateChannel(context.Background(), ownerID, domain.CreateChannelRequest{
|
|
Title: "topic group",
|
|
CreatorUserID: ownerID,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{memberID},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
sessions := &captureScopedSessions{captureSessions: &captureSessions{
|
|
channelViewers: map[int64][]int64{created.Channel.ID: {memberID}},
|
|
}}
|
|
r := New(Config{}, Deps{Channels: channels, Sessions: sessions}, zaptest.NewLogger(t), clock.System)
|
|
var authKeyID [8]byte
|
|
authKeyID[0] = 9
|
|
|
|
req := &tg.MessagesSetTypingRequest{
|
|
Peer: &tg.InputPeerChannel{
|
|
ChannelID: created.Channel.ID,
|
|
AccessHash: created.Channel.AccessHash,
|
|
},
|
|
Action: &tg.SendMessageTypingAction{},
|
|
}
|
|
req.SetTopMsgID(topicID)
|
|
ctx := WithSessionID(WithAuthKeyID(WithUserID(context.Background(), ownerID), authKeyID), 77)
|
|
|
|
ok, err := r.onMessagesSetTyping(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("set channel typing: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatalf("set channel typing = false, want true")
|
|
}
|
|
got := sessions.snapshot()
|
|
if got.userID != memberID || got.sessionID != 77 || got.messageType != proto.MessageFromServer {
|
|
t.Fatalf("channel typing push = user %d exclude session %d type %v, want member/exclude/from_server", got.userID, got.sessionID, got.messageType)
|
|
}
|
|
if gotAuthKeyID := sessions.scopedAuthKey(); gotAuthKeyID != authKeyID {
|
|
t.Fatalf("exclude auth_key_id = %x, want %x", gotAuthKeyID, authKeyID)
|
|
}
|
|
updates, ok := got.message.(*tg.Updates)
|
|
if !ok {
|
|
t.Fatalf("pushed message = %T, want *tg.Updates", got.message)
|
|
}
|
|
if len(updates.Updates) != 1 {
|
|
t.Fatalf("updates len = %d, want 1", len(updates.Updates))
|
|
}
|
|
typing, ok := updates.Updates[0].(*tg.UpdateChannelUserTyping)
|
|
if !ok {
|
|
t.Fatalf("channel update = %T, want *tg.UpdateChannelUserTyping", updates.Updates[0])
|
|
}
|
|
if typing.ChannelID != created.Channel.ID || typing.TopMsgID != topicID {
|
|
t.Fatalf("channel typing = channel %d top %d, want channel %d top %d", typing.ChannelID, typing.TopMsgID, created.Channel.ID, topicID)
|
|
}
|
|
from, ok := typing.FromID.(*tg.PeerUser)
|
|
if !ok || from.UserID != ownerID {
|
|
t.Fatalf("typing from = %T %+v, want owner peer", typing.FromID, typing.FromID)
|
|
}
|
|
if _, ok := typing.Action.(*tg.SendMessageTypingAction); !ok {
|
|
t.Fatalf("typing action = %T, want *tg.SendMessageTypingAction", typing.Action)
|
|
}
|
|
}
|
|
|
|
func TestMessagesSetTypingSkipsChannelMemberWithoutViewerInterest(t *testing.T) {
|
|
const (
|
|
ownerID = int64(1000000001)
|
|
memberID = int64(1000000002)
|
|
)
|
|
channels := appchannels.NewService(memory.NewChannelStore())
|
|
created, err := channels.CreateChannel(context.Background(), ownerID, domain.CreateChannelRequest{
|
|
Title: "quiet group",
|
|
CreatorUserID: ownerID,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{memberID},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
sessions := &captureScopedSessions{captureSessions: &captureSessions{
|
|
channelMembers: map[int64][]int64{created.Channel.ID: {memberID}},
|
|
}}
|
|
r := New(Config{}, Deps{Channels: channels, Sessions: sessions}, zaptest.NewLogger(t), clock.System)
|
|
|
|
ok, err := r.onMessagesSetTyping(WithUserID(context.Background(), ownerID), &tg.MessagesSetTypingRequest{
|
|
Peer: &tg.InputPeerChannel{
|
|
ChannelID: created.Channel.ID,
|
|
AccessHash: created.Channel.AccessHash,
|
|
},
|
|
Action: &tg.SendMessageTypingAction{},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("set channel typing: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatalf("set channel typing = false, want true")
|
|
}
|
|
if got := sessions.snapshot(); got.message != nil {
|
|
t.Fatalf("typing push without viewer interest = %+v, want none", got)
|
|
}
|
|
}
|
|
|
|
func TestMessagesGetMessagesReturnsOwnerMessages(t *testing.T) {
|
|
const (
|
|
userID = int64(1000000001)
|
|
peerID = int64(1000000002)
|
|
)
|
|
messages := &captureMessages{list: domain.MessageList{
|
|
Messages: []domain.Message{{
|
|
ID: 7,
|
|
OwnerUserID: userID,
|
|
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: peerID},
|
|
From: domain.Peer{Type: domain.PeerTypeUser, ID: peerID},
|
|
Date: 1700000000,
|
|
Body: "reply source",
|
|
}},
|
|
Count: 1,
|
|
}}
|
|
r := New(Config{}, Deps{
|
|
Messages: messages,
|
|
Users: mapUsersService{users: map[int64]domain.User{
|
|
userID: {ID: userID, FirstName: "Alice"},
|
|
peerID: {ID: peerID, FirstName: "Bob"},
|
|
}},
|
|
}, zaptest.NewLogger(t), clock.System)
|
|
|
|
got, err := r.onMessagesGetMessages(WithUserID(context.Background(), userID), []tg.InputMessageClass{&tg.InputMessageID{ID: 7}})
|
|
if err != nil {
|
|
t.Fatalf("get messages: %v", err)
|
|
}
|
|
box, ok := got.(*tg.MessagesMessages)
|
|
if !ok || len(box.Messages) != 1 {
|
|
t.Fatalf("response = %T %+v, want one messages.messages", got, got)
|
|
}
|
|
msg, ok := box.Messages[0].(*tg.Message)
|
|
if !ok || msg.ID != 7 || msg.Message != "reply source" {
|
|
t.Fatalf("message = %#v, want source message", box.Messages[0])
|
|
}
|
|
if len(box.Users) != 1 {
|
|
t.Fatalf("users = %+v, want peer user", box.Users)
|
|
}
|
|
}
|