chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
396
internal/store/memory/channel_message_edit.go
Normal file
396
internal/store/memory/channel_message_edit.go
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func (s *ChannelStore) EditChannelMessage(_ context.Context, req domain.EditChannelMessageRequest) (domain.EditChannelMessageResult, error) {
|
||||
// 空文本只在媒体替换(live location 续报/停止)时合法。
|
||||
if req.UserID == 0 || req.ChannelID == 0 || req.ID <= 0 || (strings.TrimSpace(req.Message) == "" && req.Media == nil) {
|
||||
return domain.EditChannelMessageResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
|
||||
if err != nil {
|
||||
return domain.EditChannelMessageResult{}, err
|
||||
}
|
||||
member := s.members[req.ChannelID][req.UserID]
|
||||
idx, ok := s.findMessageIndexLocked(req.ChannelID, req.ID)
|
||||
if !ok || s.messages[req.ChannelID][idx].Deleted || s.messages[req.ChannelID][idx].Action != nil {
|
||||
return domain.EditChannelMessageResult{}, domain.ErrMessageIDInvalid
|
||||
}
|
||||
prevMsg := s.messages[req.ChannelID][idx]
|
||||
msg := prevMsg
|
||||
// WebPageResolve:频道链接预览就地替换(服务端内部,幂等守卫即授权)。只换 media、
|
||||
// 不碰 body/entities/edit_date,事件为 channel_web_page。
|
||||
if req.WebPageResolve {
|
||||
if req.Media == nil || !domain.IsPendingWebPageMedia(msg.Media, req.ExpectedWebPageID) {
|
||||
return domain.EditChannelMessageResult{}, domain.ErrMessageNotModified
|
||||
}
|
||||
pts := s.nextChannelPtsLocked(req.ChannelID)
|
||||
media := *req.Media
|
||||
msg.Media = &media
|
||||
msg.Pts = pts
|
||||
s.messages[req.ChannelID][idx] = msg
|
||||
channel.Pts = pts
|
||||
s.channels[req.ChannelID] = channel
|
||||
event := domain.ChannelUpdateEvent{
|
||||
ChannelID: req.ChannelID,
|
||||
Type: domain.ChannelUpdateWebPage,
|
||||
Pts: pts,
|
||||
PtsCount: 1,
|
||||
Date: msg.Date,
|
||||
Message: cloneChannelMessage(msg),
|
||||
SenderUserID: req.UserID,
|
||||
}
|
||||
s.events[req.ChannelID] = append(s.events[req.ChannelID], event)
|
||||
return domain.EditChannelMessageResult{
|
||||
Channel: channel,
|
||||
Message: cloneChannelMessage(msg),
|
||||
Event: cloneChannelEvent(event),
|
||||
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
|
||||
}, nil
|
||||
}
|
||||
// participant todo 协作须满足与正常发消息相同的权限,禁言成员/无发帖权订阅者
|
||||
// 不得借 OthersCanComplete/Append 绕过发言限制(与 postgres 对齐)。
|
||||
participantTodoEdit := isChannelTodoParticipantEdit(req, msg) && canSendChannelMessage(channel, member)
|
||||
viaBotEditRequested := req.ViaBotEditBotID != 0
|
||||
viaBotEdit := viaBotEditRequested && msg.ViaBotID == req.ViaBotEditBotID
|
||||
if viaBotEditRequested && !viaBotEdit {
|
||||
return domain.EditChannelMessageResult{}, domain.ErrMessageAuthorRequired
|
||||
}
|
||||
if !viaBotEdit && msg.SenderUserID != req.UserID && !canEditChannelMessage(member) && !participantTodoEdit {
|
||||
return domain.EditChannelMessageResult{}, domain.ErrMessageAuthorRequired
|
||||
}
|
||||
if req.Media == nil && !req.SetReplyMarkup && msg.Body == req.Message && sameMessageEntities(msg.Entities, req.Entities) {
|
||||
return domain.EditChannelMessageResult{}, domain.ErrMessageNotModified
|
||||
}
|
||||
pts := s.nextChannelPtsLocked(req.ChannelID)
|
||||
msg.Body = req.Message
|
||||
msg.Entities = append([]domain.MessageEntity(nil), req.Entities...)
|
||||
if req.Media != nil {
|
||||
media := *req.Media
|
||||
msg.Media = &media
|
||||
}
|
||||
if req.SetReplyMarkup {
|
||||
msg.ReplyMarkup = cloneReplyMarkup(req.ReplyMarkup)
|
||||
}
|
||||
msg.EditDate = req.EditDate
|
||||
msg.Pts = pts
|
||||
s.messages[req.ChannelID][idx] = msg
|
||||
channel.Pts = pts
|
||||
s.channels[req.ChannelID] = channel
|
||||
event := domain.ChannelUpdateEvent{
|
||||
ChannelID: req.ChannelID,
|
||||
Type: domain.ChannelUpdateEditMessage,
|
||||
Pts: pts,
|
||||
PtsCount: 1,
|
||||
Date: req.EditDate,
|
||||
Message: cloneChannelMessage(msg),
|
||||
SenderUserID: req.UserID,
|
||||
}
|
||||
s.events[req.ChannelID] = append(s.events[req.ChannelID], event)
|
||||
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
|
||||
ChannelID: req.ChannelID,
|
||||
UserID: req.UserID,
|
||||
Date: req.EditDate,
|
||||
Type: domain.ChannelAdminLogEditMessage,
|
||||
PrevMessage: ptrChannelMessage(prevMsg),
|
||||
NewMessage: ptrChannelMessage(msg),
|
||||
Query: msg.Body,
|
||||
})
|
||||
var serviceMsg domain.ChannelMessage
|
||||
var serviceEvent domain.ChannelUpdateEvent
|
||||
if req.TodoServiceAction != nil {
|
||||
action := cloneChannelMessageAction(req.TodoServiceAction)
|
||||
if action == nil {
|
||||
return domain.EditChannelMessageResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
replyTo := channelTodoServiceReply(msg)
|
||||
servicePts := s.nextChannelPtsLocked(req.ChannelID)
|
||||
serviceMsg = domain.ChannelMessage{
|
||||
ChannelID: req.ChannelID,
|
||||
ID: s.nextChannelMessageIDLocked(req.ChannelID),
|
||||
SenderUserID: req.UserID,
|
||||
From: domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID},
|
||||
Date: req.EditDate,
|
||||
Post: channel.Broadcast,
|
||||
Silent: msg.Silent,
|
||||
NoForwards: msg.NoForwards || channel.NoForwards,
|
||||
ReplyTo: replyTo,
|
||||
Action: action,
|
||||
Pts: servicePts,
|
||||
}
|
||||
serviceEvent = domain.ChannelUpdateEvent{
|
||||
ChannelID: req.ChannelID,
|
||||
Type: domain.ChannelUpdateNewMessage,
|
||||
Pts: servicePts,
|
||||
PtsCount: 1,
|
||||
Date: req.EditDate,
|
||||
Message: cloneChannelMessage(serviceMsg),
|
||||
SenderUserID: req.UserID,
|
||||
}
|
||||
s.messages[req.ChannelID] = append(s.messages[req.ChannelID], serviceMsg)
|
||||
s.events[req.ChannelID] = append(s.events[req.ChannelID], serviceEvent)
|
||||
s.updateForumTopicTopMessageLocked(req.ChannelID, serviceMsg)
|
||||
channel.TopMessageID = serviceMsg.ID
|
||||
channel.Pts = servicePts
|
||||
s.channels[req.ChannelID] = channel
|
||||
for userID, member := range s.members[req.ChannelID] {
|
||||
if member.Status == domain.ChannelMemberActive {
|
||||
s.upsertChannelDialogLocked(userID, channel, serviceMsg, userID == req.UserID)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 编辑对账 @ 集合:新增者补提及,被移除的实体提及删除;reply 隐式提及保留。
|
||||
// 仅在 body/entities 实际变化时执行:media-only 编辑(geolive/todo/poll)不带
|
||||
// MentionUserIDs,无条件对账会误删 caption 里仍未读的 @ 提及(与 postgres 对齐)。
|
||||
textChanged := prevMsg.Body != req.Message || !sameMessageEntities(prevMsg.Entities, req.Entities)
|
||||
if textChanged && (!channel.Broadcast || channel.Megagroup) {
|
||||
keep := make(map[int64]struct{}, len(req.MentionUserIDs)+1)
|
||||
for _, id := range req.MentionUserIDs {
|
||||
if id != 0 {
|
||||
keep[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
if msg.ReplyTo != nil && msg.ReplyTo.MessageID > 0 {
|
||||
if target, ok := s.findMessageLocked(req.ChannelID, msg.ReplyTo.MessageID); ok && target.SenderUserID != 0 {
|
||||
keep[target.SenderUserID] = struct{}{}
|
||||
}
|
||||
}
|
||||
added := make([]int64, 0, len(req.MentionUserIDs))
|
||||
for _, id := range req.MentionUserIDs {
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := s.mentions[id][req.ChannelID][msg.ID]; !ok {
|
||||
added = append(added, id)
|
||||
}
|
||||
}
|
||||
for userID, byChannel := range s.mentions {
|
||||
if _, ok := byChannel[req.ChannelID][msg.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := keep[userID]; ok {
|
||||
continue
|
||||
}
|
||||
delete(byChannel[req.ChannelID], msg.ID)
|
||||
if dialogs := s.dialogs[userID]; dialogs != nil {
|
||||
if dialog, ok := dialogs[req.ChannelID]; ok {
|
||||
dialog.UnreadMentions = s.countChannelUnreadMentionsLocked(userID, req.ChannelID, 0)
|
||||
dialogs[req.ChannelID] = dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(added) > 0 {
|
||||
s.addChannelUnreadMentionsLocked(req.ChannelID, msg, req.UserID, added)
|
||||
for _, id := range added {
|
||||
if dialogs := s.dialogs[id]; dialogs != nil {
|
||||
if dialog, ok := dialogs[req.ChannelID]; ok {
|
||||
dialog.UnreadMentions = s.countChannelUnreadMentionsLocked(id, req.ChannelID, 0)
|
||||
dialogs[req.ChannelID] = dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return domain.EditChannelMessageResult{
|
||||
Channel: channel,
|
||||
Message: cloneChannelMessage(msg),
|
||||
Event: cloneChannelEvent(event),
|
||||
ServiceMessage: cloneChannelMessage(serviceMsg),
|
||||
ServiceEvent: cloneChannelEvent(serviceEvent),
|
||||
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isChannelTodoParticipantEdit(req domain.EditChannelMessageRequest, msg domain.ChannelMessage) bool {
|
||||
if !req.AllowTodoParticipantMutation || req.SetReplyMarkup || req.Media == nil || req.Media.Kind != domain.MessageMediaKindTodo || req.Media.Todo == nil {
|
||||
return false
|
||||
}
|
||||
if msg.Media == nil || msg.Media.Kind != domain.MessageMediaKindTodo || msg.Media.Todo == nil {
|
||||
return false
|
||||
}
|
||||
if req.TodoServiceAction == nil {
|
||||
return false
|
||||
}
|
||||
switch req.TodoServiceAction.Type {
|
||||
case domain.ChannelActionTodoCompletions:
|
||||
if !msg.Media.Todo.OthersCanComplete {
|
||||
return false
|
||||
}
|
||||
case domain.ChannelActionTodoAppendTasks:
|
||||
if !msg.Media.Todo.OthersCanAppend {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return msg.Body == req.Message && sameMessageEntities(msg.Entities, req.Entities)
|
||||
}
|
||||
|
||||
func channelTodoServiceReply(msg domain.ChannelMessage) *domain.MessageReply {
|
||||
reply := &domain.MessageReply{
|
||||
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: msg.ChannelID},
|
||||
MessageID: msg.ID,
|
||||
}
|
||||
if msg.ReplyTo != nil {
|
||||
reply.TopMessageID = msg.ReplyTo.TopMessageID
|
||||
reply.ForumTopic = msg.ReplyTo.ForumTopic
|
||||
}
|
||||
return reply
|
||||
}
|
||||
|
||||
func (s *ChannelStore) UpdatePinnedMessage(_ context.Context, req domain.UpdateChannelPinnedMessageRequest) (domain.UpdateChannelPinnedMessageResult, error) {
|
||||
if req.UserID == 0 || req.ChannelID == 0 || req.MessageID <= 0 {
|
||||
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
|
||||
if err != nil {
|
||||
return domain.UpdateChannelPinnedMessageResult{}, err
|
||||
}
|
||||
member := s.members[req.ChannelID][req.UserID]
|
||||
if !canPinChannelMessages(channel, member) {
|
||||
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelAdminRequired
|
||||
}
|
||||
msg, ok := s.findMessageLocked(req.ChannelID, req.MessageID)
|
||||
if !ok || msg.Deleted {
|
||||
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrMessageIDInvalid
|
||||
}
|
||||
// 多置顶模型:pin/unpin 只翻转目标消息自身的 pinned flag,不影响其它
|
||||
// 置顶;与官方一致不设数量上限。
|
||||
if msg.Pinned == req.Pinned {
|
||||
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelNotModified
|
||||
}
|
||||
for i := range s.messages[req.ChannelID] {
|
||||
if s.messages[req.ChannelID][i].ID == req.MessageID {
|
||||
s.messages[req.ChannelID][i].Pinned = req.Pinned
|
||||
break
|
||||
}
|
||||
}
|
||||
pts := s.nextChannelPtsLocked(req.ChannelID)
|
||||
channel.PinnedMessageID = s.latestPinnedMessageIDLocked(req.ChannelID)
|
||||
channel.Pts = pts
|
||||
s.channels[req.ChannelID] = channel
|
||||
event := domain.ChannelUpdateEvent{
|
||||
ChannelID: req.ChannelID,
|
||||
Type: domain.ChannelUpdatePinnedMessages,
|
||||
Pts: pts,
|
||||
PtsCount: 1,
|
||||
Date: req.Date,
|
||||
MessageIDs: []int{req.MessageID},
|
||||
SenderUserID: req.UserID,
|
||||
Pinned: req.Pinned,
|
||||
}
|
||||
s.events[req.ChannelID] = append(s.events[req.ChannelID], event)
|
||||
logMsg := msg
|
||||
logMsg.Pinned = req.Pinned
|
||||
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
|
||||
ChannelID: req.ChannelID,
|
||||
UserID: req.UserID,
|
||||
Date: req.Date,
|
||||
Type: domain.ChannelAdminLogUpdatePinned,
|
||||
Message: ptrChannelMessage(logMsg),
|
||||
Query: msg.Body,
|
||||
})
|
||||
return domain.UpdateChannelPinnedMessageResult{
|
||||
Channel: channel,
|
||||
Event: cloneChannelEvent(event),
|
||||
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// latestPinnedMessageIDLocked 返回最新置顶消息 id(channelFull.pinned_msg_id 缓存语义)。
|
||||
func (s *ChannelStore) latestPinnedMessageIDLocked(channelID int64) int {
|
||||
latest := 0
|
||||
for _, m := range s.messages[channelID] {
|
||||
if m.Pinned && !m.Deleted && m.ID > latest {
|
||||
latest = m.ID
|
||||
}
|
||||
}
|
||||
return latest
|
||||
}
|
||||
|
||||
// UnpinAllChannelMessages 清空全部置顶并以单条 channel 事件携带全部 id。
|
||||
func (s *ChannelStore) UnpinAllChannelMessages(_ context.Context, req domain.UnpinAllChannelMessagesRequest) (domain.UpdateChannelPinnedMessageResult, error) {
|
||||
if req.UserID == 0 || req.ChannelID == 0 {
|
||||
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
|
||||
if err != nil {
|
||||
return domain.UpdateChannelPinnedMessageResult{}, err
|
||||
}
|
||||
member := s.members[req.ChannelID][req.UserID]
|
||||
if !canPinChannelMessages(channel, member) {
|
||||
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelAdminRequired
|
||||
}
|
||||
cleared := []int(nil)
|
||||
for i := range s.messages[req.ChannelID] {
|
||||
if s.messages[req.ChannelID][i].Pinned && !s.messages[req.ChannelID][i].Deleted {
|
||||
s.messages[req.ChannelID][i].Pinned = false
|
||||
cleared = append(cleared, s.messages[req.ChannelID][i].ID)
|
||||
}
|
||||
}
|
||||
if len(cleared) == 0 {
|
||||
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelNotModified
|
||||
}
|
||||
sort.Ints(cleared)
|
||||
pts := s.nextChannelPtsLocked(req.ChannelID)
|
||||
channel.PinnedMessageID = 0
|
||||
channel.Pts = pts
|
||||
s.channels[req.ChannelID] = channel
|
||||
event := domain.ChannelUpdateEvent{
|
||||
ChannelID: req.ChannelID,
|
||||
Type: domain.ChannelUpdatePinnedMessages,
|
||||
Pts: pts,
|
||||
PtsCount: 1,
|
||||
Date: req.Date,
|
||||
MessageIDs: cleared,
|
||||
SenderUserID: req.UserID,
|
||||
Pinned: false,
|
||||
}
|
||||
s.events[req.ChannelID] = append(s.events[req.ChannelID], event)
|
||||
return domain.UpdateChannelPinnedMessageResult{
|
||||
Channel: channel,
|
||||
Event: cloneChannelEvent(event),
|
||||
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ClearDanglingPinnedMessage 把指向已删除消息的置顶值清零(unpinAll 自愈)。
|
||||
func (s *ChannelStore) ClearDanglingPinnedMessage(_ context.Context, channelID int64, messageID int) error {
|
||||
if channelID == 0 || messageID <= 0 {
|
||||
return domain.ErrChannelInvalid
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
channel, ok := s.channels[channelID]
|
||||
if !ok {
|
||||
return domain.ErrChannelInvalid
|
||||
}
|
||||
if channel.PinnedMessageID == messageID {
|
||||
channel.PinnedMessageID = 0
|
||||
s.channels[channelID] = channel
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func canPinChannelMessages(channel domain.Channel, member domain.ChannelMember) bool {
|
||||
if member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.PinMessages) {
|
||||
return true
|
||||
}
|
||||
return channel.Megagroup && !channel.DefaultBannedRights.PinMessages && !member.BannedRights.PinMessages
|
||||
}
|
||||
|
||||
func canEditChannelMessage(member domain.ChannelMember) bool {
|
||||
return member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.EditMessages)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue