fix: sync owner-only megagroup creation
This commit is contained in:
parent
de233a4c18
commit
8e00a58112
3 changed files with 205 additions and 26 deletions
|
|
@ -35,9 +35,6 @@ func (r *Router) onMessagesCreateChat(ctx context.Context, req *tg.MessagesCreat
|
||||||
zap.Int("member_ids", len(memberIDs)),
|
zap.Int("member_ids", len(memberIDs)),
|
||||||
zap.Int64s("member_user_ids", memberIDs),
|
zap.Int64s("member_user_ids", memberIDs),
|
||||||
)
|
)
|
||||||
if len(memberIDs) == 0 {
|
|
||||||
return nil, usersTooFewErr()
|
|
||||||
}
|
|
||||||
createRes, err := r.deps.Channels.CreateMegagroupFromCreateChat(ctx, userID, domain.CreateChannelRequest{
|
createRes, err := r.deps.Channels.CreateMegagroupFromCreateChat(ctx, userID, domain.CreateChannelRequest{
|
||||||
CreatorUserID: userID,
|
CreatorUserID: userID,
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
|
|
@ -63,16 +60,25 @@ func (r *Router) onMessagesCreateChat(ctx context.Context, req *tg.MessagesCreat
|
||||||
}
|
}
|
||||||
|
|
||||||
cache := newViewerPeerCache(r)
|
cache := newViewerPeerCache(r)
|
||||||
updates := r.channelOperationUpdatesWithPeerCache(ctx, userID, responseRes, cache)
|
canonicalUpdates := r.channelOperationUpdatesWithPeerCache(ctx, userID, responseRes, cache)
|
||||||
|
var inviteUpdates *tg.Updates
|
||||||
|
if inviteRes.Event.Pts != 0 {
|
||||||
|
inviteUpdates = r.channelOperationUpdatesWithPeerCache(ctx, userID, inviteRes, cache)
|
||||||
|
if inviteUpdates != nil {
|
||||||
|
canonicalUpdates.Updates = append(canonicalUpdates.Updates, inviteUpdates.Updates...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updates := canonicalUpdates
|
||||||
if createChatNeedsLegacyChat(ctx) {
|
if createChatNeedsLegacyChat(ctx) {
|
||||||
updates = r.tdesktopCreateChatUpdatesWithPeerCache(ctx, userID, responseRes, cache)
|
updates = r.tdesktopCreateChatUpdatesWithPeerCache(ctx, userID, responseRes, cache)
|
||||||
}
|
|
||||||
if inviteRes.Event.Pts != 0 {
|
|
||||||
inviteUpdates := r.channelOperationUpdatesWithPeerCache(ctx, userID, inviteRes, cache)
|
|
||||||
if inviteUpdates != nil {
|
if inviteUpdates != nil {
|
||||||
updates.Updates = append(updates.Updates, inviteUpdates.Updates...)
|
updates.Updates = append(updates.Updates, inviteUpdates.Updates...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The rpc_result reaches only the calling session. Keep the creator's other
|
||||||
|
// sessions in sync with the same canonical channel state; compatibility-only
|
||||||
|
// legacy chat projection is needed solely by the synchronous create callback.
|
||||||
|
r.pushUserUpdates(ctx, userID, canonicalUpdates)
|
||||||
if inviteRes.Event.Pts != 0 {
|
if inviteRes.Event.Pts != 0 {
|
||||||
r.pushChannelExplicitUpdates(ctx, userID, inviteRes.Channel.ID, memberIDs, func(viewerUserID int64) *tg.Updates {
|
r.pushChannelExplicitUpdates(ctx, userID, inviteRes.Channel.ID, memberIDs, func(viewerUserID int64) *tg.Updates {
|
||||||
return r.channelOperationUpdatesWithPeerCache(ctx, viewerUserID, inviteRes, cache)
|
return r.channelOperationUpdatesWithPeerCache(ctx, viewerUserID, inviteRes, cache)
|
||||||
|
|
|
||||||
|
|
@ -243,8 +243,6 @@ func authTokenExceptionErr() error { return tgerr.New(400, "AUTH_TOKEN_EXCEPTION
|
||||||
|
|
||||||
func userIDInvalidErr() error { return tgerr.New(400, "USER_ID_INVALID") }
|
func userIDInvalidErr() error { return tgerr.New(400, "USER_ID_INVALID") }
|
||||||
|
|
||||||
func usersTooFewErr() error { return tgerr.New(400, "USERS_TOO_FEW") }
|
|
||||||
|
|
||||||
func firstNameInvalidErr() error { return tgerr.New(400, "FIRSTNAME_INVALID") }
|
func firstNameInvalidErr() error { return tgerr.New(400, "FIRSTNAME_INVALID") }
|
||||||
|
|
||||||
func aboutTooLongErr() error { return tgerr.New(400, "ABOUT_TOO_LONG") }
|
func aboutTooLongErr() error { return tgerr.New(400, "ABOUT_TOO_LONG") }
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"github.com/iamxvbaba/td/clock"
|
"github.com/iamxvbaba/td/clock"
|
||||||
"github.com/iamxvbaba/td/tg"
|
"github.com/iamxvbaba/td/tg"
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
"strings"
|
|
||||||
appchannels "telesrv/internal/app/channels"
|
appchannels "telesrv/internal/app/channels"
|
||||||
appdialogs "telesrv/internal/app/dialogs"
|
appdialogs "telesrv/internal/app/dialogs"
|
||||||
appusers "telesrv/internal/app/users"
|
appusers "telesrv/internal/app/users"
|
||||||
|
|
@ -120,22 +119,198 @@ func TestMessagesCreateChatCreatesMegagroupAndDialogsRPC(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMessagesCreateChatRejectsEmptyInviteListRPC(t *testing.T) {
|
func TestMessagesCreateChatCreatesOwnerOnlyMegagroupRPC(t *testing.T) {
|
||||||
ctx := context.Background()
|
tests := []struct {
|
||||||
userStore := memory.NewUserStore()
|
name string
|
||||||
owner, err := userStore.Create(ctx, domain.User{AccessHash: 21, Phone: "15550001021", FirstName: "Owner"})
|
phone string
|
||||||
if err != nil {
|
users func(domain.User) []tg.InputUserClass
|
||||||
t.Fatalf("create owner: %v", err)
|
}{
|
||||||
|
{
|
||||||
|
name: "empty vector",
|
||||||
|
phone: "15550001021",
|
||||||
|
users: func(domain.User) []tg.InputUserClass { return nil },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "self references normalize to empty",
|
||||||
|
phone: "15550001022",
|
||||||
|
users: func(owner domain.User) []tg.InputUserClass {
|
||||||
|
return []tg.InputUserClass{
|
||||||
|
&tg.InputUserSelf{},
|
||||||
|
&tg.InputUser{UserID: owner.ID, AccessHash: owner.AccessHash},
|
||||||
|
&tg.InputUserSelf{},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
r := New(Config{}, Deps{
|
|
||||||
Users: appusers.NewService(userStore),
|
|
||||||
Channels: appchannels.NewService(memory.NewChannelStore()),
|
|
||||||
}, zaptest.NewLogger(t), clock.System)
|
|
||||||
|
|
||||||
if _, err := r.onMessagesCreateChat(WithUserID(ctx, owner.ID), &tg.MessagesCreateChatRequest{
|
for index, tc := range tests {
|
||||||
Title: "No Invitees",
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
}); err == nil || !strings.Contains(err.Error(), "USERS_TOO_FEW") {
|
ctx := context.Background()
|
||||||
t.Fatalf("create chat without users err = %v, want USERS_TOO_FEW", err)
|
userStore := memory.NewUserStore()
|
||||||
|
owner, err := userStore.Create(ctx, domain.User{AccessHash: int64(21 + index), Phone: tc.phone, FirstName: "Owner"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create owner: %v", err)
|
||||||
|
}
|
||||||
|
channelStore := memory.NewChannelStore()
|
||||||
|
channels := appchannels.NewService(channelStore)
|
||||||
|
sessions := &captureScopedSessions{captureSessions: &captureSessions{}}
|
||||||
|
r := New(Config{}, Deps{
|
||||||
|
Users: appusers.NewService(userStore),
|
||||||
|
Channels: channels,
|
||||||
|
Dialogs: appdialogs.NewService(memory.NewDialogStore(), channelStore),
|
||||||
|
Sessions: sessions,
|
||||||
|
}, zaptest.NewLogger(t), clock.System)
|
||||||
|
|
||||||
|
authKeyID := [8]byte{0x60, byte(index + 1)}
|
||||||
|
sessionID := int64(70 + index)
|
||||||
|
requestCtx := WithClientInfo(
|
||||||
|
WithSessionID(WithAuthKeyID(WithUserID(ctx, owner.ID), authKeyID), sessionID),
|
||||||
|
ClientInfo{DeviceModel: "Android", AppVersion: "12.7.3"},
|
||||||
|
)
|
||||||
|
invited, err := r.onMessagesCreateChat(requestCtx, &tg.MessagesCreateChatRequest{
|
||||||
|
Users: tc.users(owner),
|
||||||
|
Title: "Owner Only Group",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create owner-only chat: %v", err)
|
||||||
|
}
|
||||||
|
if len(invited.MissingInvitees) != 0 {
|
||||||
|
t.Fatalf("missing invitees = %+v, want empty", invited.MissingInvitees)
|
||||||
|
}
|
||||||
|
|
||||||
|
updates, ok := invited.Updates.(*tg.Updates)
|
||||||
|
if !ok || len(updates.Chats) != 2 {
|
||||||
|
t.Fatalf("updates = %T %+v, want legacy chat + channel", invited.Updates, invited.Updates)
|
||||||
|
}
|
||||||
|
legacy, ok := updates.Chats[0].(*tg.Chat)
|
||||||
|
if !ok || !legacy.Deactivated || !legacy.Creator || legacy.ParticipantsCount != 1 {
|
||||||
|
t.Fatalf("legacy chat = %#v, want migrated creator-only chat", updates.Chats[0])
|
||||||
|
}
|
||||||
|
channel, ok := updates.Chats[1].(*tg.Channel)
|
||||||
|
if !ok || !channel.Megagroup || channel.Broadcast || !channel.Creator || channel.ParticipantsCount != 1 {
|
||||||
|
t.Fatalf("channel = %#v, want owner-only megagroup", updates.Chats[1])
|
||||||
|
}
|
||||||
|
migrated, ok := legacy.GetMigratedTo()
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("legacy chat missing migrated_to")
|
||||||
|
}
|
||||||
|
migratedChannel, ok := migrated.(*tg.InputChannel)
|
||||||
|
if !ok || migratedChannel.ChannelID != channel.ID || migratedChannel.AccessHash != channel.AccessHash {
|
||||||
|
t.Fatalf("migrated_to = %#v, want channel %d/%d", migrated, channel.ID, channel.AccessHash)
|
||||||
|
}
|
||||||
|
if len(updates.Updates) != 2 {
|
||||||
|
t.Fatalf("updates len = %d, want create service message + channel refresh only", len(updates.Updates))
|
||||||
|
}
|
||||||
|
created, ok := updates.Updates[0].(*tg.UpdateNewChannelMessage)
|
||||||
|
if !ok || created.Pts != 1 || created.PtsCount != 1 {
|
||||||
|
t.Fatalf("create update = %#v, want pts=1/count=1", updates.Updates[0])
|
||||||
|
}
|
||||||
|
createdMessage, ok := created.Message.(*tg.MessageService)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("create message = %T, want messageService", created.Message)
|
||||||
|
}
|
||||||
|
if _, ok := createdMessage.Action.(*tg.MessageActionChannelCreate); !ok {
|
||||||
|
t.Fatalf("create action = %T, want messageActionChannelCreate", createdMessage.Action)
|
||||||
|
}
|
||||||
|
if refresh, ok := updates.Updates[1].(*tg.UpdateChannel); !ok || refresh.ChannelID != channel.ID {
|
||||||
|
t.Fatalf("refresh = %#v, want channel %d", updates.Updates[1], channel.ID)
|
||||||
|
}
|
||||||
|
if len(updates.Users) != 1 {
|
||||||
|
t.Fatalf("updates users len = %d, want creator only", len(updates.Users))
|
||||||
|
}
|
||||||
|
if user, ok := updates.Users[0].(*tg.User); !ok || user.ID != owner.ID {
|
||||||
|
t.Fatalf("updates user = %#v, want owner %d", updates.Users[0], owner.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
pushedUserIDs := sessions.pushedUserIDs()
|
||||||
|
if len(pushedUserIDs) != 1 || pushedUserIDs[0] != owner.ID {
|
||||||
|
t.Fatalf("push user ids = %v, want creator's other sessions", pushedUserIDs)
|
||||||
|
}
|
||||||
|
push := sessions.snapshot()
|
||||||
|
if push.sessionID != sessionID || sessions.scopedAuthKey() != authKeyID {
|
||||||
|
t.Fatalf("push exclusion = auth_key %x session %d, want %x/%d", sessions.scopedAuthKey(), push.sessionID, authKeyID, sessionID)
|
||||||
|
}
|
||||||
|
canonicalPush, ok := sessions.userMessage.(*tg.Updates)
|
||||||
|
if !ok || len(canonicalPush.Chats) != 1 {
|
||||||
|
t.Fatalf("creator push = %T %+v, want canonical channel updates", sessions.userMessage, sessions.userMessage)
|
||||||
|
}
|
||||||
|
if pushedChannel, ok := canonicalPush.Chats[0].(*tg.Channel); !ok || pushedChannel.ID != channel.ID {
|
||||||
|
t.Fatalf("creator pushed chat = %#v, want channel %d", canonicalPush.Chats[0], channel.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
participants, err := r.onChannelsGetParticipants(requestCtx, &tg.ChannelsGetParticipantsRequest{
|
||||||
|
Channel: &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
||||||
|
Filter: &tg.ChannelParticipantsRecent{},
|
||||||
|
Limit: 10,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get participants: %v", err)
|
||||||
|
}
|
||||||
|
participantList, ok := participants.(*tg.ChannelsChannelParticipants)
|
||||||
|
if !ok || participantList.Count != 1 || len(participantList.Participants) != 1 || len(participantList.Users) != 1 {
|
||||||
|
t.Fatalf("participants = %T %+v, want creator only", participants, participants)
|
||||||
|
}
|
||||||
|
if creator, ok := participantList.Participants[0].(*tg.ChannelParticipantCreator); !ok || creator.UserID != owner.ID {
|
||||||
|
t.Fatalf("participant = %#v, want creator %d", participantList.Participants[0], owner.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
view, err := channels.GetChannel(ctx, owner.ID, channel.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get created channel: %v", err)
|
||||||
|
}
|
||||||
|
if view.Self.Role != domain.ChannelRoleCreator || view.Self.Status != domain.ChannelMemberActive {
|
||||||
|
t.Fatalf("self membership = %+v, want active creator", view.Self)
|
||||||
|
}
|
||||||
|
if view.Dialog.TopMessageID != createdMessage.ID || view.Dialog.ReadInboxMaxID != createdMessage.ID || view.Dialog.UnreadCount != 0 {
|
||||||
|
t.Fatalf("creator dialog = %+v, want creation message %d read", view.Dialog, createdMessage.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialogsBuffer bin.Buffer
|
||||||
|
if err := (&tg.MessagesGetDialogsRequest{OffsetPeer: &tg.InputPeerEmpty{}, Limit: 20}).Encode(&dialogsBuffer); err != nil {
|
||||||
|
t.Fatalf("encode getDialogs: %v", err)
|
||||||
|
}
|
||||||
|
dialogsResult, err := r.Dispatch(requestCtx, authKeyID, sessionID, &dialogsBuffer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dispatch getDialogs: %v", err)
|
||||||
|
}
|
||||||
|
dialogs, ok := dialogsResult.(*tg.MessagesDialogs)
|
||||||
|
if !ok || len(dialogs.Dialogs) != 1 || len(dialogs.Chats) != 1 || len(dialogs.Messages) != 1 {
|
||||||
|
t.Fatalf("dialogs = %T %+v, want persisted owner-only group", dialogsResult, dialogsResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
var historyBuffer bin.Buffer
|
||||||
|
if err := (&tg.MessagesGetHistoryRequest{
|
||||||
|
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
||||||
|
Limit: 20,
|
||||||
|
}).Encode(&historyBuffer); err != nil {
|
||||||
|
t.Fatalf("encode getHistory: %v", err)
|
||||||
|
}
|
||||||
|
historyResult, err := r.Dispatch(requestCtx, authKeyID, sessionID, &historyBuffer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dispatch getHistory: %v", err)
|
||||||
|
}
|
||||||
|
history, ok := historyResult.(*tg.MessagesChannelMessages)
|
||||||
|
if !ok || len(history.Messages) != 1 {
|
||||||
|
t.Fatalf("history = %T %+v, want creation service message", historyResult, historyResult)
|
||||||
|
}
|
||||||
|
if message, ok := history.Messages[0].(*tg.MessageService); !ok || message.ID != createdMessage.ID {
|
||||||
|
t.Fatalf("history message = %#v, want creation service %d", history.Messages[0], createdMessage.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
difference, err := r.onUpdatesGetChannelDifference(requestCtx, &tg.UpdatesGetChannelDifferenceRequest{
|
||||||
|
Channel: &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
||||||
|
Filter: &tg.ChannelMessagesFilterEmpty{},
|
||||||
|
Pts: 0,
|
||||||
|
Limit: 10,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getChannelDifference from pts=0: %v", err)
|
||||||
|
}
|
||||||
|
fullDifference, ok := difference.(*tg.UpdatesChannelDifference)
|
||||||
|
if !ok || fullDifference.Pts != 1 || len(fullDifference.NewMessages) != 1 {
|
||||||
|
t.Fatalf("difference = %T %+v, want creation event at pts=1", difference, difference)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -346,8 +521,8 @@ func TestMessagesCreateChatDispatchRemembersTDesktopClientInfo(t *testing.T) {
|
||||||
sessions.mu.Lock()
|
sessions.mu.Lock()
|
||||||
pushUserIDs := append([]int64(nil), sessions.pushUserIDs...)
|
pushUserIDs := append([]int64(nil), sessions.pushUserIDs...)
|
||||||
sessions.mu.Unlock()
|
sessions.mu.Unlock()
|
||||||
if len(pushUserIDs) != 1 || pushUserIDs[0] != friend.ID {
|
if len(pushUserIDs) != 2 || pushUserIDs[0] != owner.ID || pushUserIDs[1] != friend.ID {
|
||||||
t.Fatalf("push user ids = %v, want only invited friend %d", pushUserIDs, friend.ID)
|
t.Fatalf("push user ids = %v, want creator then invited friend %d/%d", pushUserIDs, owner.ID, friend.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue