diff --git a/internal/app/channels/service.go b/internal/app/channels/service.go index bcfef9d9..baf16920 100644 --- a/internal/app/channels/service.go +++ b/internal/app/channels/service.go @@ -516,11 +516,11 @@ func (s *Service) SetSignatures(ctx context.Context, userID, channelID int64, en } // SetPhoto 设置/清除频道头像(photo==nil 表示清除)。 -func (s *Service) SetPhoto(ctx context.Context, userID, channelID int64, photo *domain.Photo) (domain.Channel, error) { +func (s *Service) SetPhoto(ctx context.Context, userID, channelID int64, photo *domain.Photo, date int) (domain.SetChannelPhotoResult, error) { if s == nil || s.channels == nil || userID == 0 || channelID == 0 { - return domain.Channel{}, domain.ErrChannelInvalid + return domain.SetChannelPhotoResult{}, domain.ErrChannelInvalid } - return s.channels.SetChannelPhoto(ctx, userID, channelID, photo) + return s.channels.SetChannelPhoto(ctx, userID, channelID, photo, date) } // SetPreHistoryHidden toggles hidden history for new supergroup members. diff --git a/internal/app/channels/service_test.go b/internal/app/channels/service_test.go index b907f70d..265caf9e 100644 --- a/internal/app/channels/service_test.go +++ b/internal/app/channels/service_test.go @@ -768,6 +768,83 @@ func TestCreateChatCreatesMegagroupWithChannelPts(t *testing.T) { } } +func TestSetChannelPhotoCreatesServiceMessagesAndDifference(t *testing.T) { + ctx := context.Background() + service := NewService(memory.NewChannelStore()) + + created, err := service.CreateMegagroupFromCreateChat(ctx, 1001, domain.CreateChannelRequest{ + Title: "Avatar Team", + MemberUserIDs: []int64{1002}, + Date: 10, + }) + if err != nil { + t.Fatalf("CreateMegagroupFromCreateChat: %v", err) + } + photo := domain.Photo{ + ID: 9001, + AccessHash: 9002, + DCID: 2, + Sizes: []domain.PhotoSize{ + {Kind: domain.PhotoSizeKindStripped, Type: "i", Bytes: []byte{1, 2, 3}}, + {Kind: domain.PhotoSizeKindDefault, Type: "m", W: 160, H: 160, Size: 4096}, + }, + } + + set, err := service.SetPhoto(ctx, 1001, created.Channel.ID, &photo, 11) + if err != nil { + t.Fatalf("SetPhoto: %v", err) + } + if set.Channel.PhotoID != photo.ID || set.Channel.PhotoDCID != photo.DCID || !slices.Equal(set.Channel.PhotoStripped, []byte{1, 2, 3}) { + t.Fatalf("set channel photo fields = %+v, want photo id/dc/stripped", set.Channel) + } + if set.Channel.TopMessageID != set.Message.ID || set.Channel.Pts != set.Event.Pts { + t.Fatalf("set top/pts = channel %+v message %+v event %+v, want service message as top", set.Channel, set.Message, set.Event) + } + if set.Event.Type != domain.ChannelUpdateNewMessage || set.Event.Pts != created.Event.Pts+1 || set.Event.PtsCount != 1 { + t.Fatalf("set event = %+v, want durable new-message pts", set.Event) + } + if set.Message.Action == nil || set.Message.Action.Type != domain.ChannelActionChatEditPhoto || set.Message.Action.Photo == nil || set.Message.Action.Photo.ID != photo.ID { + t.Fatalf("set action = %+v, want chat_edit_photo with photo snapshot", set.Message.Action) + } + if _, err := service.SetPhoto(ctx, 1001, created.Channel.ID, &photo, 12); !errors.Is(err, domain.ErrChannelNotModified) { + t.Fatalf("duplicate SetPhoto err = %v, want ErrChannelNotModified", err) + } + diff, err := service.GetDifference(ctx, 1002, domain.ChannelDifferenceRequest{ChannelID: created.Channel.ID, Pts: created.Event.Pts, Limit: 10}) + if err != nil { + t.Fatalf("GetDifference set photo: %v", err) + } + if !diff.Final || diff.Pts != set.Event.Pts || len(diff.NewMessages) != 1 { + t.Fatalf("diff after set = %+v, want one service message through pts %d", diff, set.Event.Pts) + } + if action := diff.NewMessages[0].Action; action == nil || action.Type != domain.ChannelActionChatEditPhoto || action.Photo == nil || action.Photo.ID != photo.ID { + t.Fatalf("diff set action = %+v, want chat_edit_photo", action) + } + + cleared, err := service.SetPhoto(ctx, 1001, created.Channel.ID, nil, 13) + if err != nil { + t.Fatalf("ClearPhoto: %v", err) + } + if cleared.Channel.PhotoID != 0 || cleared.Channel.PhotoDCID != 0 || len(cleared.Channel.PhotoStripped) != 0 { + t.Fatalf("cleared channel photo fields = %+v, want empty photo", cleared.Channel) + } + if cleared.Event.Pts != set.Event.Pts+1 || cleared.Message.Action == nil || cleared.Message.Action.Type != domain.ChannelActionChatDeletePhoto { + t.Fatalf("cleared = event %+v action %+v, want chat_delete_photo next pts", cleared.Event, cleared.Message.Action) + } + clearDiff, err := service.GetDifference(ctx, 1002, domain.ChannelDifferenceRequest{ChannelID: created.Channel.ID, Pts: set.Event.Pts, Limit: 10}) + if err != nil { + t.Fatalf("GetDifference clear photo: %v", err) + } + if !clearDiff.Final || clearDiff.Pts != cleared.Event.Pts || len(clearDiff.NewMessages) != 1 { + t.Fatalf("diff after clear = %+v, want one delete-photo service message", clearDiff) + } + if action := clearDiff.NewMessages[0].Action; action == nil || action.Type != domain.ChannelActionChatDeletePhoto { + t.Fatalf("diff clear action = %+v, want chat_delete_photo", action) + } + if _, err := service.SetPhoto(ctx, 1001, created.Channel.ID, nil, 14); !errors.Is(err, domain.ErrChannelNotModified) { + t.Fatalf("duplicate ClearPhoto err = %v, want ErrChannelNotModified", err) + } +} + func TestGroupBotPolicies(t *testing.T) { ctx := context.Background() store := memory.NewChannelStore() diff --git a/internal/domain/channel.go b/internal/domain/channel.go index 7db19cd9..5ca609fe 100644 --- a/internal/domain/channel.go +++ b/internal/domain/channel.go @@ -509,11 +509,13 @@ type ChannelDialog struct { type ChannelMessageActionType string const ( - ChannelActionNone ChannelMessageActionType = "" - ChannelActionCreate ChannelMessageActionType = "channel_create" - ChannelActionChatAddUser ChannelMessageActionType = "chat_add_user" - ChannelActionChatDelete ChannelMessageActionType = "chat_delete_user" - ChannelActionChatJoined ChannelMessageActionType = "chat_joined" + ChannelActionNone ChannelMessageActionType = "" + ChannelActionCreate ChannelMessageActionType = "channel_create" + ChannelActionChatAddUser ChannelMessageActionType = "chat_add_user" + ChannelActionChatDelete ChannelMessageActionType = "chat_delete_user" + ChannelActionChatEditPhoto ChannelMessageActionType = "chat_edit_photo" + ChannelActionChatDeletePhoto ChannelMessageActionType = "chat_delete_photo" + ChannelActionChatJoined ChannelMessageActionType = "chat_joined" // ChannelActionChatJoinedByLink 是经邀请链接加入的服务消息, // 渲染为 "X joined the group via invite link"。 ChannelActionChatJoinedByLink ChannelMessageActionType = "chat_joined_by_link" @@ -572,6 +574,8 @@ type ChannelMessageAction struct { StarGift *MessageStarGiftAction // Wallpaper 仅 set_chat_wallpaper 服务消息使用。 Wallpaper *Wallpaper + // Photo 仅 chat_edit_photo 服务消息使用。 + Photo *Photo } // ChannelMessage is a single stored message in a channel/supergroup. @@ -1341,6 +1345,16 @@ type UpdateChannelUsernameRequest struct { Username string } +// SetChannelPhotoResult describes a channel avatar mutation and its durable +// service message. +type SetChannelPhotoResult struct { + Channel Channel + Message ChannelMessage + Event ChannelUpdateEvent + Recipients []int64 + Changed bool +} + // DeleteChannelResult describes a deleted channel. type DeleteChannelResult struct { Channel Channel diff --git a/internal/domain/media.go b/internal/domain/media.go index 91b7b48e..7705f401 100644 --- a/internal/domain/media.go +++ b/internal/domain/media.go @@ -349,6 +349,21 @@ type Photo struct { Sizes []PhotoSize `json:"sizes,omitempty"` } +func ClonePhotoPtr(photo *Photo) *Photo { + if photo == nil { + return nil + } + clone := *photo + clone.FileReference = append([]byte(nil), photo.FileReference...) + clone.Sizes = append([]PhotoSize(nil), photo.Sizes...) + for i := range clone.Sizes { + clone.Sizes[i].Bytes = append([]byte(nil), photo.Sizes[i].Bytes...) + clone.Sizes[i].Sizes = append([]int(nil), photo.Sizes[i].Sizes...) + clone.Sizes[i].BackgroundColors = append([]int(nil), photo.Sizes[i].BackgroundColors...) + } + return &clone +} + // MessageMediaKind 枚举消息可挂载的媒体载荷。 type MessageMediaKind string diff --git a/internal/rpc/channels_passive_stubs_rpc_test.go b/internal/rpc/channels_passive_stubs_rpc_test.go index c3739c2d..e0ebc7c9 100644 --- a/internal/rpc/channels_passive_stubs_rpc_test.go +++ b/internal/rpc/channels_passive_stubs_rpc_test.go @@ -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{}, diff --git a/internal/rpc/channels_settings.go b/internal/rpc/channels_settings.go index a4d0cf06..116692fa 100644 --- a/internal/rpc/channels_settings.go +++ b/internal/rpc/channels_settings.go @@ -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 diff --git a/internal/rpc/channels_settings_rpc_test.go b/internal/rpc/channels_settings_rpc_test.go index ad61e325..60320e42 100644 --- a/internal/rpc/channels_settings_rpc_test.go +++ b/internal/rpc/channels_settings_rpc_test.go @@ -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() diff --git a/internal/rpc/convert_channels_core.go b/internal/rpc/convert_channels_core.go index e819a59e..abe9af95 100644 --- a/internal/rpc/convert_channels_core.go +++ b/internal/rpc/convert_channels_core.go @@ -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: diff --git a/internal/rpc/deps.go b/internal/rpc/deps.go index b3406169..d8b6da71 100644 --- a/internal/rpc/deps.go +++ b/internal/rpc/deps.go @@ -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) diff --git a/internal/store/channel.go b/internal/store/channel.go index 48d205c4..f86e5262 100644 --- a/internal/store/channel.go +++ b/internal/store/channel.go @@ -40,7 +40,7 @@ type ChannelStore interface { ResolvePublicChannelUsername(ctx context.Context, viewerUserID int64, username string) (domain.Channel, bool, error) SearchPublicChannels(ctx context.Context, viewerUserID int64, query string, limit int) (domain.PublicChannelSearchResult, error) SetSignatures(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error) - SetChannelPhoto(ctx context.Context, userID, channelID int64, photo *domain.Photo) (domain.Channel, error) + SetChannelPhoto(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) diff --git a/internal/store/memory/channel_message_helpers.go b/internal/store/memory/channel_message_helpers.go index f2aab8db..2b9fb3a8 100644 --- a/internal/store/memory/channel_message_helpers.go +++ b/internal/store/memory/channel_message_helpers.go @@ -70,6 +70,7 @@ func cloneChannelMessageAction(in *domain.ChannelMessageAction) *domain.ChannelM out.StarGift = &g } out.Wallpaper = domain.CloneWallpaperPtr(in.Wallpaper) + out.Photo = domain.ClonePhotoPtr(in.Photo) return &out } diff --git a/internal/store/memory/channel_settings.go b/internal/store/memory/channel_settings.go index 8f9cd114..7e59ff3b 100644 --- a/internal/store/memory/channel_settings.go +++ b/internal/store/memory/channel_settings.go @@ -219,31 +219,56 @@ func (s *ChannelStore) ResolvePublicChannelUsername(_ context.Context, viewerUse return domain.Channel{}, false, nil } -func (s *ChannelStore) SetChannelPhoto(_ context.Context, userID, channelID int64, photo *domain.Photo) (domain.Channel, error) { +func (s *ChannelStore) SetChannelPhoto(_ context.Context, userID, channelID int64, photo *domain.Photo, date int) (domain.SetChannelPhotoResult, error) { if userID == 0 || channelID == 0 { - return domain.Channel{}, domain.ErrChannelInvalid + return domain.SetChannelPhotoResult{}, domain.ErrChannelInvalid + } + if date == 0 { + date = int(time.Now().Unix()) } s.mu.Lock() defer s.mu.Unlock() channel, err := s.channelForMemberLocked(userID, channelID) if err != nil { - return domain.Channel{}, err + return domain.SetChannelPhotoResult{}, err } member := s.members[channelID][userID] if !canChangeChannelInfo(member) { - return domain.Channel{}, domain.ErrChannelAdminRequired + return domain.SetChannelPhotoResult{}, domain.ErrChannelAdminRequired } + var action domain.ChannelMessageAction if photo != nil && photo.ID != 0 { + if channel.PhotoID == photo.ID { + return domain.SetChannelPhotoResult{}, domain.ErrChannelNotModified + } + action = domain.ChannelMessageAction{ + Type: domain.ChannelActionChatEditPhoto, + Photo: domain.ClonePhotoPtr(photo), + } channel.PhotoID = photo.ID channel.PhotoDCID = photo.DCID channel.PhotoStripped = domain.StrippedFromSizes(photo.Sizes) } else { + if channel.PhotoID == 0 { + return domain.SetChannelPhotoResult{}, domain.ErrChannelNotModified + } + action = domain.ChannelMessageAction{Type: domain.ChannelActionChatDeletePhoto} channel.PhotoID = 0 channel.PhotoDCID = 0 channel.PhotoStripped = nil } + msg, event := s.appendChannelServiceMessageLocked(channelID, userID, date, action) + channel.TopMessageID = msg.ID + channel.Pts = event.Pts s.channels[channelID] = channel - return channel, nil + s.upsertChannelDialogLocked(userID, channel, msg, true) + return domain.SetChannelPhotoResult{ + Channel: cloneChannel(channel), + Message: cloneChannelMessage(msg), + Event: cloneChannelEvent(event), + Recipients: s.activeMemberIDsLocked(channelID, 0, 0), + Changed: true, + }, nil } func (s *ChannelStore) SetSignatures(_ context.Context, userID, channelID int64, enabled bool) (domain.Channel, error) { diff --git a/internal/store/postgres/channel_settings.go b/internal/store/postgres/channel_settings.go index bd510820..fefa5823 100644 --- a/internal/store/postgres/channel_settings.go +++ b/internal/store/postgres/channel_settings.go @@ -361,18 +361,21 @@ func (s *ChannelStore) SetSignatures(ctx context.Context, userID, channelID int6 return channel, nil } -// SetChannelPhoto 设置/清除频道头像(反范式列)。photo==nil 表示清除。 -func (s *ChannelStore) SetChannelPhoto(ctx context.Context, userID, channelID int64, photo *domain.Photo) (domain.Channel, error) { +// SetChannelPhoto 设置/清除频道头像,并生成对应频道服务消息。photo==nil 表示清除。 +func (s *ChannelStore) SetChannelPhoto(ctx context.Context, userID, channelID int64, photo *domain.Photo, date int) (domain.SetChannelPhotoResult, error) { if userID == 0 || channelID == 0 { - return domain.Channel{}, domain.ErrChannelInvalid + return domain.SetChannelPhotoResult{}, domain.ErrChannelInvalid } beginner, ok := s.db.(txBeginner) if !ok { - return domain.Channel{}, fmt.Errorf("set channel photo: db does not support transactions") + return domain.SetChannelPhotoResult{}, fmt.Errorf("set channel photo: db does not support transactions") + } + if date == 0 { + date = nowUnix() } tx, err := beginner.Begin(ctx) if err != nil { - return domain.Channel{}, fmt.Errorf("begin set channel photo: %w", err) + return domain.SetChannelPhotoResult{}, fmt.Errorf("begin set channel photo: %w", err) } committed := false defer func() { @@ -382,36 +385,68 @@ func (s *ChannelStore) SetChannelPhoto(ctx context.Context, userID, channelID in }() channel, member, err := s.getChannelForMember(ctx, tx, userID, channelID) if err != nil { - return domain.Channel{}, err + return domain.SetChannelPhotoResult{}, err } if !canChangeChannelInfo(member) { - return domain.Channel{}, domain.ErrChannelAdminRequired + return domain.SetChannelPhotoResult{}, domain.ErrChannelAdminRequired } var ( photoID int64 dcID int stripped []byte + action domain.ChannelMessageAction ) if photo != nil && photo.ID != 0 { + if channel.PhotoID == photo.ID { + return domain.SetChannelPhotoResult{}, domain.ErrChannelNotModified + } photoID = photo.ID dcID = photo.DCID stripped = domain.StrippedFromSizes(photo.Sizes) + action = domain.ChannelMessageAction{ + Type: domain.ChannelActionChatEditPhoto, + Photo: domain.ClonePhotoPtr(photo), + } + } else { + if channel.PhotoID == 0 { + return domain.SetChannelPhotoResult{}, domain.ErrChannelNotModified + } + action = domain.ChannelMessageAction{Type: domain.ChannelActionChatDeletePhoto} } if stripped == nil { stripped = []byte{} } if _, err := tx.Exec(ctx, `UPDATE channels SET photo_id = $2, photo_dc_id = $3, photo_stripped = $4, updated_at = now() WHERE id = $1`, channelID, photoID, dcID, stripped); err != nil { - return domain.Channel{}, fmt.Errorf("update channel photo: %w", err) + return domain.SetChannelPhotoResult{}, fmt.Errorf("update channel photo: %w", err) + } + if s.rowCache != nil { + s.rowCache.delete(channelID) } channel.PhotoID = photoID channel.PhotoDCID = dcID channel.PhotoStripped = stripped + msg, event, err := s.insertServiceMessage(ctx, tx, channel, userID, date, action) + if err != nil { + return domain.SetChannelPhotoResult{}, err + } + channel.TopMessageID = msg.ID + channel.Pts = event.Pts + if err := upsertChannelDialogTx(ctx, tx, userID, channel, msg, msg.ID, 0); err != nil { + return domain.SetChannelPhotoResult{}, err + } if err := tx.Commit(ctx); err != nil { - return domain.Channel{}, fmt.Errorf("commit set channel photo: %w", err) + return domain.SetChannelPhotoResult{}, fmt.Errorf("commit set channel photo: %w", err) } committed = true - return channel, nil + recipients, _ := s.ListActiveChannelMemberIDs(ctx, userID, channelID, 0) + return domain.SetChannelPhotoResult{ + Channel: channel, + Message: msg, + Event: event, + Recipients: recipients, + Changed: true, + }, nil } func (s *ChannelStore) SetAutotranslation(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error) { diff --git a/internal/store/postgres/message_helpers.go b/internal/store/postgres/message_helpers.go index d500844b..538a9c81 100644 --- a/internal/store/postgres/message_helpers.go +++ b/internal/store/postgres/message_helpers.go @@ -46,6 +46,7 @@ func cloneChannelMessageAction(action *domain.ChannelMessageAction) *domain.Chan clone.StarGift = &g } clone.Wallpaper = domain.CloneWallpaperPtr(action.Wallpaper) + clone.Photo = domain.ClonePhotoPtr(action.Photo) return &clone }