owpengram-server/internal/rpc/channels_settings_rpc_test.go

1530 lines
69 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"strconv"
"strings"
"testing"
"github.com/gotd/td/clock"
"github.com/gotd/td/tg"
"go.uber.org/zap/zaptest"
appchannels "telesrv/internal/app/channels"
appdialogs "telesrv/internal/app/dialogs"
"telesrv/internal/app/readmodel"
appusers "telesrv/internal/app/users"
"telesrv/internal/domain"
"telesrv/internal/store"
"telesrv/internal/store/memory"
)
type fakeRPCReadModelVersions struct {
hashes map[store.ReadModelKey]int64
}
func (f *fakeRPCReadModelVersions) ReadModelHash(_ context.Context, model string, ownerUserID int64, peerType domain.PeerType, peerID int64) (int64, bool, error) {
hash := f.hashes[store.ReadModelKey{Model: model, OwnerUserID: ownerUserID, PeerType: peerType, PeerID: peerID}]
return hash, hash != 0, nil
}
func (f *fakeRPCReadModelVersions) ReadModelHashes(_ context.Context, keys []store.ReadModelKey) (map[store.ReadModelKey]int64, error) {
out := make(map[store.ReadModelKey]int64, len(keys))
for _, key := range keys {
if hash := f.hashes[key]; hash != 0 {
out[key] = hash
}
}
return out, nil
}
func TestChannelInputAccessHashIsValidatedRPC(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550001101", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
friend, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "15550001102", FirstName: "Friend"})
if err != nil {
t.Fatalf("create friend: %v", err)
}
channelStore := memory.NewChannelStore()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: appchannels.NewService(channelStore),
Dialogs: appdialogs.NewService(memory.NewDialogStore(), channelStore),
}, zaptest.NewLogger(t), clock.System)
invited, err := r.onMessagesCreateChat(WithUserID(ctx, owner.ID), &tg.MessagesCreateChatRequest{
Users: []tg.InputUserClass{&tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash}},
Title: "Hash Group",
})
if err != nil {
t.Fatalf("create chat: %v", err)
}
channel := invited.Updates.(*tg.Updates).Chats[0].(*tg.Channel)
badHash := channel.AccessHash + 1
if badHash == 0 {
badHash = 1
}
if _, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: badHash}); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") {
t.Fatalf("get full bad access_hash err = %v, want CHANNEL_PRIVATE", err)
}
if _, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: badHash},
Message: "bad hash",
RandomID: 991,
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") {
t.Fatalf("send bad access_hash err = %v, want CHANNEL_PRIVATE", err)
}
if _, err := r.onMessagesGetPeerSettings(WithUserID(ctx, owner.ID), &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: badHash}); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") {
t.Fatalf("peer settings bad access_hash err = %v, want CHANNEL_PRIVATE", err)
}
if _, err := r.onMessagesToggleDialogPin(WithUserID(ctx, owner.ID), &tg.MessagesToggleDialogPinRequest{
Peer: &tg.InputDialogPeer{Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: badHash}},
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") {
t.Fatalf("toggle pin bad access_hash err = %v, want CHANNEL_PRIVATE", err)
}
if _, err := r.onFoldersEditPeerFolders(WithUserID(ctx, owner.ID), []tg.InputFolderPeer{{
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: badHash},
FolderID: domain.DialogArchiveFolderID,
}}); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") {
t.Fatalf("edit peer folder bad access_hash err = %v, want CHANNEL_PRIVATE", err)
}
if _, err := r.onUpdatesGetChannelDifference(WithUserID(ctx, friend.ID), &tg.UpdatesGetChannelDifferenceRequest{
Channel: &tg.InputChannel{ChannelID: channel.ID, AccessHash: badHash},
Filter: &tg.ChannelMessagesFilterEmpty{},
Pts: 0,
Limit: 10,
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") {
t.Fatalf("difference bad access_hash err = %v, want CHANNEL_PRIVATE", err)
}
chats, err := r.onChannelsGetChannels(WithUserID(ctx, owner.ID), []tg.InputChannelClass{&tg.InputChannel{ChannelID: channel.ID, AccessHash: badHash}})
if err != nil {
t.Fatalf("get channels bad access_hash: %v", err)
}
if got := len(chats.(*tg.MessagesChats).Chats); got != 0 {
t.Fatalf("get channels bad access_hash chats = %d, want 0", got)
}
mixedChats, err := r.onChannelsGetChannels(WithUserID(ctx, owner.ID), []tg.InputChannelClass{
&tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
&tg.InputChannel{ChannelID: channel.ID, AccessHash: badHash},
&tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
})
if err != nil {
t.Fatalf("get channels mixed access_hash: %v", err)
}
if got := len(mixedChats.(*tg.MessagesChats).Chats); got != 2 {
t.Fatalf("get channels mixed access_hash chats = %d, want two good refs", got)
}
if _, err := r.dialogFilterFromRequest(WithUserID(ctx, owner.ID), owner.ID, &tg.MessagesGetDialogsRequest{
OffsetPeer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: badHash},
Limit: 20,
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") {
t.Fatalf("get dialogs offset bad access_hash err = %v, want CHANNEL_PRIVATE", err)
}
if _, err := r.onMessagesSaveDraft(WithUserID(ctx, owner.ID), &tg.MessagesSaveDraftRequest{
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: badHash},
Message: "draft",
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") {
t.Fatalf("save draft bad access_hash err = %v, want CHANNEL_PRIVATE", err)
}
filterReq := &tg.MessagesUpdateDialogFilterRequest{ID: domain.DialogCustomFolderMinID}
filterReq.SetFilter(&tg.DialogFilter{
ID: domain.DialogCustomFolderMinID,
Title: tg.TextWithEntities{Text: "Channels"},
IncludePeers: []tg.InputPeerClass{
&tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: badHash},
},
})
if ok, err := r.onMessagesUpdateDialogFilter(WithUserID(ctx, owner.ID), filterReq); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") || ok {
t.Fatalf("update dialog filter bad access_hash = %v, %v; want CHANNEL_PRIVATE", ok, err)
}
reply := &tg.InputReplyToMessage{ReplyToMsgID: 1}
reply.SetReplyToPeerID(&tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: badHash})
sendReq := &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
Message: "bad reply hash",
RandomID: 992,
}
sendReq.SetReplyTo(reply)
if _, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), sendReq); err == nil || !strings.Contains(err.Error(), "REPLY_MESSAGE_ID_INVALID") {
t.Fatalf("send reply_to_peer bad access_hash err = %v, want REPLY_MESSAGE_ID_INVALID", err)
}
}
func TestChannelsGetChannelsUsesBatchServiceRPC(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, err := userStore.Create(ctx, domain.User{AccessHash: 31, Phone: "15550003101", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
channelStore := memory.NewChannelStore()
channelService := appchannels.NewService(channelStore)
first, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
Title: "Batch One",
Broadcast: true,
Date: 1700001300,
})
if err != nil {
t.Fatalf("create first channel: %v", err)
}
second, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
Title: "Batch Two",
Megagroup: true,
Date: 1700001310,
})
if err != nil {
t.Fatalf("create second channel: %v", err)
}
counting := &countingChannelsService{Service: channelService}
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: counting,
Dialogs: appdialogs.NewService(memory.NewDialogStore(), channelStore),
}, zaptest.NewLogger(t), clock.System)
chats, err := r.onChannelsGetChannels(WithUserID(ctx, owner.ID), []tg.InputChannelClass{
&tg.InputChannel{ChannelID: first.Channel.ID, AccessHash: first.Channel.AccessHash},
&tg.InputChannel{ChannelID: second.Channel.ID, AccessHash: second.Channel.AccessHash},
})
if err != nil {
t.Fatalf("get channels batch: %v", err)
}
if got := len(chats.(*tg.MessagesChats).Chats); got != 2 {
t.Fatalf("get channels returned %d chats, want 2", got)
}
if counting.getChannelsCalls != 1 || counting.getChannelCalls != 0 {
t.Fatalf("channel service calls: GetChannels=%d GetChannel=%d, want one batch call only", counting.getChannelsCalls, counting.getChannelCalls)
}
}
func TestMessagesGetChatsUsesBatchServiceRPC(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, err := userStore.Create(ctx, domain.User{AccessHash: 41, Phone: "15550004101", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
channelStore := memory.NewChannelStore()
channelService := appchannels.NewService(channelStore)
first, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
Title: "Legacy Batch One",
Broadcast: true,
Date: 1700001400,
})
if err != nil {
t.Fatalf("create first channel: %v", err)
}
second, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
Title: "Legacy Batch Two",
Megagroup: true,
Date: 1700001410,
})
if err != nil {
t.Fatalf("create second channel: %v", err)
}
counting := &countingChannelsService{Service: channelService}
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: counting,
Dialogs: appdialogs.NewService(memory.NewDialogStore(), channelStore),
}, zaptest.NewLogger(t), clock.System)
chats, err := r.onMessagesGetChats(WithUserID(ctx, owner.ID), []int64{
first.Channel.ID,
second.Channel.ID,
first.Channel.ID,
0,
})
if err != nil {
t.Fatalf("messages.getChats batch: %v", err)
}
got := chats.(*tg.MessagesChats).Chats
if len(got) != 3 {
t.Fatalf("messages.getChats returned %d chats, want duplicate-preserving 3", len(got))
}
if got[0].(*tg.Channel).ID != first.Channel.ID || got[1].(*tg.Channel).ID != second.Channel.ID || got[2].(*tg.Channel).ID != first.Channel.ID {
t.Fatalf("messages.getChats order = %+v, want first, second, first", got)
}
if counting.getChannelsCalls != 1 || counting.getChannelCalls != 0 {
t.Fatalf("channel service calls: GetChannels=%d GetChannel=%d, want one batch call only", counting.getChannelsCalls, counting.getChannelCalls)
}
}
func TestMessagesGetPeerSettingsUsesResolveChannelForAccessCheck(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, err := userStore.Create(ctx, domain.User{AccessHash: 51, Phone: "15550005101", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
channelStore := memory.NewChannelStore()
channelService := appchannels.NewService(channelStore)
created, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
Title: "Peer Settings Channel",
Broadcast: true,
Date: 1700001500,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
counting := &countingChannelsService{Service: channelService}
r := New(Config{}, Deps{Channels: counting}, zaptest.NewLogger(t), clock.System)
input := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash}
if _, err := r.onMessagesGetPeerSettings(WithUserID(ctx, owner.ID), input); err != nil {
t.Fatalf("messages.getPeerSettings: %v", err)
}
if counting.resolveChannelCalls != 1 || counting.getChannelCalls != 0 {
t.Fatalf("channel access check calls: ResolveChannel=%d GetChannel=%d, want one resolve and no full get", counting.resolveChannelCalls, counting.getChannelCalls)
}
bad := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash + 1}
if _, err := r.onMessagesGetPeerSettings(WithUserID(ctx, owner.ID), bad); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") {
t.Fatalf("bad access_hash err = %v, want CHANNEL_PRIVATE", err)
}
}
func TestChannelsGetFullChannelUsesChannelReadModel(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, err := userStore.Create(ctx, domain.User{AccessHash: 61, Phone: "15550006101", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
channelStore := memory.NewChannelStore()
creator := appchannels.NewService(channelStore)
created, err := creator.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
Title: "Full Read Model",
Megagroup: true,
Date: 1700004200,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: created.Channel.ID}
versions := &fakeRPCReadModelVersions{hashes: map[store.ReadModelKey]int64{
{Model: readmodel.ModelChannelBase, OwnerUserID: 0, PeerType: peer.Type, PeerID: peer.ID}: 101,
{Model: readmodel.ModelChannelMember, OwnerUserID: owner.ID, PeerType: peer.Type, PeerID: peer.ID}: 202,
{Model: readmodel.ModelDialogLight, OwnerUserID: owner.ID, PeerType: peer.Type, PeerID: peer.ID}: 303,
}}
counting := &countingChannelsService{Service: appchannels.NewService(channelStore, appchannels.WithReadModelVersions(versions))}
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: counting,
}, zaptest.NewLogger(t), clock.System)
input := &tg.InputChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash}
if _, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), input); err != nil {
t.Fatalf("first getFullChannel: %v", err)
}
if _, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), input); err != nil {
t.Fatalf("second getFullChannel: %v", err)
}
if counting.getChannelReadModelCalls != 1 || counting.getChannelCalls != 0 {
t.Fatalf("channel calls readModel/GetChannel = %d/%d, want 1/0 after projection cache hit", counting.getChannelReadModelCalls, counting.getChannelCalls)
}
}
type countingChannelsService struct {
*appchannels.Service
getChannelCalls int
getChannelReadModelCalls int
resolveChannelCalls int
getChannelsCalls int
}
func (s *countingChannelsService) GetChannel(ctx context.Context, userID, channelID int64) (domain.ChannelView, error) {
s.getChannelCalls++
return s.Service.GetChannel(ctx, userID, channelID)
}
func (s *countingChannelsService) GetChannelReadModel(ctx context.Context, userID, channelID int64) (domain.ChannelView, error) {
s.getChannelReadModelCalls++
return s.Service.GetChannelReadModel(ctx, userID, channelID)
}
func (s *countingChannelsService) ResolveChannel(ctx context.Context, userID, channelID int64) (domain.ChannelView, error) {
s.resolveChannelCalls++
return s.Service.ResolveChannel(ctx, userID, channelID)
}
func (s *countingChannelsService) GetChannels(ctx context.Context, userID int64, channelIDs []int64) ([]domain.ChannelView, error) {
s.getChannelsCalls++
return s.Service.GetChannels(ctx, userID, channelIDs)
}
func TestChannelsCreateChannelUnsupportedOptionsReturnExplicitErrors(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 73, Phone: "15550002173", FirstName: "Owner"})
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: appchannels.NewService(memory.NewChannelStore()),
}, zaptest.NewLogger(t), clock.System)
tests := []struct {
name string
req *tg.ChannelsCreateChannelRequest
want string
}{
{
name: "history import",
req: &tg.ChannelsCreateChannelRequest{
Title: "Import Group",
Megagroup: true,
ForImport: true,
},
want: "CHAT_INVALID",
},
{
name: "geogroup",
req: &tg.ChannelsCreateChannelRequest{
Title: "Geo Group",
Megagroup: true,
GeoPoint: &tg.InputGeoPoint{Lat: 1.2, Long: 3.4},
Address: "somewhere",
},
want: "ADDRESS_INVALID",
},
{
name: "address without geo",
req: &tg.ChannelsCreateChannelRequest{
Title: "Bad Geo Group",
Megagroup: true,
Address: "somewhere",
},
want: "ADDRESS_INVALID",
},
{
name: "negative ttl",
req: &tg.ChannelsCreateChannelRequest{
Title: "Bad TTL Group",
Megagroup: true,
TTLPeriod: -1,
},
want: "TTL_PERIOD_INVALID",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if _, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), tc.req); err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("create channel err = %v, want %s", err, tc.want)
}
})
}
}
func TestChannelsGetFullChannelCanSetUsernameOnlyForCreator(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 56, Phone: "15550002211", FirstName: "Owner"})
member, _ := userStore.Create(ctx, domain.User{AccessHash: 57, Phone: "15550002212", FirstName: "Member"})
channelStore := memory.NewChannelStore()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: appchannels.NewService(channelStore),
}, zaptest.NewLogger(t), clock.System)
tests := []struct {
name string
req *tg.ChannelsCreateChannelRequest
}{
{
name: "broadcast",
req: &tg.ChannelsCreateChannelRequest{
Title: "Editable Broadcast",
About: "public link editable",
Broadcast: true,
},
},
{
name: "megagroup",
req: &tg.ChannelsCreateChannelRequest{
Title: "Editable Megagroup",
About: "public link editable",
Megagroup: true,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
created, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), tc.req)
if err != nil {
t.Fatalf("create channel: %v", err)
}
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
input := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
ownerFull, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), input)
if err != nil {
t.Fatalf("owner get full channel: %v", err)
}
ownerChannelFull := ownerFull.FullChat.(*tg.ChannelFull)
if !ownerChannelFull.CanSetUsername || !ownerChannelFull.CanDeleteChannel {
t.Fatalf("owner full flags can_set_username=%v can_delete_channel=%v, want both true", ownerChannelFull.CanSetUsername, ownerChannelFull.CanDeleteChannel)
}
if _, err := r.onChannelsInviteToChannel(WithUserID(ctx, owner.ID), &tg.ChannelsInviteToChannelRequest{
Channel: input,
Users: []tg.InputUserClass{&tg.InputUser{UserID: member.ID, AccessHash: member.AccessHash}},
}); err != nil {
t.Fatalf("invite member: %v", err)
}
memberFull, err := r.onChannelsGetFullChannel(WithUserID(ctx, member.ID), input)
if err != nil {
t.Fatalf("member get full channel: %v", err)
}
memberChannelFull := memberFull.FullChat.(*tg.ChannelFull)
if memberChannelFull.CanSetUsername || memberChannelFull.CanDeleteChannel {
t.Fatalf("member full flags can_set_username=%v can_delete_channel=%v, want both false", memberChannelFull.CanSetUsername, memberChannelFull.CanDeleteChannel)
}
})
}
}
func TestChannelUsernameAndManagementRPC(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 61, Phone: "15550002301", FirstName: "Owner"})
requester, _ := userStore.Create(ctx, domain.User{AccessHash: 62, Phone: "15550002302", FirstName: "Requester"})
channelStore := memory.NewChannelStore()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: appchannels.NewService(channelStore),
}, zaptest.NewLogger(t), clock.System)
created, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), &tg.ChannelsCreateChannelRequest{
Title: "Public Team",
About: "public",
Megagroup: true,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
input := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
sent, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
Message: "channel management seed",
RandomID: 991,
})
if err != nil {
t.Fatalf("send seed message: %v", err)
}
msgID := sent.(*tg.Updates).Updates[0].(*tg.UpdateMessageID).ID
okUsername, err := r.onChannelsCheckUsername(WithUserID(ctx, owner.ID), &tg.ChannelsCheckUsernameRequest{
Channel: input,
Username: "public_team",
})
if err != nil || !okUsername {
t.Fatalf("check username = ok %v err %v, want true", okUsername, err)
}
updated, err := r.onChannelsUpdateUsername(WithUserID(ctx, owner.ID), &tg.ChannelsUpdateUsernameRequest{
Channel: input,
Username: "public_team",
})
if err != nil || !updated {
t.Fatalf("update username = %v err %v, want true", updated, err)
}
admined, err := r.onChannelsGetAdminedPublicChannels(WithUserID(ctx, owner.ID), &tg.ChannelsGetAdminedPublicChannelsRequest{})
if err != nil {
t.Fatalf("get admined public channels: %v", err)
}
adminedChats := admined.(*tg.MessagesChats).Chats
if len(adminedChats) != 1 || adminedChats[0].(*tg.Channel).Username != "public_team" {
t.Fatalf("admined chats = %+v, want public channel with username", adminedChats)
}
signatures, err := r.onChannelsToggleSignatures(WithUserID(ctx, owner.ID), &tg.ChannelsToggleSignaturesRequest{
Channel: input,
SignaturesEnabled: true,
})
if err != nil {
t.Fatalf("toggle signatures: %v", err)
}
if signed := signatures.(*tg.Updates).Chats[0].(*tg.Channel); !signed.Signatures {
t.Fatalf("signed channel = %+v, want signatures enabled", signed)
}
if _, err := r.onChannelsToggleSlowMode(WithUserID(ctx, owner.ID), &tg.ChannelsToggleSlowModeRequest{Channel: input, Seconds: 7}); err == nil {
t.Fatalf("toggle slow mode invalid seconds err = nil, want SECONDS_INVALID")
}
slowUpdates, err := r.onChannelsToggleSlowMode(WithUserID(ctx, owner.ID), &tg.ChannelsToggleSlowModeRequest{Channel: input, Seconds: 30})
if err != nil {
t.Fatalf("toggle slow mode valid: %v", err)
}
if slowChannel := slowUpdates.(*tg.Updates).Chats[0].(*tg.Channel); !slowChannel.SlowmodeEnabled {
t.Fatalf("slow mode channel = %+v, want slowmode enabled", slowChannel)
}
if ok, err := r.onChannelsSetStickers(WithUserID(ctx, owner.ID), &tg.ChannelsSetStickersRequest{Channel: input, Stickerset: &tg.InputStickerSetEmpty{}}); err != nil || !ok {
t.Fatalf("set stickers = ok %v err %v, want true", ok, err)
}
if _, err := r.onChannelsSetStickers(WithUserID(ctx, owner.ID), &tg.ChannelsSetStickersRequest{
Channel: input,
Stickerset: &tg.InputStickerSetID{ID: 1, AccessHash: 2},
}); err == nil || !strings.Contains(err.Error(), "STICKERSET_INVALID") {
t.Fatalf("set stickers non-empty err = %v, want STICKERSET_INVALID", err)
}
if ok, err := r.onChannelsReorderUsernames(WithUserID(ctx, owner.ID), &tg.ChannelsReorderUsernamesRequest{Channel: input, Order: []string{"public_team"}}); err != nil || !ok {
t.Fatalf("reorder usernames = ok %v err %v, want true", ok, err)
}
if _, err := r.onChannelsTogglePreHistoryHidden(WithUserID(ctx, owner.ID), &tg.ChannelsTogglePreHistoryHiddenRequest{Channel: input, Enabled: true}); err != nil {
t.Fatalf("toggle prehistory hidden: %v", err)
}
fullAfterSettings, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), input)
if err != nil {
t.Fatalf("get full channel after settings: %v", err)
}
fullSettings := fullAfterSettings.FullChat.(*tg.ChannelFull)
if !fullSettings.HiddenPrehistory || fullSettings.SlowmodeSeconds != 30 {
t.Fatalf("full settings = %+v, want hidden prehistory and slowmode=30", fullSettings)
}
joinToSendUpdates, err := r.onChannelsToggleJoinToSend(WithUserID(ctx, owner.ID), &tg.ChannelsToggleJoinToSendRequest{Channel: input, Enabled: true})
if err != nil {
t.Fatalf("toggle join-to-send: %v", err)
}
if joinToSendChannel := joinToSendUpdates.(*tg.Updates).Chats[0].(*tg.Channel); !joinToSendChannel.GetJoinToSend() {
t.Fatalf("join-to-send channel = %+v, want join_to_send flag", joinToSendChannel)
}
joinRequestUpdates, err := r.onChannelsToggleJoinRequest(WithUserID(ctx, owner.ID), &tg.ChannelsToggleJoinRequestRequest{Channel: input, Enabled: true})
if err != nil {
t.Fatalf("toggle join request: %v", err)
}
if joinRequestChannel := joinRequestUpdates.(*tg.Updates).Chats[0].(*tg.Channel); !joinRequestChannel.GetJoinRequest() {
t.Fatalf("join-request channel = %+v, want join_request flag", joinRequestChannel)
}
chatsWithJoinFlags, err := r.onChannelsGetChannels(WithUserID(ctx, owner.ID), []tg.InputChannelClass{input})
if err != nil {
t.Fatalf("get channels after join settings: %v", err)
}
listedJoinChannel := chatsWithJoinFlags.(*tg.MessagesChats).Chats[0].(*tg.Channel)
if !listedJoinChannel.GetJoinToSend() || !listedJoinChannel.GetJoinRequest() {
t.Fatalf("listed channel join flags = %+v, want both flags", listedJoinChannel)
}
if _, err := r.onChannelsJoinChannel(WithUserID(ctx, requester.ID), input); err == nil || !strings.Contains(err.Error(), "INVITE_REQUEST_SENT") {
t.Fatalf("public join request err = %v, want INVITE_REQUEST_SENT", err)
}
pendingPublic, err := r.onMessagesGetChatInviteImporters(WithUserID(ctx, owner.ID), &tg.MessagesGetChatInviteImportersRequest{
Requested: true,
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
Limit: 10,
})
if err != nil {
t.Fatalf("get public pending join request: %v", err)
}
if pendingPublic.Count != 1 || len(pendingPublic.Importers) != 1 || pendingPublic.Importers[0].UserID != requester.ID || !pendingPublic.Importers[0].Requested {
t.Fatalf("public pending importers = %+v, want requester pending", pendingPublic)
}
fullWithPublicPending, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), input)
if err != nil {
t.Fatalf("get full channel with public pending request: %v", err)
}
fullPublicPending := fullWithPublicPending.FullChat.(*tg.ChannelFull)
publicPendingCount, ok := fullPublicPending.GetRequestsPending()
publicRecent, recentOK := fullPublicPending.GetRecentRequesters()
if !ok || publicPendingCount != 1 || !recentOK || len(publicRecent) != 1 || publicRecent[0] != requester.ID {
t.Fatalf("public full pending = count %d ok %v recent %+v ok %v, want requester", publicPendingCount, ok, publicRecent, recentOK)
}
approvedPublic, err := r.onMessagesHideChatJoinRequest(WithUserID(ctx, owner.ID), &tg.MessagesHideChatJoinRequestRequest{
Approved: true,
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
UserID: &tg.InputUser{UserID: requester.ID, AccessHash: requester.AccessHash},
})
if err != nil {
t.Fatalf("approve public join request: %v", err)
}
var publicPendingCleared *tg.UpdatePendingJoinRequests
for _, update := range approvedPublic.(*tg.Updates).Updates {
if pending, ok := update.(*tg.UpdatePendingJoinRequests); ok {
publicPendingCleared = pending
break
}
}
if publicPendingCleared == nil || publicPendingCleared.RequestsPending != 0 || len(publicPendingCleared.RecentRequesters) != 0 {
t.Fatalf("public approve pending update = %+v, want cleared pending requests", publicPendingCleared)
}
colorReq := &tg.ChannelsUpdateColorRequest{Channel: input}
colorReq.SetForProfile(true)
colorReq.SetColor(1)
colorReq.SetBackgroundEmojiID(9001)
colorUpdates, err := r.onChannelsUpdateColor(WithUserID(ctx, owner.ID), colorReq)
if err != nil {
t.Fatalf("update color: %v", err)
}
colorChannel := colorUpdates.(*tg.Updates).Chats[0].(*tg.Channel)
profileColor, ok := colorChannel.GetProfileColor()
if !ok {
t.Fatalf("update color channel = %+v, want profile color", colorChannel)
}
if peerColor := profileColor.(*tg.PeerColor); peerColor.Color != 1 || peerColor.BackgroundEmojiID != 9001 {
t.Fatalf("profile color = %+v, want color/background", peerColor)
}
status := &tg.EmojiStatus{DocumentID: 9101}
status.SetUntil(1700000100)
statusUpdates, err := r.onChannelsUpdateEmojiStatus(WithUserID(ctx, owner.ID), &tg.ChannelsUpdateEmojiStatusRequest{Channel: input, EmojiStatus: status})
if err != nil {
t.Fatalf("update emoji status: %v", err)
}
statusChannel := statusUpdates.(*tg.Updates).Chats[0].(*tg.Channel)
emojiStatus, ok := statusChannel.GetEmojiStatus()
if !ok {
t.Fatalf("update emoji status channel = %+v, want emoji status", statusChannel)
}
if got := emojiStatus.(*tg.EmojiStatus); got.DocumentID != 9101 || got.Until != 1700000100 {
t.Fatalf("emoji status = %+v, want document/until", got)
}
chatsWithAppearance, err := r.onChannelsGetChannels(WithUserID(ctx, owner.ID), []tg.InputChannelClass{input})
if err != nil {
t.Fatalf("get channels after appearance settings: %v", err)
}
persistedAppearance := chatsWithAppearance.(*tg.MessagesChats).Chats[0].(*tg.Channel)
if _, ok := persistedAppearance.GetProfileColor(); !ok {
t.Fatalf("persisted channel appearance = %+v, want profile color", persistedAppearance)
}
if _, ok := persistedAppearance.GetEmojiStatus(); !ok {
t.Fatalf("persisted channel appearance = %+v, want emoji status", persistedAppearance)
}
link, err := r.onChannelsExportMessageLink(WithUserID(ctx, owner.ID), &tg.ChannelsExportMessageLinkRequest{Channel: input, ID: msgID})
if err != nil {
t.Fatalf("export message link: %v", err)
}
if !strings.Contains(link.Link, "public_team") || !strings.Contains(link.Link, strconv.Itoa(msgID)) {
t.Fatalf("exported link = %+v, want username/message id", link)
}
replyToSeed := &tg.InputReplyToMessage{ReplyToMsgID: msgID}
replyReq := &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
Message: "thread reply",
RandomID: 992,
}
replyReq.SetReplyTo(replyToSeed)
replySent, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), replyReq)
if err != nil {
t.Fatalf("send reply for export link: %v", err)
}
replyMsgID := replySent.(*tg.Updates).Updates[0].(*tg.UpdateMessageID).ID
threadLink, err := r.onChannelsExportMessageLink(WithUserID(ctx, owner.ID), &tg.ChannelsExportMessageLinkRequest{Thread: true, Channel: input, ID: replyMsgID})
if err != nil {
t.Fatalf("export thread message link: %v", err)
}
if !strings.Contains(threadLink.Link, strconv.Itoa(replyMsgID)) || !strings.Contains(threadLink.Link, "?thread="+strconv.Itoa(msgID)) {
t.Fatalf("thread exported link = %+v, want reply id and thread root %d", threadLink, msgID)
}
if _, err := r.onChannelsExportMessageLink(WithUserID(ctx, owner.ID), &tg.ChannelsExportMessageLinkRequest{Channel: input, ID: replyMsgID + 10000}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("export missing message link err = %v, want MESSAGE_ID_INVALID", err)
}
if ok, err := r.onChannelsReadMessageContents(WithUserID(ctx, owner.ID), &tg.ChannelsReadMessageContentsRequest{Channel: input, ID: []int{msgID}}); err != nil || !ok {
t.Fatalf("read message contents = ok %v err %v, want true", ok, err)
}
author, err := r.onChannelsGetMessageAuthor(WithUserID(ctx, owner.ID), &tg.ChannelsGetMessageAuthorRequest{Channel: input, ID: msgID})
if err != nil {
t.Fatalf("get message author: %v", err)
}
if author.(*tg.User).ID != owner.ID {
t.Fatalf("message author = %+v, want owner", author)
}
if _, err := r.onChannelsGetMessageAuthor(WithUserID(ctx, owner.ID), &tg.ChannelsGetMessageAuthorRequest{Channel: input, ID: msgID + 10000}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("get missing message author err = %v, want MESSAGE_ID_INVALID", err)
}
if ok, err := r.onChannelsReportSpam(WithUserID(ctx, owner.ID), &tg.ChannelsReportSpamRequest{
Channel: input,
Participant: &tg.InputPeerUser{UserID: owner.ID, AccessHash: owner.AccessHash},
ID: []int{msgID},
}); err != nil || !ok {
t.Fatalf("report spam = ok %v err %v, want true", ok, err)
}
if _, err := r.onChannelsGetLeftChannels(WithUserID(ctx, owner.ID), 0); err != nil {
t.Fatalf("get left channels: %v", err)
}
if _, err := r.onChannelsGetInactiveChannels(WithUserID(ctx, owner.ID)); err != nil {
t.Fatalf("get inactive channels: %v", err)
}
if _, err := r.onChannelsGetGroupsForDiscussion(WithUserID(ctx, owner.ID)); err != nil {
t.Fatalf("get groups for discussion: %v", err)
}
if _, err := r.onChannelsSetDiscussionGroup(WithUserID(ctx, owner.ID), &tg.ChannelsSetDiscussionGroupRequest{Broadcast: input, Group: &tg.InputChannelEmpty{}}); err == nil || !strings.Contains(err.Error(), "BROADCAST_ID_INVALID") {
t.Fatalf("set discussion group on megagroup err = %v, want BROADCAST_ID_INVALID", err)
}
if ok, err := r.onChannelsEditLocation(WithUserID(ctx, owner.ID), &tg.ChannelsEditLocationRequest{Channel: input}); err != nil || !ok {
t.Fatalf("edit location = ok %v err %v, want true", ok, err)
}
if _, err := r.onChannelsConvertToGigagroup(WithUserID(ctx, owner.ID), input); err != nil {
t.Fatalf("convert to gigagroup: %v", err)
}
// 建群服务消息id=1被保留为会话兜底 top message不随创建者
// 历史一起删除;可删的是 seed 文本与 thread reply 两条。
if affected, err := r.onChannelsDeleteParticipantHistory(WithUserID(ctx, owner.ID), &tg.ChannelsDeleteParticipantHistoryRequest{Channel: input, Participant: &tg.InputPeerUser{UserID: owner.ID, AccessHash: owner.AccessHash}}); err != nil || affected.PtsCount != 2 || affected.Pts == 0 || affected.Offset != 0 {
t.Fatalf("delete participant history = %+v err %v, want one bounded delete update for owner text and reply messages", affected, err)
}
if updates, err := r.onChannelsToggleParticipantsHidden(WithUserID(ctx, owner.ID), &tg.ChannelsToggleParticipantsHiddenRequest{Channel: input, Enabled: true}); err != nil {
t.Fatalf("toggle participants hidden: %v", err)
} else if len(updates.(*tg.Updates).Chats) != 1 {
t.Fatalf("toggle participants hidden updates = %+v, want channel chat", updates)
}
fullHidden, err := r.onChannelsGetFullChannel(WithUserID(ctx, requester.ID), input)
if err != nil {
t.Fatalf("requester get full after participants hidden: %v", err)
}
fullChannel := fullHidden.FullChat.(*tg.ChannelFull)
if hidden := fullChannel.GetParticipantsHidden(); !hidden || fullChannel.CanViewParticipants {
t.Fatalf("requester full channel = %+v, want participants_hidden and can_view_participants=false", fullChannel)
}
hiddenMembers, err := r.onChannelsGetParticipants(WithUserID(ctx, requester.ID), &tg.ChannelsGetParticipantsRequest{
Channel: input,
Filter: &tg.ChannelParticipantsRecent{},
Limit: 10,
})
if err != nil {
t.Fatalf("requester get participants hidden: %v", err)
}
if page := hiddenMembers.(*tg.ChannelsChannelParticipants); len(page.Participants) != 0 || page.Count == 0 {
t.Fatalf("hidden participants page = %+v, want empty page with aggregate count", page)
}
viewAsMessagesUpdates, err := r.onChannelsToggleViewForumAsMessages(WithUserID(ctx, owner.ID), &tg.ChannelsToggleViewForumAsMessagesRequest{Channel: input, Enabled: true})
if err != nil {
t.Fatalf("toggle forum as messages: %v", err)
}
viewAsMessagesUpdate, ok := viewAsMessagesUpdates.(*tg.Updates).Updates[0].(*tg.UpdateChannelViewForumAsMessages)
if !ok || viewAsMessagesUpdate.ChannelID != channel.ID || !viewAsMessagesUpdate.Enabled {
t.Fatalf("toggle forum as messages updates = %+v, want updateChannelViewForumAsMessages", viewAsMessagesUpdates)
}
fullWithViewAsMessages, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), input)
if err != nil {
t.Fatalf("full channel after view as messages: %v", err)
}
if full := fullWithViewAsMessages.FullChat.(*tg.ChannelFull); !full.ViewForumAsMessages {
t.Fatalf("full channel view_forum_as_messages = false, want true")
}
forumUpdates, err := r.onChannelsToggleForum(WithUserID(ctx, owner.ID), &tg.ChannelsToggleForumRequest{Channel: input, Enabled: true, Tabs: true})
if err != nil {
t.Fatalf("toggle forum: %v", err)
}
if len(forumUpdates.(*tg.Updates).Chats) != 1 {
t.Fatalf("toggle forum updates = %+v, want channel chat", forumUpdates)
}
forumChat, ok := forumUpdates.(*tg.Updates).Chats[0].(*tg.Channel)
if !ok || !forumChat.Forum || !forumChat.ForumTabs {
t.Fatalf("toggle forum chat = %+v, want forum+forum_tabs", forumUpdates.(*tg.Updates).Chats[0])
}
forumPeer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
forumTopics, err := r.onMessagesGetForumTopics(WithUserID(ctx, owner.ID), &tg.MessagesGetForumTopicsRequest{
Peer: forumPeer,
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getForumTopics after toggle: %v", err)
}
if forumTopics.Count != 1 || len(forumTopics.Topics) != 1 || len(forumTopics.Chats) != 1 || forumTopics.Pts == 0 {
t.Fatalf("messages.getForumTopics after toggle = %+v, want general topic with channel context", forumTopics)
}
generalTopic, ok := forumTopics.Topics[0].(*tg.ForumTopic)
if !ok || generalTopic.ID != forumGeneralTopicID || generalTopic.Title != "General" {
t.Fatalf("forum topic = %+v, want General topic", forumTopics.Topics[0])
}
forumTopicsByID, err := r.onMessagesGetForumTopicsByID(WithUserID(ctx, owner.ID), &tg.MessagesGetForumTopicsByIDRequest{
Peer: forumPeer,
Topics: []int{forumGeneralTopicID},
})
if err != nil {
t.Fatalf("messages.getForumTopicsByID after toggle: %v", err)
}
if forumTopicsByID.Count != 1 || len(forumTopicsByID.Topics) != 1 {
t.Fatalf("messages.getForumTopicsByID after toggle = %+v, want General topic", forumTopicsByID)
}
createdForumTopic, err := r.onMessagesCreateForumTopic(WithUserID(ctx, owner.ID), &tg.MessagesCreateForumTopicRequest{
Peer: forumPeer,
Title: "Ops",
IconColor: domain.DefaultForumTopicIconColor,
RandomID: 17803001,
})
if err != nil {
t.Fatalf("messages.createForumTopic after toggle: %v", err)
}
var topicID int
for _, update := range createdForumTopic.(*tg.Updates).Updates {
newMsg, ok := update.(*tg.UpdateNewChannelMessage)
if !ok {
continue
}
msg, ok := newMsg.Message.(*tg.MessageService)
if !ok {
continue
}
action, ok := msg.Action.(*tg.MessageActionTopicCreate)
if ok && action.Title == "Ops" {
topicID = msg.ID
break
}
}
if topicID == 0 {
t.Fatalf("messages.createForumTopic updates = %+v, want topic root service message", createdForumTopic)
}
forumTopicsWithCreated, err := r.onMessagesGetForumTopics(WithUserID(ctx, owner.ID), &tg.MessagesGetForumTopicsRequest{
Peer: forumPeer,
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getForumTopics with created topic: %v", err)
}
if forumTopicsWithCreated.Count != 2 || len(forumTopicsWithCreated.Topics) != 2 || len(forumTopicsWithCreated.Messages) == 0 {
t.Fatalf("messages.getForumTopics with created topic = %+v, want General + created topic + root message", forumTopicsWithCreated)
}
// 回归:官方 web(telegram-tt 首屏 loadTopics)发 limit=500,超单页上限。服务端必须钳到上限正常返回,
// 而非报 LIMIT_INVALID(此前 web 端每次 getForumTopics 恒失败)。客户端据返回的 count 用 offset 翻页。
forumTopicsLargeLimit, err := r.onMessagesGetForumTopics(WithUserID(ctx, owner.ID), &tg.MessagesGetForumTopicsRequest{
Peer: forumPeer,
Limit: 500,
})
if err != nil {
t.Fatalf("messages.getForumTopics limit=500 should clamp to page size, got err: %v", err)
}
if forumTopicsLargeLimit.Count != 2 || len(forumTopicsLargeLimit.Topics) != 2 {
t.Fatalf("messages.getForumTopics limit=500 = %+v, want clamped result (General + created topic)", forumTopicsLargeLimit)
}
var foundCreated bool
for _, item := range forumTopicsWithCreated.Topics {
topic, ok := item.(*tg.ForumTopic)
if ok && topic.ID == topicID && topic.Title == "Ops" && topic.TopMessage == topicID {
foundCreated = true
break
}
}
if !foundCreated {
t.Fatalf("messages.getForumTopics topics = %+v, want created topic id %d", forumTopicsWithCreated.Topics, topicID)
}
forumTopicsByID, err = r.onMessagesGetForumTopicsByID(WithUserID(ctx, owner.ID), &tg.MessagesGetForumTopicsByIDRequest{
Peer: forumPeer,
Topics: []int{forumGeneralTopicID, topicID},
})
if err != nil {
t.Fatalf("messages.getForumTopicsByID with created topic: %v", err)
}
if forumTopicsByID.Count != 2 || len(forumTopicsByID.Topics) != 2 || len(forumTopicsByID.Messages) == 0 {
t.Fatalf("messages.getForumTopicsByID with created topic = %+v, want General + created topic", forumTopicsByID)
}
topicReply := &tg.InputReplyToMessage{ReplyToMsgID: 0}
topicReply.SetTopMsgID(topicID)
topicSendReq := &tg.MessagesSendMessageRequest{
Peer: forumPeer,
Message: "topic body",
RandomID: 17803002,
}
topicSendReq.SetReplyTo(topicReply)
topicSent, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), topicSendReq)
if err != nil {
t.Fatalf("messages.sendMessage topic-only reply: %v", err)
}
var topicMsg *tg.Message
for _, update := range topicSent.(*tg.Updates).Updates {
newMsg, ok := update.(*tg.UpdateNewChannelMessage)
if !ok {
continue
}
if msg, ok := newMsg.Message.(*tg.Message); ok && msg.Message == "topic body" {
topicMsg = msg
break
}
}
if topicMsg == nil {
t.Fatalf("messages.sendMessage topic updates = %+v, want topic message", topicSent)
}
header, ok := topicMsg.ReplyTo.(*tg.MessageReplyHeader)
if !ok || !header.ForumTopic || header.ReplyToMsgID != 0 {
t.Fatalf("topic reply header = %#v, want forum topic without reply_to_msg_id", topicMsg.ReplyTo)
}
if topID, ok := header.GetReplyToTopID(); !ok || topID != topicID {
t.Fatalf("topic reply top id = %d ok %v, want %d", topID, ok, topicID)
}
topicReplies, err := r.onMessagesGetReplies(WithUserID(ctx, owner.ID), &tg.MessagesGetRepliesRequest{
Peer: forumPeer,
MsgID: topicID,
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getReplies forum topic: %v", err)
}
topicReplyPage, ok := topicReplies.(*tg.MessagesChannelMessages)
if !ok || topicReplyPage.Pts == 0 || len(topicReplyPage.Messages) != 1 || len(topicReplyPage.Topics) != 1 {
t.Fatalf("messages.getReplies forum topic = %T %+v, want channelMessages with one topic", topicReplies, topicReplies)
}
if got := topicReplyPage.Messages[0].(*tg.Message); got.ID != topicMsg.ID || got.Message != "topic body" {
t.Fatalf("topic reply message = %#v, want id %d", got, topicMsg.ID)
}
if topic, ok := topicReplyPage.Topics[0].(*tg.ForumTopic); !ok || !topic.Short || topic.ID != topicID || topic.TopMessage != topicMsg.ID {
t.Fatalf("topic reply page topic = %#v, want short topic with updated top message %d", topicReplyPage.Topics[0], topicMsg.ID)
}
forumTopicsAfterMessage, err := r.onMessagesGetForumTopics(WithUserID(ctx, owner.ID), &tg.MessagesGetForumTopicsRequest{
Peer: forumPeer,
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getForumTopics after topic message: %v", err)
}
var foundTopicTop bool
for _, item := range forumTopicsAfterMessage.Topics {
topic, ok := item.(*tg.ForumTopic)
if ok && topic.ID == topicID && topic.TopMessage == topicMsg.ID {
foundTopicTop = true
break
}
}
if !foundTopicTop {
t.Fatalf("messages.getForumTopics after topic message topics = %+v, want top message %d", forumTopicsAfterMessage.Topics, topicMsg.ID)
}
forwardToTopicReq := &tg.MessagesForwardMessagesRequest{
FromPeer: forumPeer,
ToPeer: forumPeer,
ID: []int{topicMsg.ID},
RandomID: []int64{17803003},
}
forwardToTopicReq.SetTopMsgID(topicID)
forwardToTopic, err := r.onMessagesForwardMessages(WithUserID(ctx, owner.ID), forwardToTopicReq)
if err != nil {
t.Fatalf("messages.forwardMessages to forum topic: %v", err)
}
var forwardedTopicMsg *tg.Message
for _, update := range forwardToTopic.(*tg.Updates).Updates {
newMsg, ok := update.(*tg.UpdateNewChannelMessage)
if !ok {
continue
}
if msg, ok := newMsg.Message.(*tg.Message); ok && msg.Message == "topic body" && msg.ID != topicMsg.ID {
forwardedTopicMsg = msg
break
}
}
if forwardedTopicMsg == nil {
t.Fatalf("messages.forwardMessages to topic updates = %+v, want forwarded topic message", forwardToTopic)
}
if _, ok := forwardedTopicMsg.GetFwdFrom(); !ok {
t.Fatalf("forwarded topic message = %#v, want fwd header", forwardedTopicMsg)
}
forwardReply, ok := forwardedTopicMsg.ReplyTo.(*tg.MessageReplyHeader)
if !ok || !forwardReply.ForumTopic || forwardReply.ReplyToMsgID != 0 {
t.Fatalf("forward topic reply header = %#v, want forum topic without reply_to_msg_id", forwardedTopicMsg.ReplyTo)
}
if topID, ok := forwardReply.GetReplyToTopID(); !ok || topID != topicID {
t.Fatalf("forward topic reply top id = %d ok %v, want %d", topID, ok, topicID)
}
forumTopicsAfterForward, err := r.onMessagesGetForumTopics(WithUserID(ctx, owner.ID), &tg.MessagesGetForumTopicsRequest{
Peer: forumPeer,
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getForumTopics after forward: %v", err)
}
var foundForwardTop bool
for _, item := range forumTopicsAfterForward.Topics {
topic, ok := item.(*tg.ForumTopic)
if ok && topic.ID == topicID && topic.TopMessage == forwardedTopicMsg.ID {
foundForwardTop = true
break
}
}
if !foundForwardTop {
t.Fatalf("messages.getForumTopics after forward topics = %+v, want top message %d", forumTopicsAfterForward.Topics, forwardedTopicMsg.ID)
}
invalidForwardTop := &tg.MessagesForwardMessagesRequest{
FromPeer: forumPeer,
ToPeer: forumPeer,
ID: []int{topicMsg.ID},
RandomID: []int64{17803004},
}
invalidForwardTop.SetTopMsgID(domain.MaxMessageBoxID + 1)
if _, err := r.onMessagesForwardMessages(WithUserID(ctx, owner.ID), invalidForwardTop); err == nil || !strings.Contains(err.Error(), "REPLY_MESSAGE_ID_INVALID") {
t.Fatalf("messages.forwardMessages huge top_msg_id err = %v, want REPLY_MESSAGE_ID_INVALID", err)
}
editTopicReq := &tg.MessagesEditForumTopicRequest{Peer: forumPeer, TopicID: topicID}
editTopicReq.SetTitle("Ops 2")
editTopicReq.SetClosed(true)
editedForumTopic, err := r.onMessagesEditForumTopic(WithUserID(ctx, owner.ID), editTopicReq)
if err != nil {
t.Fatalf("messages.editForumTopic: %v", err)
}
var editedRootID int
for _, update := range editedForumTopic.(*tg.Updates).Updates {
newMsg, ok := update.(*tg.UpdateNewChannelMessage)
if !ok {
continue
}
msg, ok := newMsg.Message.(*tg.MessageService)
if !ok {
continue
}
action, ok := msg.Action.(*tg.MessageActionTopicEdit)
if ok && action.Title == "Ops 2" {
if closed, closedOK := action.GetClosed(); closedOK && closed {
editedRootID = msg.ID
break
}
}
}
if editedRootID == 0 {
t.Fatalf("messages.editForumTopic updates = %+v, want topic edit service message", editedForumTopic)
}
pinnedTopic, err := r.onMessagesUpdatePinnedForumTopic(WithUserID(ctx, owner.ID), &tg.MessagesUpdatePinnedForumTopicRequest{
Peer: forumPeer,
TopicID: topicID,
Pinned: true,
})
if err != nil {
t.Fatalf("messages.updatePinnedForumTopic: %v", err)
}
if got := pinnedTopic.(*tg.Updates).Updates; len(got) != 1 {
t.Fatalf("messages.updatePinnedForumTopic updates = %+v, want one update", got)
} else if update, ok := got[0].(*tg.UpdatePinnedForumTopic); !ok || update.TopicID != topicID || !update.GetPinned() {
t.Fatalf("messages.updatePinnedForumTopic update = %+v, want pinned topic", got[0])
}
reorderedTopics, err := r.onMessagesReorderPinnedForumTopics(WithUserID(ctx, owner.ID), &tg.MessagesReorderPinnedForumTopicsRequest{
Peer: forumPeer,
Order: []int{topicID},
Force: true,
})
if err != nil {
t.Fatalf("messages.reorderPinnedForumTopics: %v", err)
}
if got := reorderedTopics.(*tg.Updates).Updates; len(got) != 1 {
t.Fatalf("messages.reorderPinnedForumTopics updates = %+v, want one update", got)
} else if update, ok := got[0].(*tg.UpdatePinnedForumTopics); !ok || len(update.Order) != 1 || update.Order[0] != topicID {
t.Fatalf("messages.reorderPinnedForumTopics update = %+v, want order", got[0])
}
deletedTopic, err := r.onMessagesDeleteTopicHistory(WithUserID(ctx, owner.ID), &tg.MessagesDeleteTopicHistoryRequest{
Peer: forumPeer,
TopMsgID: topicID,
})
if err != nil {
t.Fatalf("messages.deleteTopicHistory: %v", err)
}
if deletedTopic.Pts == 0 || deletedTopic.PtsCount == 0 || deletedTopic.Offset != 0 {
t.Fatalf("messages.deleteTopicHistory = %+v, want affected history with final offset", deletedTopic)
}
forumTopicsAfterDelete, err := r.onMessagesGetForumTopics(WithUserID(ctx, owner.ID), &tg.MessagesGetForumTopicsRequest{
Peer: forumPeer,
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getForumTopics after delete topic: %v", err)
}
if forumTopicsAfterDelete.Count != 1 || len(forumTopicsAfterDelete.Topics) != 1 {
t.Fatalf("messages.getForumTopics after delete topic = %+v, want General only", forumTopicsAfterDelete)
}
antiSpamUpdates, err := r.onChannelsToggleAntiSpam(WithUserID(ctx, owner.ID), &tg.ChannelsToggleAntiSpamRequest{Channel: input, Enabled: true})
if err != nil {
t.Fatalf("toggle antispam: %v", err)
}
if len(antiSpamUpdates.(*tg.Updates).Chats) != 1 {
t.Fatalf("toggle antispam updates = %+v, want channel chat", antiSpamUpdates)
}
fullWithAntiSpam, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), input)
if err != nil {
t.Fatalf("full channel after antispam: %v", err)
}
if full := fullWithAntiSpam.FullChat.(*tg.ChannelFull); !full.GetAntispam() {
t.Fatalf("full channel antispam = false, want true")
}
settingsFilter := tg.ChannelAdminLogEventsFilter{}
settingsFilter.SetSettings(true)
antiSpamLog, err := r.onChannelsGetAdminLog(WithUserID(ctx, owner.ID), &tg.ChannelsGetAdminLogRequest{
Channel: input,
EventsFilter: settingsFilter,
Limit: 10,
})
if err != nil {
t.Fatalf("admin log after antispam: %v", err)
}
foundAntiSpamLog := false
foundForumLog := false
for _, event := range antiSpamLog.Events {
if action, ok := event.Action.(*tg.ChannelAdminLogEventActionToggleAntiSpam); ok && action.NewValue {
foundAntiSpamLog = true
}
if action, ok := event.Action.(*tg.ChannelAdminLogEventActionToggleForum); ok && action.NewValue {
foundForumLog = true
}
}
if !foundAntiSpamLog || !foundForumLog {
t.Fatalf("admin log events = %+v, want toggle antispam and toggle forum actions", antiSpamLog.Events)
}
channelUpdateStubs := []struct {
name string
call func() (tg.UpdatesClass, error)
}{
{"set boosts", func() (tg.UpdatesClass, error) {
return r.onChannelsSetBoostsToUnblockRestrictions(WithUserID(ctx, owner.ID), &tg.ChannelsSetBoostsToUnblockRestrictionsRequest{Channel: input, Boosts: 1})
}},
{"restrict sponsored", func() (tg.UpdatesClass, error) {
return r.onChannelsRestrictSponsoredMessages(WithUserID(ctx, owner.ID), &tg.ChannelsRestrictSponsoredMessagesRequest{Channel: input, Restricted: true})
}},
{"update paid messages", func() (tg.UpdatesClass, error) {
return r.onChannelsUpdatePaidMessagesPrice(WithUserID(ctx, owner.ID), &tg.ChannelsUpdatePaidMessagesPriceRequest{Channel: input, SendPaidMessagesStars: 7})
}},
{"toggle autotranslation", func() (tg.UpdatesClass, error) {
return r.onChannelsToggleAutotranslation(WithUserID(ctx, owner.ID), &tg.ChannelsToggleAutotranslationRequest{Channel: input, Enabled: true})
}},
}
for _, item := range channelUpdateStubs {
updates, err := item.call()
if err != nil {
t.Fatalf("%s: %v", item.name, err)
}
if len(updates.(*tg.Updates).Chats) != 1 {
t.Fatalf("%s updates = %+v, want channel chat", item.name, updates)
}
}
fullAfterPaidSettings, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), input)
if err != nil {
t.Fatalf("full channel after paid/settings toggles: %v", err)
}
fullPaidSettings := fullAfterPaidSettings.FullChat.(*tg.ChannelFull)
if !fullPaidSettings.GetRestrictedSponsored() {
t.Fatalf("full channel restricted_sponsored = false, want true")
}
if stars, ok := fullPaidSettings.GetSendPaidMessagesStars(); !fullPaidSettings.GetPaidMessagesAvailable() || !ok || stars != 7 {
t.Fatalf("full channel paid messages = available %v stars %d ok %v, want available true stars 7", fullPaidSettings.GetPaidMessagesAvailable(), stars, ok)
}
if boosts, ok := fullPaidSettings.GetBoostsUnrestrict(); !ok || boosts != 1 {
t.Fatalf("full channel boosts_unrestrict = %d ok %v, want 1", boosts, ok)
}
chatsAfterAutotranslation, err := r.onChannelsGetChannels(WithUserID(ctx, owner.ID), []tg.InputChannelClass{input})
if err != nil {
t.Fatalf("get channels after autotranslation: %v", err)
}
channelAfterAutotranslation := chatsAfterAutotranslation.(*tg.MessagesChats).Chats[0].(*tg.Channel)
if !channelAfterAutotranslation.GetAutotranslation() {
t.Fatalf("channel autotranslation = false, want true")
}
autoLog, err := r.onChannelsGetAdminLog(WithUserID(ctx, owner.ID), &tg.ChannelsGetAdminLogRequest{
Channel: input,
EventsFilter: settingsFilter,
Limit: 10,
})
if err != nil {
t.Fatalf("admin log after autotranslation: %v", err)
}
foundAutoLog := false
for _, event := range autoLog.Events {
if action, ok := event.Action.(*tg.ChannelAdminLogEventActionToggleAutotranslation); ok && action.NewValue {
foundAutoLog = true
break
}
}
if !foundAutoLog {
t.Fatalf("admin log events = %+v, want toggle autotranslation action", autoLog.Events)
}
if _, err := r.onChannelsSetBoostsToUnblockRestrictions(WithUserID(ctx, owner.ID), &tg.ChannelsSetBoostsToUnblockRestrictionsRequest{Channel: input, Boosts: maxChannelBoostsToUnblockRestrictions + 1}); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("set boosts over max err = %v, want LIMIT_INVALID", err)
}
if _, err := r.onChannelsUpdatePaidMessagesPrice(WithUserID(ctx, owner.ID), &tg.ChannelsUpdatePaidMessagesPriceRequest{Channel: input, SendPaidMessagesStars: -1}); err == nil || !strings.Contains(err.Error(), "STARS_AMOUNT_INVALID") {
t.Fatalf("update paid messages negative supergroup err = %v, want STARS_AMOUNT_INVALID", err)
}
if _, err := r.onChannelsUpdatePaidMessagesPrice(WithUserID(ctx, owner.ID), &tg.ChannelsUpdatePaidMessagesPriceRequest{Channel: input, SendPaidMessagesStars: maxChannelPaidMessageStars + 1}); err == nil || !strings.Contains(err.Error(), "STARS_AMOUNT_INVALID") {
t.Fatalf("update paid messages over max err = %v, want STARS_AMOUNT_INVALID", err)
}
broadcastCreated, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), &tg.ChannelsCreateChannelRequest{
Title: "Broadcast Paid",
About: "broadcast direct messages",
Broadcast: true,
})
if err != nil {
t.Fatalf("create broadcast channel: %v", err)
}
broadcast := broadcastCreated.(*tg.Updates).Chats[0].(*tg.Channel)
broadcastInput := &tg.InputChannel{ChannelID: broadcast.ID, AccessHash: broadcast.AccessHash}
disabledUpdates, err := r.onChannelsUpdatePaidMessagesPrice(WithUserID(ctx, owner.ID), &tg.ChannelsUpdatePaidMessagesPriceRequest{Channel: broadcastInput, SendPaidMessagesStars: -1})
if err != nil {
t.Fatalf("update paid messages broadcast disable: %v", err)
}
if len(disabledUpdates.(*tg.Updates).Chats) != 1 {
t.Fatalf("disable paid messages chats = %d, want parent only", len(disabledUpdates.(*tg.Updates).Chats))
}
if _, ok := disabledUpdates.(*tg.Updates).Chats[0].(*tg.Channel).GetLinkedMonoforumID(); ok {
t.Fatalf("disable paid messages projected linked_monoforum_id, want hidden")
}
freeUpdates, err := r.onChannelsUpdatePaidMessagesPrice(WithUserID(ctx, owner.ID), &tg.ChannelsUpdatePaidMessagesPriceRequest{
BroadcastMessagesAllowed: true,
Channel: broadcastInput,
SendPaidMessagesStars: 0,
})
if err != nil {
t.Fatalf("update paid messages broadcast free: %v", err)
}
freeChats := freeUpdates.(*tg.Updates).Chats
if len(freeChats) != 2 {
t.Fatalf("enable free direct messages chats = %d, want parent + monoforum", len(freeChats))
}
freeParent := freeChats[0].(*tg.Channel)
monoID, ok := freeParent.GetLinkedMonoforumID()
if !ok || monoID == 0 {
t.Fatalf("enable free direct messages parent linked_monoforum_id = %d ok %v, want monoforum id", monoID, ok)
}
if stars, ok := freeParent.GetSendPaidMessagesStars(); !ok || stars != 0 {
t.Fatalf("free parent send_paid_messages_stars = %d ok %v, want 0 true", stars, ok)
}
monoChat, ok := freeChats[1].(*tg.Channel)
if !ok || monoChat.ID != monoID || !monoChat.GetMonoforum() || monoChat.GetMin() {
t.Fatalf("enable free direct messages linked chat = %T %+v, want full monoforum %d", freeChats[1], freeChats[1], monoID)
}
if stars, ok := monoChat.GetSendPaidMessagesStars(); !ok || stars != 0 {
t.Fatalf("free monoforum send_paid_messages_stars = %d ok %v, want 0 true", stars, ok)
}
if parentID, ok := monoChat.GetLinkedMonoforumID(); !ok || parentID != broadcast.ID {
t.Fatalf("free monoforum linked_monoforum_id = %d ok %v, want parent %d", parentID, ok, broadcast.ID)
}
if monoChat.Broadcast || !monoChat.Megagroup {
t.Fatalf("free monoforum kind = broadcast:%v megagroup:%v, want megagroup monoforum", monoChat.Broadcast, monoChat.Megagroup)
}
// 母广播频道收到 paid_messages_price(渲染 "Channel enabled Direct Messages");monoforum 首次
// 创建则收到 channelCreate 创建消息(渲染 "Direct messages were enabled in this channel."),
// 不再收 paid_messages_price——在 megagroup 里那会渲染成错误的"消息免费/设价"文案。
freeServices := paidMessagesPriceServicePeers(t, freeUpdates)
if price := freeServices[broadcast.ID]; price == nil || !price.BroadcastMessagesAllowed || price.Stars != 0 {
t.Fatalf("free parent direct messages action = %+v, want enabled stars=0", price)
}
if _, ok := freeServices[monoID]; ok {
t.Fatalf("free monoforum got a paid_messages_price service, want only the channelCreate creation message")
}
if _, ok := monoforumCreationServicePeers(t, freeUpdates)[monoID]; !ok {
t.Fatalf("free enable did not deliver monoforum creation service for %d", monoID)
}
paidUpdates, err := r.onChannelsUpdatePaidMessagesPrice(WithUserID(ctx, owner.ID), &tg.ChannelsUpdatePaidMessagesPriceRequest{
BroadcastMessagesAllowed: true,
Channel: broadcastInput,
SendPaidMessagesStars: maxChannelPaidMessageStars,
})
if err != nil {
t.Fatalf("update paid messages broadcast max: %v", err)
}
paidChats := paidUpdates.(*tg.Updates).Chats
if len(paidChats) != 2 {
t.Fatalf("enable paid messages chats = %d, want parent + monoforum", len(paidChats))
}
paidParent := paidChats[0].(*tg.Channel)
paidMonoID, ok := paidParent.GetLinkedMonoforumID()
if !ok || paidMonoID != monoID {
t.Fatalf("enable paid messages parent linked_monoforum_id = %d ok %v, want stable monoforum id %d", paidMonoID, ok, monoID)
}
monoChat, ok = paidChats[1].(*tg.Channel)
if !ok || monoChat.ID != monoID || !monoChat.GetMonoforum() || monoChat.GetMin() {
t.Fatalf("enable paid messages linked chat = %T %+v, want full monoforum %d", paidChats[1], paidChats[1], monoID)
}
if stars, ok := monoChat.GetSendPaidMessagesStars(); !ok || stars != maxChannelPaidMessageStars {
t.Fatalf("paid monoforum send_paid_messages_stars = %d ok %v, want %d true", stars, ok, maxChannelPaidMessageStars)
}
if parentID, ok := monoChat.GetLinkedMonoforumID(); !ok || parentID != broadcast.ID {
t.Fatalf("monoforum linked_monoforum_id = %d ok %v, want parent %d", parentID, ok, broadcast.ID)
}
if monoChat.Broadcast || !monoChat.Megagroup {
t.Fatalf("paid monoforum kind = broadcast:%v megagroup:%v, want megagroup monoforum", monoChat.Broadcast, monoChat.Megagroup)
}
// 价格变更只进母频道;monoforum 已存在,不再收任何服务消息(仍作为引用 chat 被同批下发)。
paidServices := paidMessagesPriceServicePeers(t, paidUpdates)
if price := paidServices[broadcast.ID]; price == nil || !price.BroadcastMessagesAllowed || price.Stars != maxChannelPaidMessageStars {
t.Fatalf("paid parent direct messages action = %+v, want enabled stars=%d", price, maxChannelPaidMessageStars)
}
if _, ok := paidServices[monoID]; ok {
t.Fatalf("paid monoforum got a paid_messages_price service on re-enable, want none")
}
disabledAfterEnableUpdates, err := r.onChannelsUpdatePaidMessagesPrice(WithUserID(ctx, owner.ID), &tg.ChannelsUpdatePaidMessagesPriceRequest{Channel: broadcastInput, SendPaidMessagesStars: -1})
if err != nil {
t.Fatalf("disable direct messages after enable: %v", err)
}
disabledAfterEnableChats := disabledAfterEnableUpdates.(*tg.Updates).Chats
// 关闭后只下发母频道(linked_monoforum_id 被隐藏 → 客户端缓存里 monoforum 翻出 MonoforumDisabled
// 状态显示停用页脚);monoforum 自身不再随关闭重新下发(无新服务消息,且 broadcast_messages_allowed
// =false 时 linkedMonoforumForChannelState 也不再附带它)。
if len(disabledAfterEnableChats) != 1 {
t.Fatalf("disable direct messages chats = %d, want parent only", len(disabledAfterEnableChats))
}
disabledParent := disabledAfterEnableChats[0].(*tg.Channel)
if disabledParent.GetBroadcastMessagesAllowed() {
t.Fatalf("disable direct messages parent broadcast_messages_allowed = true, want false")
}
if _, ok := disabledParent.GetLinkedMonoforumID(); ok {
t.Fatalf("disable direct messages parent projected linked_monoforum_id, want hidden")
}
// 关闭只进母频道;monoforum 不再收服务消息(停用页脚靠客户端从隐藏的 linked_monoforum_id 派生)。
disabledServices := paidMessagesPriceServicePeers(t, disabledAfterEnableUpdates)
if price := disabledServices[broadcast.ID]; price == nil || price.BroadcastMessagesAllowed || price.Stars != 0 {
t.Fatalf("disable parent direct messages action = %+v, want disabled stars=0", price)
}
if _, ok := disabledServices[monoID]; ok {
t.Fatalf("disable monoforum got a paid_messages_price service, want none")
}
broadcastChats, err := r.onChannelsGetChannels(WithUserID(ctx, owner.ID), []tg.InputChannelClass{broadcastInput})
if err != nil {
t.Fatalf("get broadcast after paid messages: %v", err)
}
broadcastAfterPaid := broadcastChats.(*tg.MessagesChats).Chats[0].(*tg.Channel)
if broadcastAfterPaid.GetBroadcastMessagesAllowed() {
t.Fatalf("broadcast_messages_allowed = true after disable, want false")
}
if _, ok := broadcastAfterPaid.GetLinkedMonoforumID(); ok {
t.Fatalf("broadcast after disable projected linked_monoforum_id, want hidden")
}
_, err = r.onChannelsUpdatePaidMessagesPrice(WithUserID(ctx, owner.ID), &tg.ChannelsUpdatePaidMessagesPriceRequest{
BroadcastMessagesAllowed: true,
Channel: broadcastInput,
SendPaidMessagesStars: maxChannelPaidMessageStars,
})
if err != nil {
t.Fatalf("re-enable direct messages before full channel: %v", err)
}
fullBroadcast, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), broadcastInput)
if err != nil {
t.Fatalf("get full broadcast after paid messages: %v", err)
}
if len(fullBroadcast.Chats) != 2 {
t.Fatalf("full broadcast chats = %d, want parent + monoforum", len(fullBroadcast.Chats))
}
fullParent := fullBroadcast.Chats[0].(*tg.Channel)
fullMonoID, ok := fullParent.GetLinkedMonoforumID()
if !ok || fullMonoID != monoID {
t.Fatalf("full parent linked_monoforum_id = %d ok %v, want %d", fullMonoID, ok, monoID)
}
fullMono := fullBroadcast.Chats[1].(*tg.Channel)
if fullMono.ID != monoID || !fullMono.GetMonoforum() || fullMono.GetMin() {
t.Fatalf("full monoforum chat = %+v, want full monoforum %d", fullMono, monoID)
}
if fullMono.Broadcast || !fullMono.Megagroup {
t.Fatalf("full monoforum kind = broadcast:%v megagroup:%v, want megagroup monoforum", fullMono.Broadcast, fullMono.Megagroup)
}
cachedFullBroadcast, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), broadcastInput)
if err != nil {
t.Fatalf("get cached full broadcast after paid messages: %v", err)
}
if len(cachedFullBroadcast.Chats) != 2 {
t.Fatalf("cached full broadcast chats = %d, want parent + monoforum", len(cachedFullBroadcast.Chats))
}
if _, err := r.onChannelsSetStickers(WithUserID(ctx, owner.ID), &tg.ChannelsSetStickersRequest{Channel: broadcastInput, Stickerset: &tg.InputStickerSetEmpty{}}); err == nil || !strings.Contains(err.Error(), "CHANNEL_INVALID") {
t.Fatalf("set stickers broadcast err = %v, want CHANNEL_INVALID", err)
}
if ok, err := r.onChannelsReportAntiSpamFalsePositive(WithUserID(ctx, owner.ID), &tg.ChannelsReportAntiSpamFalsePositiveRequest{Channel: input, MsgID: msgID}); err != nil || !ok {
t.Fatalf("report antispam false positive = ok %v err %v, want true", ok, err)
}
recommendationsReq := &tg.ChannelsGetChannelRecommendationsRequest{}
recommendationsReq.SetChannel(input)
if _, err := r.onChannelsGetChannelRecommendations(WithUserID(ctx, owner.ID), recommendationsReq); err == nil || !strings.Contains(err.Error(), "CHANNEL_INVALID") {
t.Fatalf("megagroup channel recommendations err = %v, want CHANNEL_INVALID", err)
}
if ok, err := r.onChannelsSetEmojiStickers(WithUserID(ctx, owner.ID), &tg.ChannelsSetEmojiStickersRequest{Channel: input, Stickerset: &tg.InputStickerSetEmpty{}}); err != nil || !ok {
t.Fatalf("set emoji stickers = ok %v err %v, want true", ok, err)
}
if _, err := r.onChannelsSetEmojiStickers(WithUserID(ctx, owner.ID), &tg.ChannelsSetEmojiStickersRequest{
Channel: input,
Stickerset: &tg.InputStickerSetShortName{ShortName: "custom_emoji"},
}); err == nil || !strings.Contains(err.Error(), "STICKERSET_INVALID") {
t.Fatalf("set emoji stickers non-empty err = %v, want STICKERSET_INVALID", err)
}
if _, err := r.onChannelsSetEmojiStickers(WithUserID(ctx, owner.ID), &tg.ChannelsSetEmojiStickersRequest{Channel: broadcastInput, Stickerset: &tg.InputStickerSetEmpty{}}); err == nil || !strings.Contains(err.Error(), "CHANNEL_INVALID") {
t.Fatalf("set emoji stickers broadcast err = %v, want CHANNEL_INVALID", err)
}
searchPostsReq := &tg.ChannelsSearchPostsRequest{OffsetPeer: &tg.InputPeerEmpty{}, Limit: 10}
searchPostsReq.SetHashtag("ops")
if posts, err := r.onChannelsSearchPosts(WithUserID(ctx, owner.ID), searchPostsReq); err != nil || len(posts.(*tg.MessagesMessages).Messages) != 0 {
t.Fatalf("search posts = %+v err %v, want empty", posts, err)
}
floodReq := &tg.ChannelsCheckSearchPostsFloodRequest{}
floodReq.SetQuery("ops")
if flood, err := r.onChannelsCheckSearchPostsFlood(WithUserID(ctx, owner.ID), floodReq); err != nil || !flood.QueryIsFree {
t.Fatalf("check search posts flood = %+v err %v, want free", flood, err)
}
if ok, err := r.onChannelsSetMainProfileTab(WithUserID(ctx, owner.ID), &tg.ChannelsSetMainProfileTabRequest{Channel: input}); err != nil || !ok {
t.Fatalf("set main profile tab = ok %v err %v, want true", ok, err)
}
if ok, err := r.onChannelsToggleUsername(WithUserID(ctx, owner.ID), &tg.ChannelsToggleUsernameRequest{Channel: input, Username: "public_team", Active: false}); err != nil || !ok {
t.Fatalf("toggle username = ok %v err %v, want true", ok, err)
}
if ok, err := r.onChannelsDeactivateAllUsernames(WithUserID(ctx, owner.ID), input); err != nil || !ok {
t.Fatalf("deactivate usernames = ok %v err %v, want true", ok, err)
}
stillPublic, err := r.onChannelsGetChannels(WithUserID(ctx, owner.ID), []tg.InputChannelClass{input})
if err != nil {
t.Fatalf("get channel after deactivate: %v", err)
}
if got := stillPublic.(*tg.MessagesChats).Chats[0].(*tg.Channel); got.Username != "public_team" {
t.Fatalf("channel after fragment username stubs = %+v, want primary username preserved", got)
}
if ok, err := r.onChannelsUpdateUsername(WithUserID(ctx, owner.ID), &tg.ChannelsUpdateUsernameRequest{Channel: input, Username: ""}); err != nil || !ok {
t.Fatalf("clear username = ok %v err %v, want true", ok, err)
}
after, err := r.onChannelsGetChannels(WithUserID(ctx, owner.ID), []tg.InputChannelClass{input})
if err != nil {
t.Fatalf("get channel after clear: %v", err)
}
if got := after.(*tg.MessagesChats).Chats[0].(*tg.Channel); got.Username != "" || len(got.Usernames) != 0 {
t.Fatalf("channel after clear = %+v, want username cleared", got)
}
}
func TestChannelsGetChannelsRejectsHugeVector(t *testing.T) {
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zaptest.NewLogger(t), clock.System)
ids := make([]tg.InputChannelClass, maxGetMessagesIDs+1)
for i := range ids {
ids[i] = &tg.InputChannel{ChannelID: int64(i + 1)}
}
if _, err := r.onChannelsGetChannels(context.Background(), ids); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("channels.getChannels huge vector err = %v, want LIMIT_INVALID", err)
}
}
func paidMessagesPriceServicePeers(t *testing.T, updates tg.UpdatesClass) map[int64]*tg.MessageActionPaidMessagesPrice {
t.Helper()
box, ok := updates.(*tg.Updates)
if !ok {
t.Fatalf("updates = %T, want *tg.Updates", updates)
}
out := make(map[int64]*tg.MessageActionPaidMessagesPrice)
for _, update := range box.Updates {
newMsg, ok := update.(*tg.UpdateNewChannelMessage)
if !ok {
continue
}
service, ok := newMsg.Message.(*tg.MessageService)
if !ok {
continue
}
peer, ok := service.PeerID.(*tg.PeerChannel)
if !ok {
continue
}
action, ok := service.Action.(*tg.MessageActionPaidMessagesPrice)
if !ok {
continue
}
out[peer.ChannelID] = action
}
return out
}
// monoforumCreationServicePeers 收集 updates 里 messageActionChannelCreate 服务消息的 peer 集合。
// monoforum 首次开启时收到一条创建消息(TDesktop 渲染 "Direct messages were enabled in this
// channel."),这是它在直连会话里唯一的服务消息;开关/价格变更不再往 mono 追加服务消息。
func monoforumCreationServicePeers(t *testing.T, updates tg.UpdatesClass) map[int64]struct{} {
t.Helper()
box, ok := updates.(*tg.Updates)
if !ok {
t.Fatalf("updates = %T, want *tg.Updates", updates)
}
out := make(map[int64]struct{})
for _, update := range box.Updates {
newMsg, ok := update.(*tg.UpdateNewChannelMessage)
if !ok {
continue
}
service, ok := newMsg.Message.(*tg.MessageService)
if !ok {
continue
}
peer, ok := service.PeerID.(*tg.PeerChannel)
if !ok {
continue
}
if _, ok := service.Action.(*tg.MessageActionChannelCreate); !ok {
continue
}
out[peer.ChannelID] = struct{}{}
}
return out
}