fix: emit channel avatar service messages
This commit is contained in:
parent
4d9f1e271d
commit
c38dd73fdd
14 changed files with 346 additions and 27 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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{},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue