fix: emit channel avatar service messages

This commit is contained in:
A 2026-07-05 15:14:41 +08:00
parent 4d9f1e271d
commit c38dd73fdd
14 changed files with 346 additions and 27 deletions

View file

@ -1206,6 +1206,10 @@ func TestTDesktopPassiveChannelStubs(t *testing.T) {
}); err != nil {
t.Fatalf("messages.addChatUser legacy wrapper: %v", err)
}
seedPhoto := domain.Photo{ID: 7701, AccessHash: 7702, DCID: 2}
if _, err := f.channels.SetChannelPhoto(f.ctx, owner.ID, channel.ID, &seedPhoto, 1700007701); err != nil {
t.Fatalf("seed channel photo before legacy clear: %v", err)
}
if _, err := r.onMessagesEditChatPhoto(ownerCtx, &tg.MessagesEditChatPhotoRequest{
ChatID: channel.ID,
Photo: &tg.InputChatPhotoEmpty{},

View file

@ -372,11 +372,24 @@ func (r *Router) onChannelsEditPhoto(ctx context.Context, req *tg.ChannelsEditPh
if err != nil {
return nil, err
}
channel, err := r.deps.Channels.SetPhoto(ctx, userID, channelID, photo)
res, err := r.deps.Channels.SetPhoto(ctx, userID, channelID, photo, int(r.clock.Now().Unix()))
if err != nil {
return nil, channelAdminErr(err)
}
return r.channelStateMutationUpdates(ctx, userID, channel), nil
r.invalidateRPCProjectionForChannel(res.Channel.ID)
updates := r.channelPhotoUpdates(ctx, userID, res)
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
return r.channelStateUpdates(viewerUserID, res.Channel)
})
if res.Event.Pts != 0 {
r.enqueueChannelMessageFanout(ctx, userID, domain.SendChannelMessageResult{
Channel: res.Channel,
Message: res.Message,
Event: res.Event,
Recipients: res.Recipients,
}, nil)
}
return updates, nil
}
func (r *Router) channelTitleUpdates(ctx context.Context, viewerUserID int64, res domain.EditChannelTitleResult) *tg.Updates {
@ -395,6 +408,22 @@ func (r *Router) channelTitleUpdates(ctx context.Context, viewerUserID int64, re
}
}
func (r *Router) channelPhotoUpdates(ctx context.Context, viewerUserID int64, res domain.SetChannelPhotoResult) *tg.Updates {
updates := []tg.UpdateClass{&tg.UpdateChannel{ChannelID: res.Channel.ID}}
if res.Event.Pts != 0 {
if update := tgChannelUpdate(viewerUserID, res.Event); update != nil {
updates = append(updates, update)
}
}
return &tg.Updates{
Updates: updates,
Users: r.tgUsersForIDs(ctx, viewerUserID, []int64{res.Message.SenderUserID}),
Chats: []tg.ChatClass{tgChannelChatMin(viewerUserID, res.Channel)},
Date: int(r.clock.Now().Unix()),
Seq: 0,
}
}
func validChannelTitle(title string) bool {
n := utf8.RuneCountInString(title)
return n > 0 && n <= maxChannelTitleLength

View file

@ -255,6 +255,113 @@ func TestMessagesGetChatsUsesBatchServiceRPC(t *testing.T) {
}
}
func TestChannelsEditPhotoReturnsServiceMessageUpdatesRPC(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, err := userStore.Create(ctx, domain.User{AccessHash: 91, Phone: "15550009101", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
member, err := userStore.Create(ctx, domain.User{AccessHash: 92, Phone: "15550009102", FirstName: "Member"})
if err != nil {
t.Fatalf("create member: %v", err)
}
channelStore := memory.NewChannelStore()
channelService := appchannels.NewService(channelStore)
created, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
Title: "Photo RPC",
Megagroup: true,
MemberUserIDs: []int64{member.ID},
Date: 1700001900,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
files := &fakeFiles{}
photo := files.putPhoto(domain.Photo{
ID: 9901,
AccessHash: 9902,
DCID: 2,
Date: 1700001901,
Sizes: []domain.PhotoSize{
{Kind: domain.PhotoSizeKindStripped, Type: "i", Bytes: []byte{4, 5, 6}},
{Kind: domain.PhotoSizeKindDefault, Type: "m", W: 160, H: 160, Size: 4096},
},
})
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: channelService,
Files: files,
}, zaptest.NewLogger(t), clock.System)
input := &tg.InputChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash}
setResult, err := r.onChannelsEditPhoto(WithUserID(ctx, owner.ID), &tg.ChannelsEditPhotoRequest{
Channel: input,
Photo: &tg.InputChatPhoto{ID: &tg.InputPhoto{ID: photo.ID, AccessHash: photo.AccessHash}},
})
if err != nil {
t.Fatalf("channels.editPhoto set: %v", err)
}
setAction := requireChannelPhotoServiceAction(t, setResult, created.Channel.ID)
editPhoto, ok := setAction.(*tg.MessageActionChatEditPhoto)
if !ok {
t.Fatalf("set action = %T, want MessageActionChatEditPhoto", setAction)
}
tgPhoto, ok := editPhoto.Photo.(*tg.Photo)
if !ok || tgPhoto.ID != photo.ID {
t.Fatalf("set action photo = %#v, want tg.Photo id %d", editPhoto.Photo, photo.ID)
}
clearResult, err := r.onChannelsEditPhoto(WithUserID(ctx, owner.ID), &tg.ChannelsEditPhotoRequest{
Channel: input,
Photo: &tg.InputChatPhotoEmpty{},
})
if err != nil {
t.Fatalf("channels.editPhoto clear: %v", err)
}
clearAction := requireChannelPhotoServiceAction(t, clearResult, created.Channel.ID)
if _, ok := clearAction.(*tg.MessageActionChatDeletePhoto); !ok {
t.Fatalf("clear action = %T, want MessageActionChatDeletePhoto", clearAction)
}
}
func requireChannelPhotoServiceAction(t *testing.T, updatesClass tg.UpdatesClass, channelID int64) tg.MessageActionClass {
t.Helper()
updates, ok := updatesClass.(*tg.Updates)
if !ok {
t.Fatalf("updates = %T, want *tg.Updates", updatesClass)
}
hasUpdateChannel := false
var action tg.MessageActionClass
for _, update := range updates.Updates {
switch item := update.(type) {
case *tg.UpdateChannel:
if item.ChannelID == channelID {
hasUpdateChannel = true
}
case *tg.UpdateNewChannelMessage:
service, ok := item.Message.(*tg.MessageService)
if !ok {
t.Fatalf("new channel message = %T, want MessageService", item.Message)
}
action = service.Action
}
}
if !hasUpdateChannel {
t.Fatalf("updates = %+v, want UpdateChannel for %d", updates.Updates, channelID)
}
if action == nil {
t.Fatalf("updates = %+v, want UpdateNewChannelMessage service action", updates.Updates)
}
if len(updates.Chats) == 0 {
t.Fatalf("updates chats empty, want channel projection")
}
if len(updates.Users) == 0 {
t.Fatalf("updates users empty, want service sender projection")
}
return action
}
func TestMessagesGetPeerSettingsUsesResolveChannelForAccessCheck(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()

View file

@ -205,6 +205,17 @@ func tgChannelMessageAction(action domain.ChannelMessageAction) tg.MessageAction
userID = action.UserIDs[0]
}
return &tg.MessageActionChatDeleteUser{UserID: userID}
case domain.ChannelActionChatEditPhoto:
if action.Photo == nil {
return nil
}
photo := tgPhoto(*action.Photo)
if _, empty := photo.(*tg.PhotoEmpty); empty {
return nil
}
return &tg.MessageActionChatEditPhoto{Photo: photo}
case domain.ChannelActionChatDeletePhoto:
return &tg.MessageActionChatDeletePhoto{}
case domain.ChannelActionEditTitle:
return &tg.MessageActionChatEditTitle{Title: action.Title}
case domain.ChannelActionTopicCreate:

View file

@ -485,7 +485,7 @@ type ChannelsService interface {
ResolvePublicUsername(ctx context.Context, userID int64, username string) (domain.Channel, bool, error)
SearchPublicChannels(ctx context.Context, userID int64, query string, limit int) (domain.PublicChannelSearchResult, error)
SetSignatures(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error)
SetPhoto(ctx context.Context, userID, channelID int64, photo *domain.Photo) (domain.Channel, error)
SetPhoto(ctx context.Context, userID, channelID int64, photo *domain.Photo, date int) (domain.SetChannelPhotoResult, error)
SetPreHistoryHidden(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error)
SetParticipantsHidden(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error)
SetForum(ctx context.Context, userID, channelID int64, enabled, tabs bool) (domain.Channel, error)