用户设置

This commit is contained in:
hawklin2017 2026-04-13 11:13:04 +08:00
parent 42064d31a7
commit e9d75d8e78
3 changed files with 150 additions and 83 deletions

View File

@ -153,13 +153,12 @@ func (m *msgServer) sendMsgNotification(ctx context.Context, req *pbmsg.SendMsgR
} }
func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq) (resp *pbmsg.SendMsgResp, err error) { func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq) (resp *pbmsg.SendMsgResp, err error) {
if err := m.messageVerification(ctx, req); err != nil {
return nil, err
}
isSend := true
isNotification := msgprocessor.IsNotificationByMsg(req.MsgData) isNotification := msgprocessor.IsNotificationByMsg(req.MsgData)
log.ZInfo(ctx, "sendMsgSingleChat", "isNotification", isNotification, "msgdata", req.MsgData)
isSend := true
if !isNotification { if !isNotification {
log.ZInfo(ctx, "sendMsgSingleChat", "isNotification", isNotification, "msgdata", req.MsgData) // 非通知类消息:执行发送权限校验 + 接收偏好校验(含 blacklist / MsgReceiveSetting / webhook / FriendVerify / globalOpt / convOpt
isSend, err = m.modifyMessageByUserMessageReceiveOpt( isSend, err = m.modifyMessageByUserMessageReceiveOpt(
ctx, ctx,
req.MsgData.RecvID, req.MsgData.RecvID,
@ -174,23 +173,21 @@ func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq
if !isSend { if !isSend {
prommetrics.SingleChatMsgProcessFailedCounter.Inc() prommetrics.SingleChatMsgProcessFailedCounter.Inc()
return nil, nil return nil, nil
} else {
if err := m.webhookBeforeMsgModify(ctx, &m.config.WebhooksConfig.BeforeMsgModify, req); err != nil {
return nil, err
}
log.ZInfo(ctx, "sendMsgSingleChat", "isNotification", isNotification, "msgdata", req.MsgData)
if err := m.MsgDatabase.MsgToMQ(ctx, conversationutil.GenConversationUniqueKeyForSingle(req.MsgData.SendID, req.MsgData.RecvID), req.MsgData); err != nil {
prommetrics.SingleChatMsgProcessFailedCounter.Inc()
return nil, err
}
m.webhookAfterSendSingleMsg(ctx, &m.config.WebhooksConfig.AfterSendSingleMsg, req)
prommetrics.SingleChatMsgProcessSuccessCounter.Inc()
return &pbmsg.SendMsgResp{
ServerMsgID: req.MsgData.ServerMsgID,
ClientMsgID: req.MsgData.ClientMsgID,
SendTime: req.MsgData.SendTime,
}, nil
} }
if err := m.webhookBeforeMsgModify(ctx, &m.config.WebhooksConfig.BeforeMsgModify, req); err != nil {
return nil, err
}
log.ZInfo(ctx, "sendMsgSingleChat after modify", "isNotification", isNotification, "msgdata", req.MsgData)
if err := m.MsgDatabase.MsgToMQ(ctx, conversationutil.GenConversationUniqueKeyForSingle(req.MsgData.SendID, req.MsgData.RecvID), req.MsgData); err != nil {
prommetrics.SingleChatMsgProcessFailedCounter.Inc()
return nil, err
}
m.webhookAfterSendSingleMsg(ctx, &m.config.WebhooksConfig.AfterSendSingleMsg, req)
prommetrics.SingleChatMsgProcessSuccessCounter.Inc()
return &pbmsg.SendMsgResp{
ServerMsgID: req.MsgData.ServerMsgID,
ClientMsgID: req.MsgData.ClientMsgID,
SendTime: req.MsgData.SendTime,
}, nil
} }

View File

@ -16,18 +16,19 @@ package msg
import ( import (
"context" "context"
"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/tools/utils/encrypt"
"github.com/openimsdk/tools/utils/timeutil"
"math/rand" "math/rand"
"strconv" "strconv"
"time" "time"
"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs"
"github.com/openimsdk/protocol/constant" "github.com/openimsdk/protocol/constant"
"github.com/openimsdk/protocol/msg" "github.com/openimsdk/protocol/msg"
"github.com/openimsdk/protocol/sdkws" "github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/tools/utils/encrypt"
"github.com/openimsdk/tools/utils/timeutil"
) )
var ExcludeContentType = []int{constant.HasReadReceipt} var ExcludeContentType = []int{constant.HasReadReceipt}
@ -52,61 +53,14 @@ type MessageRevoked struct {
func (m *msgServer) messageVerification(ctx context.Context, data *msg.SendMsgReq) error { func (m *msgServer) messageVerification(ctx context.Context, data *msg.SendMsgReq) error {
switch data.MsgData.SessionType { switch data.MsgData.SessionType {
case constant.SingleChatType: case constant.SingleChatType:
if datautil.Contain(data.MsgData.SendID, m.config.Share.IMAdminUserID...) { // 单聊发送权限校验已迁移至 modifyMessageByUserMessageReceiveOpt
return nil
}
if data.MsgData.ContentType <= constant.NotificationEnd &&
data.MsgData.ContentType >= constant.NotificationBegin {
return nil
}
// 先做本地轻量级拦截(黑名单 + 消息接收权限),避免不必要的 webhook 触发
black, err := m.FriendLocalCache.IsBlack(ctx, data.MsgData.SendID, data.MsgData.RecvID)
if err != nil {
return err
}
if black {
return servererrs.ErrBlockedByPeer.Wrap()
}
// 校验接收方消息接收权限MsgReceiveSetting
// 0=所有人可发送1=仅好友可发送2=所有人不可发送
recvUserInfo, err := m.UserLocalCache.GetUserInfo(ctx, data.MsgData.RecvID)
if err != nil {
return err
}
switch recvUserInfo.MsgReceiveSetting {
case 2: // MsgReceiveSettingNobody
return servererrs.ErrMsgReceiveNotAllowed.Wrap()
case 1: // MsgReceiveSettingFriends
isFriend, err := m.FriendLocalCache.IsFriend(ctx, data.MsgData.RecvID, data.MsgData.SendID)
if err != nil {
return err
}
if !isFriend {
return servererrs.ErrMsgReceiveNotAllowed.Wrap()
}
// 已确认是好友,触发 webhook 后放行,不做 FriendVerify 冗余查询
if err := m.webhookBeforeSendSingleMsg(ctx, &m.config.WebhooksConfig.BeforeSendSingleMsg, data); err != nil {
return err
}
return nil
}
// MsgReceiveSetting==0所有人可发触发 webhook再按全局 FriendVerify 兜底
if err := m.webhookBeforeSendSingleMsg(ctx, &m.config.WebhooksConfig.BeforeSendSingleMsg, data); err != nil {
return err
}
if m.config.RpcConfig.FriendVerify {
friend, err := m.FriendLocalCache.IsFriend(ctx, data.MsgData.SendID, data.MsgData.RecvID)
if err != nil {
return err
}
if !friend {
return servererrs.ErrNotPeersFriend.Wrap()
}
}
return nil return nil
case constant.ReadGroupChatType: case constant.ReadGroupChatType:
groupInfo, err := m.GroupLocalCache.GetGroupInfo(ctx, data.MsgData.GroupID) groupInfo, err := m.GroupLocalCache.GetGroupInfo(ctx, data.MsgData.GroupID)
if err != nil { if err != nil {
log.ZError(ctx, "messageVerification group: GetGroupInfo failed", err,
"groupID", data.MsgData.GroupID, "sendID", data.MsgData.SendID,
"contentType", data.MsgData.ContentType, "clientMsgID", data.MsgData.ClientMsgID)
return err return err
} }
if groupInfo.Status == constant.GroupStatusDismissed && if groupInfo.Status == constant.GroupStatusDismissed &&
@ -126,6 +80,9 @@ func (m *msgServer) messageVerification(ctx context.Context, data *msg.SendMsgRe
} }
memberIDs, err := m.GroupLocalCache.GetGroupMemberIDMap(ctx, data.MsgData.GroupID) memberIDs, err := m.GroupLocalCache.GetGroupMemberIDMap(ctx, data.MsgData.GroupID)
if err != nil { if err != nil {
log.ZError(ctx, "messageVerification group: GetGroupMemberIDMap failed", err,
"groupID", data.MsgData.GroupID, "sendID", data.MsgData.SendID,
"contentType", data.MsgData.ContentType, "clientMsgID", data.MsgData.ClientMsgID)
return err return err
} }
if _, ok := memberIDs[data.MsgData.SendID]; !ok { if _, ok := memberIDs[data.MsgData.SendID]; !ok {
@ -137,6 +94,9 @@ func (m *msgServer) messageVerification(ctx context.Context, data *msg.SendMsgRe
if errs.ErrRecordNotFound.Is(err) { if errs.ErrRecordNotFound.Is(err) {
return servererrs.ErrNotInGroupYet.WrapMsg(err.Error()) return servererrs.ErrNotInGroupYet.WrapMsg(err.Error())
} }
log.ZError(ctx, "messageVerification group: GetGroupMember failed", err,
"groupID", data.MsgData.GroupID, "sendID", data.MsgData.SendID,
"contentType", data.MsgData.ContentType, "clientMsgID", data.MsgData.ClientMsgID)
return err return err
} }
if groupMemberInfo.RoleLevel == constant.GroupOwner { if groupMemberInfo.RoleLevel == constant.GroupOwner {
@ -211,21 +171,101 @@ func GetMsgID(sendID string) string {
} }
func (m *msgServer) modifyMessageByUserMessageReceiveOpt(ctx context.Context, userID, conversationID string, sessionType int, pb *msg.SendMsgReq) (bool, error) { func (m *msgServer) modifyMessageByUserMessageReceiveOpt(ctx context.Context, userID, conversationID string, sessionType int, pb *msg.SendMsgReq) (bool, error) {
// 第一优先级:接收方全局接收设置
// NotReceiveMessage 直接丢弃,无需执行后续任何权限或偏好查询
opt, err := m.UserLocalCache.GetUserGlobalMsgRecvOpt(ctx, userID) opt, err := m.UserLocalCache.GetUserGlobalMsgRecvOpt(ctx, userID)
if err != nil { if err != nil {
return false, err return false, err
} }
switch opt { if opt == constant.NotReceiveMessage {
case constant.ReceiveMessage:
case constant.NotReceiveMessage:
return false, nil return false, nil
case constant.ReceiveNotNotifyMessage: }
if opt == constant.ReceiveNotNotifyMessage {
if pb.MsgData.Options == nil { if pb.MsgData.Options == nil {
pb.MsgData.Options = make(map[string]bool, 10) pb.MsgData.Options = make(map[string]bool, 10)
} }
datautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false) datautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)
return true, nil // 全局静音:仅关闭离线推送,仍需继续执行发送权限校验 + 会话级偏好校验
} }
// 第二优先级:单聊发送权限校验(从 messageVerification 迁移)
// 仅对非通知类消息生效(调用方已通过 !isNotification 做过前置过滤)
if sessionType == constant.SingleChatType {
// 管理员跳过发送权限拦截,直接进入接收偏好校验
if !datautil.Contain(pb.MsgData.SendID, m.config.Share.IMAdminUserID...) {
// 黑名单拦截
black, err := m.FriendLocalCache.IsBlack(ctx, pb.MsgData.SendID, pb.MsgData.RecvID)
if err != nil {
log.ZError(ctx, "modifyMessageByUserMessageReceiveOpt: IsBlack failed", err,
"sendID", pb.MsgData.SendID, "recvID", pb.MsgData.RecvID,
"contentType", pb.MsgData.ContentType, "clientMsgID", pb.MsgData.ClientMsgID)
return false, err
}
if black {
return false, servererrs.ErrBlockedByPeer.Wrap()
}
// 接收方消息接收权限MsgReceiveSetting
// 0=所有人可发送1=仅好友可发送2=所有人不可发送
recvUserInfo, err := m.UserLocalCache.GetUserInfo(ctx, pb.MsgData.RecvID)
if err != nil {
log.ZError(ctx, "modifyMessageByUserMessageReceiveOpt: GetUserInfo(recv) failed", err,
"sendID", pb.MsgData.SendID, "recvID", pb.MsgData.RecvID,
"contentType", pb.MsgData.ContentType, "clientMsgID", pb.MsgData.ClientMsgID)
return false, err
}
// skipFriendVerify: MsgReceiveSetting=1 已确认好友关系,无需再做 FriendVerify 重复查询
skipFriendVerify := false
switch recvUserInfo.MsgReceiveSetting {
case 2: // MsgReceiveSettingNobody
return false, servererrs.ErrMsgReceiveNotAllowed.Wrap()
case 1: // MsgReceiveSettingFriends
isFriend, err := m.FriendLocalCache.IsFriend(ctx, pb.MsgData.RecvID, pb.MsgData.SendID)
if err != nil {
log.ZError(ctx, "modifyMessageByUserMessageReceiveOpt: IsFriend failed (MsgReceiveSetting)", err,
"sendID", pb.MsgData.SendID, "recvID", pb.MsgData.RecvID,
"contentType", pb.MsgData.ContentType, "clientMsgID", pb.MsgData.ClientMsgID)
return false, err
}
if !isFriend {
return false, servererrs.ErrMsgReceiveNotAllowed.Wrap()
}
// 已确认好友关系,触发 webhook 后跳过 FriendVerify直接进入接收偏好校验
if err := m.webhookBeforeSendSingleMsg(ctx, &m.config.WebhooksConfig.BeforeSendSingleMsg, pb); err != nil {
log.ZError(ctx, "modifyMessageByUserMessageReceiveOpt: webhookBeforeSendSingleMsg failed (friends-only)", err,
"sendID", pb.MsgData.SendID, "recvID", pb.MsgData.RecvID,
"contentType", pb.MsgData.ContentType, "clientMsgID", pb.MsgData.ClientMsgID)
return false, err
}
skipFriendVerify = true
}
if !skipFriendVerify {
// MsgReceiveSetting==0所有人可发触发 webhook再按全局 FriendVerify 兜底
if err := m.webhookBeforeSendSingleMsg(ctx, &m.config.WebhooksConfig.BeforeSendSingleMsg, pb); err != nil {
log.ZError(ctx, "modifyMessageByUserMessageReceiveOpt: webhookBeforeSendSingleMsg failed", err,
"sendID", pb.MsgData.SendID, "recvID", pb.MsgData.RecvID,
"contentType", pb.MsgData.ContentType, "clientMsgID", pb.MsgData.ClientMsgID)
return false, err
}
if m.config.RpcConfig.FriendVerify {
friend, err := m.FriendLocalCache.IsFriend(ctx, pb.MsgData.SendID, pb.MsgData.RecvID)
if err != nil {
log.ZError(ctx, "modifyMessageByUserMessageReceiveOpt: IsFriend failed (FriendVerify)", err,
"sendID", pb.MsgData.SendID, "recvID", pb.MsgData.RecvID,
"contentType", pb.MsgData.ContentType, "clientMsgID", pb.MsgData.ClientMsgID)
return false, err
}
if !friend {
return false, servererrs.ErrNotPeersFriend.Wrap()
}
}
}
}
}
// 第三优先级:会话级接收偏好
singleOpt, err := m.ConversationLocalCache.GetSingleConversationRecvMsgOpt(ctx, userID, conversationID) singleOpt, err := m.ConversationLocalCache.GetSingleConversationRecvMsgOpt(ctx, userID, conversationID)
if errs.ErrRecordNotFound.Is(err) { if errs.ErrRecordNotFound.Is(err) {
return true, nil return true, nil

View File

@ -48,6 +48,7 @@ import (
"github.com/openimsdk/tools/db/pagination" "github.com/openimsdk/tools/db/pagination"
registry "github.com/openimsdk/tools/discovery" registry "github.com/openimsdk/tools/discovery"
"github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil" "github.com/openimsdk/tools/utils/datautil"
"google.golang.org/grpc" "google.golang.org/grpc"
) )
@ -137,10 +138,14 @@ func (s *userServer) GetDesignateUsers(ctx context.Context, req *pbuser.GetDesig
resp = &pbuser.GetDesignateUsersResp{} resp = &pbuser.GetDesignateUsersResp{}
users, err := s.db.Find(ctx, req.UserIDs) users, err := s.db.Find(ctx, req.UserIDs)
if err != nil { if err != nil {
log.ZError(ctx, "GetDesignateUsers: db.Find failed", err,
"opUserID", mcontext.GetOpUserID(ctx), "reqUserCount", len(req.UserIDs))
return nil, err return nil, err
} }
if blocked, err := s.globalBlackDB.FindBlocked(ctx, req.UserIDs); err != nil { if blocked, err := s.globalBlackDB.FindBlocked(ctx, req.UserIDs); err != nil {
log.ZError(ctx, "GetDesignateUsers: globalBlackDB.FindBlocked failed", err,
"opUserID", mcontext.GetOpUserID(ctx), "reqUserCount", len(req.UserIDs))
return nil, err return nil, err
} else if len(blocked) > 0 { } else if len(blocked) > 0 {
bannedIDs := make([]string, 0, len(blocked)) bannedIDs := make([]string, 0, len(blocked))
@ -153,6 +158,8 @@ func (s *userServer) GetDesignateUsers(ctx context.Context, req *pbuser.GetDesig
pbUsers := convert.UsersDB2Pb(users) pbUsers := convert.UsersDB2Pb(users)
viewerID := mcontext.GetOpUserID(ctx) viewerID := mcontext.GetOpUserID(ctx)
if err := s.applyPhoneVisibility(ctx, viewerID, pbUsers, users); err != nil { if err := s.applyPhoneVisibility(ctx, viewerID, pbUsers, users); err != nil {
log.ZError(ctx, "GetDesignateUsers: applyPhoneVisibility failed", err,
"opUserID", viewerID, "userCount", len(users))
return nil, err return nil, err
} }
resp.UsersInfo = pbUsers resp.UsersInfo = pbUsers
@ -182,6 +189,8 @@ func (s *userServer) applyPhoneVisibility(ctx context.Context, viewerID string,
} }
isFriend, err := s.relationClient.IsFriend(ctx, viewerID, db.UserID) isFriend, err := s.relationClient.IsFriend(ctx, viewerID, db.UserID)
if err != nil { if err != nil {
log.ZError(ctx, "applyPhoneVisibility: IsFriend failed", err,
"viewerID", viewerID, "targetUserID", db.UserID)
return err return err
} }
if !isFriend { if !isFriend {
@ -288,9 +297,13 @@ func (s *userServer) SetPhoneVisibility(ctx context.Context, req *pbuser.SetPhon
return nil, errs.ErrArgs.WrapMsg("phoneVisibility must be 0, 1 or 2") return nil, errs.ErrArgs.WrapMsg("phoneVisibility must be 0, 1 or 2")
} }
if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil { if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil {
log.ZWarn(ctx, "SetPhoneVisibility: access denied", err,
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID)
return nil, err return nil, err
} }
if _, err := s.db.FindWithError(ctx, []string{req.UserID}); err != nil { if _, err := s.db.FindWithError(ctx, []string{req.UserID}); err != nil {
log.ZError(ctx, "SetPhoneVisibility: user not found or db error", err,
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID)
return nil, err return nil, err
} }
m := map[string]any{ m := map[string]any{
@ -300,6 +313,9 @@ func (s *userServer) SetPhoneVisibility(ctx context.Context, req *pbuser.SetPhon
m["phone"] = req.Phone m["phone"] = req.Phone
} }
if err := s.db.UpdateByMap(ctx, req.UserID, m); err != nil { if err := s.db.UpdateByMap(ctx, req.UserID, m); err != nil {
log.ZError(ctx, "SetPhoneVisibility: UpdateByMap failed", err,
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID,
"phoneVisibility", req.PhoneVisibility, "hasPhoneUpdate", req.Phone != "")
return nil, err return nil, err
} }
s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserID) s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserID)
@ -316,14 +332,21 @@ func (s *userServer) SetCallAcceptSetting(ctx context.Context, req *pbuser.SetCa
return nil, errs.ErrArgs.WrapMsg("callAcceptSetting must be 0, 1 or 2") return nil, errs.ErrArgs.WrapMsg("callAcceptSetting must be 0, 1 or 2")
} }
if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil { if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil {
log.ZWarn(ctx, "SetCallAcceptSetting: access denied", err,
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID)
return nil, err return nil, err
} }
if _, err := s.db.FindWithError(ctx, []string{req.UserID}); err != nil { if _, err := s.db.FindWithError(ctx, []string{req.UserID}); err != nil {
log.ZError(ctx, "SetCallAcceptSetting: user not found or db error", err,
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID)
return nil, err return nil, err
} }
if err := s.db.UpdateByMap(ctx, req.UserID, map[string]any{ if err := s.db.UpdateByMap(ctx, req.UserID, map[string]any{
"call_accept_setting": req.CallAcceptSetting, "call_accept_setting": req.CallAcceptSetting,
}); err != nil { }); err != nil {
log.ZError(ctx, "SetCallAcceptSetting: UpdateByMap failed", err,
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID,
"callAcceptSetting", req.CallAcceptSetting)
return nil, err return nil, err
} }
s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserID) s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserID)
@ -340,14 +363,21 @@ func (s *userServer) SetMsgReceiveSetting(ctx context.Context, req *pbuser.SetMs
return nil, errs.ErrArgs.WrapMsg("msgReceiveSetting must be 0, 1 or 2") return nil, errs.ErrArgs.WrapMsg("msgReceiveSetting must be 0, 1 or 2")
} }
if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil { if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil {
log.ZWarn(ctx, "SetMsgReceiveSetting: access denied", err,
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID)
return nil, err return nil, err
} }
if _, err := s.db.FindWithError(ctx, []string{req.UserID}); err != nil { if _, err := s.db.FindWithError(ctx, []string{req.UserID}); err != nil {
log.ZError(ctx, "SetMsgReceiveSetting: user not found or db error", err,
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID)
return nil, err return nil, err
} }
if err := s.db.UpdateByMap(ctx, req.UserID, map[string]any{ if err := s.db.UpdateByMap(ctx, req.UserID, map[string]any{
"msg_receive_setting": req.MsgReceiveSetting, "msg_receive_setting": req.MsgReceiveSetting,
}); err != nil { }); err != nil {
log.ZError(ctx, "SetMsgReceiveSetting: UpdateByMap failed", err,
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID,
"msgReceiveSetting", req.MsgReceiveSetting)
return nil, err return nil, err
} }
s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserID) s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserID)