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 表示清除)。
|
// 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 {
|
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.
|
// 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) {
|
func TestGroupBotPolicies(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
store := memory.NewChannelStore()
|
store := memory.NewChannelStore()
|
||||||
|
|
|
||||||
|
|
@ -509,11 +509,13 @@ type ChannelDialog struct {
|
||||||
type ChannelMessageActionType string
|
type ChannelMessageActionType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ChannelActionNone ChannelMessageActionType = ""
|
ChannelActionNone ChannelMessageActionType = ""
|
||||||
ChannelActionCreate ChannelMessageActionType = "channel_create"
|
ChannelActionCreate ChannelMessageActionType = "channel_create"
|
||||||
ChannelActionChatAddUser ChannelMessageActionType = "chat_add_user"
|
ChannelActionChatAddUser ChannelMessageActionType = "chat_add_user"
|
||||||
ChannelActionChatDelete ChannelMessageActionType = "chat_delete_user"
|
ChannelActionChatDelete ChannelMessageActionType = "chat_delete_user"
|
||||||
ChannelActionChatJoined ChannelMessageActionType = "chat_joined"
|
ChannelActionChatEditPhoto ChannelMessageActionType = "chat_edit_photo"
|
||||||
|
ChannelActionChatDeletePhoto ChannelMessageActionType = "chat_delete_photo"
|
||||||
|
ChannelActionChatJoined ChannelMessageActionType = "chat_joined"
|
||||||
// ChannelActionChatJoinedByLink 是经邀请链接加入的服务消息,
|
// ChannelActionChatJoinedByLink 是经邀请链接加入的服务消息,
|
||||||
// 渲染为 "X joined the group via invite link"。
|
// 渲染为 "X joined the group via invite link"。
|
||||||
ChannelActionChatJoinedByLink ChannelMessageActionType = "chat_joined_by_link"
|
ChannelActionChatJoinedByLink ChannelMessageActionType = "chat_joined_by_link"
|
||||||
|
|
@ -572,6 +574,8 @@ type ChannelMessageAction struct {
|
||||||
StarGift *MessageStarGiftAction
|
StarGift *MessageStarGiftAction
|
||||||
// Wallpaper 仅 set_chat_wallpaper 服务消息使用。
|
// Wallpaper 仅 set_chat_wallpaper 服务消息使用。
|
||||||
Wallpaper *Wallpaper
|
Wallpaper *Wallpaper
|
||||||
|
// Photo 仅 chat_edit_photo 服务消息使用。
|
||||||
|
Photo *Photo
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChannelMessage is a single stored message in a channel/supergroup.
|
// ChannelMessage is a single stored message in a channel/supergroup.
|
||||||
|
|
@ -1341,6 +1345,16 @@ type UpdateChannelUsernameRequest struct {
|
||||||
Username string
|
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.
|
// DeleteChannelResult describes a deleted channel.
|
||||||
type DeleteChannelResult struct {
|
type DeleteChannelResult struct {
|
||||||
Channel Channel
|
Channel Channel
|
||||||
|
|
|
||||||
|
|
@ -349,6 +349,21 @@ type Photo struct {
|
||||||
Sizes []PhotoSize `json:"sizes,omitempty"`
|
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 枚举消息可挂载的媒体载荷。
|
// MessageMediaKind 枚举消息可挂载的媒体载荷。
|
||||||
type MessageMediaKind string
|
type MessageMediaKind string
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1206,6 +1206,10 @@ func TestTDesktopPassiveChannelStubs(t *testing.T) {
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("messages.addChatUser legacy wrapper: %v", err)
|
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{
|
if _, err := r.onMessagesEditChatPhoto(ownerCtx, &tg.MessagesEditChatPhotoRequest{
|
||||||
ChatID: channel.ID,
|
ChatID: channel.ID,
|
||||||
Photo: &tg.InputChatPhotoEmpty{},
|
Photo: &tg.InputChatPhotoEmpty{},
|
||||||
|
|
|
||||||
|
|
@ -372,11 +372,24 @@ func (r *Router) onChannelsEditPhoto(ctx context.Context, req *tg.ChannelsEditPh
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, channelAdminErr(err)
|
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 {
|
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 {
|
func validChannelTitle(title string) bool {
|
||||||
n := utf8.RuneCountInString(title)
|
n := utf8.RuneCountInString(title)
|
||||||
return n > 0 && n <= maxChannelTitleLength
|
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) {
|
func TestMessagesGetPeerSettingsUsesResolveChannelForAccessCheck(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userStore := memory.NewUserStore()
|
userStore := memory.NewUserStore()
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,17 @@ func tgChannelMessageAction(action domain.ChannelMessageAction) tg.MessageAction
|
||||||
userID = action.UserIDs[0]
|
userID = action.UserIDs[0]
|
||||||
}
|
}
|
||||||
return &tg.MessageActionChatDeleteUser{UserID: userID}
|
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:
|
case domain.ChannelActionEditTitle:
|
||||||
return &tg.MessageActionChatEditTitle{Title: action.Title}
|
return &tg.MessageActionChatEditTitle{Title: action.Title}
|
||||||
case domain.ChannelActionTopicCreate:
|
case domain.ChannelActionTopicCreate:
|
||||||
|
|
|
||||||
|
|
@ -485,7 +485,7 @@ type ChannelsService interface {
|
||||||
ResolvePublicUsername(ctx context.Context, userID int64, username string) (domain.Channel, bool, error)
|
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)
|
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)
|
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)
|
SetPreHistoryHidden(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error)
|
||||||
SetParticipantsHidden(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)
|
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)
|
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)
|
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)
|
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)
|
SetPreHistoryHidden(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error)
|
||||||
SetParticipantsHidden(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)
|
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.StarGift = &g
|
||||||
}
|
}
|
||||||
out.Wallpaper = domain.CloneWallpaperPtr(in.Wallpaper)
|
out.Wallpaper = domain.CloneWallpaperPtr(in.Wallpaper)
|
||||||
|
out.Photo = domain.ClonePhotoPtr(in.Photo)
|
||||||
return &out
|
return &out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -219,31 +219,56 @@ func (s *ChannelStore) ResolvePublicChannelUsername(_ context.Context, viewerUse
|
||||||
return domain.Channel{}, false, nil
|
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 {
|
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()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
channel, err := s.channelForMemberLocked(userID, channelID)
|
channel, err := s.channelForMemberLocked(userID, channelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Channel{}, err
|
return domain.SetChannelPhotoResult{}, err
|
||||||
}
|
}
|
||||||
member := s.members[channelID][userID]
|
member := s.members[channelID][userID]
|
||||||
if !canChangeChannelInfo(member) {
|
if !canChangeChannelInfo(member) {
|
||||||
return domain.Channel{}, domain.ErrChannelAdminRequired
|
return domain.SetChannelPhotoResult{}, domain.ErrChannelAdminRequired
|
||||||
}
|
}
|
||||||
|
var action domain.ChannelMessageAction
|
||||||
if photo != nil && photo.ID != 0 {
|
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.PhotoID = photo.ID
|
||||||
channel.PhotoDCID = photo.DCID
|
channel.PhotoDCID = photo.DCID
|
||||||
channel.PhotoStripped = domain.StrippedFromSizes(photo.Sizes)
|
channel.PhotoStripped = domain.StrippedFromSizes(photo.Sizes)
|
||||||
} else {
|
} else {
|
||||||
|
if channel.PhotoID == 0 {
|
||||||
|
return domain.SetChannelPhotoResult{}, domain.ErrChannelNotModified
|
||||||
|
}
|
||||||
|
action = domain.ChannelMessageAction{Type: domain.ChannelActionChatDeletePhoto}
|
||||||
channel.PhotoID = 0
|
channel.PhotoID = 0
|
||||||
channel.PhotoDCID = 0
|
channel.PhotoDCID = 0
|
||||||
channel.PhotoStripped = nil
|
channel.PhotoStripped = nil
|
||||||
}
|
}
|
||||||
|
msg, event := s.appendChannelServiceMessageLocked(channelID, userID, date, action)
|
||||||
|
channel.TopMessageID = msg.ID
|
||||||
|
channel.Pts = event.Pts
|
||||||
s.channels[channelID] = channel
|
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) {
|
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
|
return channel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetChannelPhoto 设置/清除频道头像(反范式列)。photo==nil 表示清除。
|
// SetChannelPhoto 设置/清除频道头像,并生成对应频道服务消息。photo==nil 表示清除。
|
||||||
func (s *ChannelStore) SetChannelPhoto(ctx context.Context, userID, channelID int64, photo *domain.Photo) (domain.Channel, error) {
|
func (s *ChannelStore) SetChannelPhoto(ctx context.Context, userID, channelID int64, photo *domain.Photo, date int) (domain.SetChannelPhotoResult, error) {
|
||||||
if userID == 0 || channelID == 0 {
|
if userID == 0 || channelID == 0 {
|
||||||
return domain.Channel{}, domain.ErrChannelInvalid
|
return domain.SetChannelPhotoResult{}, domain.ErrChannelInvalid
|
||||||
}
|
}
|
||||||
beginner, ok := s.db.(txBeginner)
|
beginner, ok := s.db.(txBeginner)
|
||||||
if !ok {
|
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)
|
tx, err := beginner.Begin(ctx)
|
||||||
if err != nil {
|
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
|
committed := false
|
||||||
defer func() {
|
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)
|
channel, member, err := s.getChannelForMember(ctx, tx, userID, channelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Channel{}, err
|
return domain.SetChannelPhotoResult{}, err
|
||||||
}
|
}
|
||||||
if !canChangeChannelInfo(member) {
|
if !canChangeChannelInfo(member) {
|
||||||
return domain.Channel{}, domain.ErrChannelAdminRequired
|
return domain.SetChannelPhotoResult{}, domain.ErrChannelAdminRequired
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
photoID int64
|
photoID int64
|
||||||
dcID int
|
dcID int
|
||||||
stripped []byte
|
stripped []byte
|
||||||
|
action domain.ChannelMessageAction
|
||||||
)
|
)
|
||||||
if photo != nil && photo.ID != 0 {
|
if photo != nil && photo.ID != 0 {
|
||||||
|
if channel.PhotoID == photo.ID {
|
||||||
|
return domain.SetChannelPhotoResult{}, domain.ErrChannelNotModified
|
||||||
|
}
|
||||||
photoID = photo.ID
|
photoID = photo.ID
|
||||||
dcID = photo.DCID
|
dcID = photo.DCID
|
||||||
stripped = domain.StrippedFromSizes(photo.Sizes)
|
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 {
|
if stripped == nil {
|
||||||
stripped = []byte{}
|
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`,
|
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 {
|
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.PhotoID = photoID
|
||||||
channel.PhotoDCID = dcID
|
channel.PhotoDCID = dcID
|
||||||
channel.PhotoStripped = stripped
|
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 {
|
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
|
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) {
|
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.StarGift = &g
|
||||||
}
|
}
|
||||||
clone.Wallpaper = domain.CloneWallpaperPtr(action.Wallpaper)
|
clone.Wallpaper = domain.CloneWallpaperPtr(action.Wallpaper)
|
||||||
|
clone.Photo = domain.ClonePhotoPtr(action.Photo)
|
||||||
return &clone
|
return &clone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue