diff --git a/internal/rpc/group/notification.go b/internal/rpc/group/notification.go index 3b1a24688..71daa3a16 100644 --- a/internal/rpc/group/notification.go +++ b/internal/rpc/group/notification.go @@ -852,8 +852,8 @@ func (g *NotificationSender) GroupMemberSetToAdminNotification(ctx context.Conte g.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMemberSetToAdminNotification, tips) } -// GroupMessagePinnedNotification 通知群成员有消息被置顶或取消置顶 -// pinType: 1=置顶, 2=取消置顶 +// GroupMessagePinnedNotification 按成员分别下发置顶通知:每人收到的 pinnedList 按其群会话 minSeq/maxSeq 过滤。 +// pinType: 1=置顶,2=取消置顶 func (g *NotificationSender) GroupMessagePinnedNotification(ctx context.Context, groupID string, pinType int32, pinned *sdkws.GroupPinnedMsgInfo, pinnedList []*sdkws.GroupPinnedMsgInfo) { var err error @@ -866,16 +866,38 @@ func (g *NotificationSender) GroupMessagePinnedNotification(ctx context.Context, if err != nil { return } - tips := &sdkws.GroupMessagePinnedTips{ - Group: groupInfo, - Type: pinType, - PinnedMsg: pinned, - PinnedList: pinnedList, + memberIDs, err := g.db.FindGroupMemberUserID(ctx, groupID) + if err != nil { + return } - if err = g.fillOpUser(ctx, &tips.OpUser, groupID); err != nil { + var opUser *sdkws.GroupMemberFullInfo + if err = g.fillOpUser(ctx, &opUser, groupID); err != nil { return } - g.Notification(ctx, mcontext.GetOpUserID(ctx), groupID, constant.GroupMessagePinnedNotification, tips) + sendID := mcontext.GetOpUserID(ctx) + conversationID := msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, groupID) + for _, memberID := range memberIDs { + minSeq, maxSeq := int64(0), int64(0) + conv, convErr := g.conversationClient.GetConversation(ctx, conversationID, memberID) + if convErr != nil { + if errs.ErrRecordNotFound.Is(convErr) { + continue + } + log.ZWarn(ctx, "GroupMessagePinnedNotification GetConversation failed", convErr, + "groupID", groupID, "userID", memberID) + continue + } + minSeq, maxSeq = conv.MinSeq, conv.MaxSeq + tips := &sdkws.GroupMessagePinnedTips{ + Group: groupInfo, + OpUser: opUser, + Type: pinType, + PinnedMsg: pinnedMsgPBVisibleToUser(pinned, minSeq, maxSeq), + PinnedList: filterPinnedListPB(pinnedList, minSeq, maxSeq), + } + g.NotificationWithSessionType(ctx, sendID, memberID, constant.GroupMessagePinnedNotification, + constant.SingleChatType, tips, notification.WithGroupID(groupID)) + } } func (g *NotificationSender) GroupMemberSetToOrdinaryUserNotification(ctx context.Context, groupID, groupMemberUserID string) { diff --git a/internal/rpc/group/pinned_msg.go b/internal/rpc/group/pinned_msg.go index d28977111..4d9ab6785 100644 --- a/internal/rpc/group/pinned_msg.go +++ b/internal/rpc/group/pinned_msg.go @@ -22,7 +22,8 @@ import ( // 群置顶消息相关 RPC 实现: // - 自动滚动保留最近 N 条置顶消息(N=model.GroupPinnedMsgMaxKeep,默认为 3) -// - 置顶时把整条消息内容做完整快照存档,避免后续消息删除/撤回影响展示 +// - 置顶时记录消息 seq,并按 seq 做完整内容快照存档 +// - 返回置顶列表与群通知均按各用户会话 minSeq/maxSeq 过滤可见置顶 // - 每条置顶记录拥有唯一 pinID,作为 unpin 时的精准删除凭据 // - 权限:默认全员可置顶;当 group.AllowPinMsg=1 时,仅群主/管理员可置顶或取消置顶 @@ -75,13 +76,17 @@ func (s *groupServer) PinGroupMessage(ctx context.Context, req *pbgroup.PinGroup } pbPinned := pinnedMsgDB2PB(pin) - pbList := pinnedListDB2PB(pinnedList) + pbListAll := pinnedListDB2PB(pinnedList) + visibleList, err := s.filterPinnedMsgsByUserSeq(ctx, req.GroupID, mcontext.GetOpUserID(ctx), pinnedList) + if err != nil { + return nil, err + } - s.notification.GroupMessagePinnedNotification(ctx, req.GroupID, groupPinnedActionPin, pbPinned, pbList) + s.notification.GroupMessagePinnedNotification(ctx, req.GroupID, groupPinnedActionPin, pbPinned, pbListAll) return &pbgroup.PinGroupMessageResp{ PinnedMsg: pbPinned, - PinnedList: pbList, + PinnedList: pinnedListDB2PB(visibleList), }, nil } @@ -131,11 +136,15 @@ func (s *groupServer) UnpinGroupMessage(ctx context.Context, req *pbgroup.UnpinG } pbPinned := pinnedMsgDB2PB(target) - pbList := pinnedListDB2PB(pinnedList) + pbListAll := pinnedListDB2PB(pinnedList) + visibleList, err := s.filterPinnedMsgsByUserSeq(ctx, req.GroupID, mcontext.GetOpUserID(ctx), pinnedList) + if err != nil { + return nil, err + } - s.notification.GroupMessagePinnedNotification(ctx, req.GroupID, groupPinnedActionUnpin, pbPinned, pbList) + s.notification.GroupMessagePinnedNotification(ctx, req.GroupID, groupPinnedActionUnpin, pbPinned, pbListAll) - return &pbgroup.UnpinGroupMessageResp{PinnedList: pbList}, nil + return &pbgroup.UnpinGroupMessageResp{PinnedList: pinnedListDB2PB(visibleList)}, nil } // GetGroupPinnedMessages 获取群置顶消息列表 @@ -150,11 +159,80 @@ func (s *groupServer) GetGroupPinnedMessages(ctx context.Context, req *pbgroup.G if err != nil { return nil, err } + userID := mcontext.GetOpUserID(ctx) + visibleList, err := s.filterPinnedMsgsByUserSeq(ctx, req.GroupID, userID, pinnedList) + if err != nil { + return nil, err + } return &pbgroup.GetGroupPinnedMessagesResp{ - PinnedList: pinnedListDB2PB(pinnedList), + PinnedList: pinnedListDB2PB(visibleList), }, nil } +// filterPinnedMsgsByUserSeq 按用户在该群会话的 minSeq/maxSeq 过滤置顶消息。 +// 与拉取群消息可见范围一致:新成员(minSeq 被抬高)看不到入群前的置顶;被踢成员(maxSeq 受限)看不到踢出后的置顶。 +func (s *groupServer) filterPinnedMsgsByUserSeq(ctx context.Context, groupID, userID string, list []*model.GroupPinnedMessage) ([]*model.GroupPinnedMessage, error) { + if len(list) == 0 { + return nil, nil + } + if authverify.IsAppManagerUid(ctx, s.config.Share.IMAdminUserID) { + return list, nil + } + if userID == "" { + return nil, errs.ErrNoPermission.WrapMsg("op user id empty") + } + conversationID := msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, groupID) + conv, err := s.conversationClient.GetConversation(ctx, conversationID, userID) + if err != nil { + if errs.ErrRecordNotFound.Is(err) { + return nil, nil + } + return nil, err + } + out := make([]*model.GroupPinnedMessage, 0, len(list)) + for _, m := range list { + if m == nil || !isPinnedSeqVisible(m.Seq, conv.MinSeq, conv.MaxSeq) { + continue + } + out = append(out, m) + } + return out, nil +} + +func isPinnedSeqVisible(seq, minSeq, maxSeq int64) bool { + if seq <= 0 { + return false + } + if minSeq > 0 && seq < minSeq { + return false + } + if maxSeq > 0 && seq > maxSeq { + return false + } + return true +} + +func filterPinnedListPB(list []*sdkws.GroupPinnedMsgInfo, minSeq, maxSeq int64) []*sdkws.GroupPinnedMsgInfo { + if len(list) == 0 { + return nil + } + out := make([]*sdkws.GroupPinnedMsgInfo, 0, len(list)) + for _, m := range list { + if m == nil || !isPinnedSeqVisible(m.Seq, minSeq, maxSeq) { + continue + } + out = append(out, m) + } + return out +} + +func pinnedMsgPBVisibleToUser(pinned *sdkws.GroupPinnedMsgInfo, minSeq, maxSeq int64) *sdkws.GroupPinnedMsgInfo { + if pinned == nil || !isPinnedSeqVisible(pinned.Seq, minSeq, maxSeq) { + return nil + } + return pinned +} + // checkPinPermission 校验当前操作者是否具备群消息置顶权限 func (s *groupServer) checkPinPermission(ctx context.Context, group *model.Group) error { if authverify.IsAppManagerUid(ctx, s.config.Share.IMAdminUserID) { diff --git a/pkg/notification/msg.go b/pkg/notification/msg.go index ba8a9185a..880939dd4 100644 --- a/pkg/notification/msg.go +++ b/pkg/notification/msg.go @@ -181,6 +181,7 @@ func NewNotificationSender(conf *config.Notification, opts ...NotificationSender type notificationOpt struct { RpcGetUsername bool SendMessage *bool + GroupID string } type NotificationOptions func(*notificationOpt) @@ -196,6 +197,13 @@ func WithSendMessage(sendMessage *bool) NotificationOptions { } } +// WithGroupID sets MsgData.GroupID for notifications sent as single chat (e.g. per-member group events). +func WithGroupID(groupID string) NotificationOptions { + return func(opt *notificationOpt) { + opt.GroupID = groupID + } +} + func (s *NotificationSender) send(ctx context.Context, sendID, recvID string, contentType, sessionType int32, m proto.Message, opts ...NotificationOptions) { ctx = context.WithoutCancel(ctx) ctx, cancel := context.WithTimeout(ctx, time.Second*time.Duration(5)) @@ -231,9 +239,15 @@ func (s *NotificationSender) send(ctx context.Context, sendID, recvID string, co msg.SessionType = sessionType if msg.SessionType == constant.ReadGroupChatType { msg.GroupID = recvID + } else if notificationOpt.GroupID != "" { + msg.GroupID = notificationOpt.GroupID } msg.CreateTime = timeutil.GetCurrentTimestampByMill() - msg.ClientMsgID = idutil.GetMsgIDByMD5(sendID) + if notificationOpt.GroupID != "" { + msg.ClientMsgID = idutil.GetMsgIDByMD5(sendID + "_" + recvID + "_" + notificationOpt.GroupID) + } else { + msg.ClientMsgID = idutil.GetMsgIDByMD5(sendID) + } optionsConfig := s.contentTypeConf[contentType] if sendID == recvID && contentType == constant.HasReadReceipt { optionsConfig.ReliabilityLevel = constant.UnreliableNotification