mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-07-14 18:11:12 +08:00
Merge pull request #8 from sok-im/feature/user_setting
Feature/user setting
This commit is contained in:
commit
2033b1db48
@ -15,3 +15,6 @@ prometheus:
|
|||||||
# Prometheus listening ports, must be consistent with the number of rpc.ports
|
# Prometheus listening ports, must be consistent with the number of rpc.ports
|
||||||
# It will only take effect when autoSetPorts is set to false.
|
# It will only take effect when autoSetPorts is set to false.
|
||||||
ports: [ 12320 ]
|
ports: [ 12320 ]
|
||||||
|
|
||||||
|
# GetUserByPhone: false = ignore phone_visibility when searching by phone; true = enforce phone_visibility (hidden / friends-only).
|
||||||
|
phoneSearchVisibility: false
|
||||||
|
|||||||
@ -43,3 +43,11 @@ func (o *AuthApi) ParseToken(c *gin.Context) {
|
|||||||
func (o *AuthApi) ForceLogout(c *gin.Context) {
|
func (o *AuthApi) ForceLogout(c *gin.Context) {
|
||||||
a2r.Call(c, auth.AuthClient.ForceLogout, o.Client)
|
a2r.Call(c, auth.AuthClient.ForceLogout, o.Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *AuthApi) GetActiveDevices(c *gin.Context) {
|
||||||
|
a2r.Call(c, auth.AuthClient.GetActiveDevices, o.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *AuthApi) KickDevice(c *gin.Context) {
|
||||||
|
a2r.Call(c, auth.AuthClient.KickDevice, o.Client)
|
||||||
|
}
|
||||||
|
|||||||
@ -118,3 +118,7 @@ func (o *FriendApi) GetFullFriendUserIDs(c *gin.Context) {
|
|||||||
func (o *FriendApi) GetSelfUnhandledApplyCount(c *gin.Context) {
|
func (o *FriendApi) GetSelfUnhandledApplyCount(c *gin.Context) {
|
||||||
a2r.Call(c, relation.FriendClient.GetSelfUnhandledApplyCount, o.Client)
|
a2r.Call(c, relation.FriendClient.GetSelfUnhandledApplyCount, o.Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *FriendApi) GetPinnedFriendIDs(c *gin.Context) {
|
||||||
|
a2r.Call(c, relation.FriendClient.GetPinnedFriendIDs, o.Client)
|
||||||
|
}
|
||||||
|
|||||||
@ -379,3 +379,15 @@ func (m *MessageApi) GetStreamMsg(c *gin.Context) {
|
|||||||
func (m *MessageApi) AppendStreamMsg(c *gin.Context) {
|
func (m *MessageApi) AppendStreamMsg(c *gin.Context) {
|
||||||
a2r.Call(c, msg.MsgClient.GetServerTime, m.Client)
|
a2r.Call(c, msg.MsgClient.GetServerTime, m.Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MessageApi) ReportSpam(c *gin.Context) {
|
||||||
|
a2r.Call(c, msg.MsgClient.ReportSpam, m.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MessageApi) GetSpamReports(c *gin.Context) {
|
||||||
|
a2r.Call(c, msg.MsgClient.GetSpamReports, m.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MessageApi) HandleSpamReport(c *gin.Context) {
|
||||||
|
a2r.Call(c, msg.MsgClient.HandleSpamReport, m.Client)
|
||||||
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||||
"github.com/openimsdk/tools/apiresp"
|
"github.com/openimsdk/tools/apiresp"
|
||||||
"github.com/openimsdk/tools/errs"
|
"github.com/openimsdk/tools/errs"
|
||||||
|
"github.com/openimsdk/tools/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PhoneSNApi struct {
|
type PhoneSNApi struct {
|
||||||
@ -42,15 +43,23 @@ func (a *PhoneSNApi) GetSNInfo(c *gin.Context) {
|
|||||||
var req phoneGetSNInfoReq
|
var req phoneGetSNInfoReq
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
|
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
|
||||||
|
log.ZError(c, "GetSNInfo", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
phone := strings.TrimSpace(req.Phone)
|
phone := strings.TrimSpace(req.Phone)
|
||||||
if phone == "" {
|
if phone == "" {
|
||||||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("phone is empty"))
|
apiresp.GinError(c, errs.ErrArgs.WrapMsg("phone is empty"))
|
||||||
|
log.ZError(c, "GetSNInfo", errs.ErrArgs.WrapMsg("phone is empty"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info, err := a.db.GetByPhone(c, phone)
|
info, err := a.db.GetByPhone(c, phone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errs.ErrRecordNotFound.Is(err) {
|
||||||
|
apiresp.GinSuccess(c, phoneGetSNInfoResp{IsSnd: false, UserID: 0})
|
||||||
|
log.ZDebug(c, "GetSNInfo", "phone not found", phone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.ZError(c, "GetSNInfo", err)
|
||||||
apiresp.GinError(c, err)
|
apiresp.GinError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -159,6 +159,15 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
|||||||
userRouterGroup.POST("/update_notification_account", u.UpdateNotificationAccountInfo)
|
userRouterGroup.POST("/update_notification_account", u.UpdateNotificationAccountInfo)
|
||||||
userRouterGroup.POST("/search_notification_account", u.SearchNotificationAccount)
|
userRouterGroup.POST("/search_notification_account", u.SearchNotificationAccount)
|
||||||
|
|
||||||
|
// 手机号可见性设置(所有人/仅好友/隐藏)
|
||||||
|
userRouterGroup.POST("/set_phone_visibility", u.SetPhoneVisibility)
|
||||||
|
userRouterGroup.POST("/set_call_accept_setting", u.SetCallAcceptSetting)
|
||||||
|
userRouterGroup.POST("/set_msg_receive_setting", u.SetMsgReceiveSetting)
|
||||||
|
// 根据手机号精确查找用户(phoneSearchVisibility=true 时遵守 phone_visibility 设置)
|
||||||
|
userRouterGroup.POST("/get_user_by_phone", u.GetUserByPhone)
|
||||||
|
// 根据昵称精确查询用户(可多结果,与 getPaginationUsers 模糊搜索不同)
|
||||||
|
userRouterGroup.POST("/get_users_by_nickname", u.GetUsersByNickname)
|
||||||
|
|
||||||
// 全局黑名单管理(仅管理员)
|
// 全局黑名单管理(仅管理员)
|
||||||
userRouterGroup.POST("/add_global_blacklist", bl.AddGlobalBlacklist)
|
userRouterGroup.POST("/add_global_blacklist", bl.AddGlobalBlacklist)
|
||||||
userRouterGroup.POST("/remove_global_blacklist", bl.RemoveGlobalBlacklist)
|
userRouterGroup.POST("/remove_global_blacklist", bl.RemoveGlobalBlacklist)
|
||||||
@ -190,6 +199,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
|||||||
friendRouterGroup.POST("/get_incremental_friends", f.GetIncrementalFriends)
|
friendRouterGroup.POST("/get_incremental_friends", f.GetIncrementalFriends)
|
||||||
friendRouterGroup.POST("/get_full_friend_user_ids", f.GetFullFriendUserIDs)
|
friendRouterGroup.POST("/get_full_friend_user_ids", f.GetFullFriendUserIDs)
|
||||||
friendRouterGroup.POST("/get_self_unhandled_apply_count", f.GetSelfUnhandledApplyCount)
|
friendRouterGroup.POST("/get_self_unhandled_apply_count", f.GetSelfUnhandledApplyCount)
|
||||||
|
friendRouterGroup.POST("/get_pinned_friend_ids", f.GetPinnedFriendIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
g := NewGroupApi(group.NewGroupClient(groupConn))
|
g := NewGroupApi(group.NewGroupClient(groupConn))
|
||||||
@ -237,7 +247,8 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
|||||||
authRouterGroup.POST("/get_user_token", a.GetUserToken)
|
authRouterGroup.POST("/get_user_token", a.GetUserToken)
|
||||||
authRouterGroup.POST("/parse_token", a.ParseToken)
|
authRouterGroup.POST("/parse_token", a.ParseToken)
|
||||||
authRouterGroup.POST("/force_logout", a.ForceLogout)
|
authRouterGroup.POST("/force_logout", a.ForceLogout)
|
||||||
|
authRouterGroup.POST("/get_active_devices", a.GetActiveDevices)
|
||||||
|
authRouterGroup.POST("/kick_device", a.KickDevice)
|
||||||
}
|
}
|
||||||
// Third service
|
// Third service
|
||||||
{
|
{
|
||||||
@ -287,6 +298,9 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
|||||||
msgGroup.POST("/batch_send_msg", m.BatchSendMsg)
|
msgGroup.POST("/batch_send_msg", m.BatchSendMsg)
|
||||||
msgGroup.POST("/check_msg_is_send_success", m.CheckMsgIsSendSuccess)
|
msgGroup.POST("/check_msg_is_send_success", m.CheckMsgIsSendSuccess)
|
||||||
msgGroup.POST("/get_server_time", m.GetServerTime)
|
msgGroup.POST("/get_server_time", m.GetServerTime)
|
||||||
|
msgGroup.POST("/report_spam", m.ReportSpam)
|
||||||
|
msgGroup.POST("/get_spam_reports", m.GetSpamReports)
|
||||||
|
msgGroup.POST("/handle_spam_report", m.HandleSpamReport)
|
||||||
}
|
}
|
||||||
// Conversation
|
// Conversation
|
||||||
{
|
{
|
||||||
|
|||||||
@ -305,3 +305,23 @@ func (u *UserApi) UpdateNotificationAccountInfo(c *gin.Context) {
|
|||||||
func (u *UserApi) SearchNotificationAccount(c *gin.Context) {
|
func (u *UserApi) SearchNotificationAccount(c *gin.Context) {
|
||||||
a2r.Call(c, user.UserClient.SearchNotificationAccount, u.Client)
|
a2r.Call(c, user.UserClient.SearchNotificationAccount, u.Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UserApi) SetPhoneVisibility(c *gin.Context) {
|
||||||
|
a2r.Call(c, user.UserClient.SetPhoneVisibility, u.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserApi) SetCallAcceptSetting(c *gin.Context) {
|
||||||
|
a2r.Call(c, user.UserClient.SetCallAcceptSetting, u.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserApi) SetMsgReceiveSetting(c *gin.Context) {
|
||||||
|
a2r.Call(c, user.UserClient.SetMsgReceiveSetting, u.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserApi) GetUserByPhone(c *gin.Context) {
|
||||||
|
a2r.Call(c, user.UserClient.GetUserByPhone, u.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserApi) GetUsersByNickname(c *gin.Context) {
|
||||||
|
a2r.Call(c, user.UserClient.GetUsersByNickname, u.Client)
|
||||||
|
}
|
||||||
|
|||||||
@ -294,3 +294,50 @@ func (s *authServer) KickTokens(ctx context.Context, req *pbauth.KickTokensReq)
|
|||||||
}
|
}
|
||||||
return &pbauth.KickTokensResp{}, nil
|
return &pbauth.KickTokensResp{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetActiveDevices returns all platforms that have at least one valid (non-kicked) token for the user.
|
||||||
|
// Only the user themselves or an admin can call this.
|
||||||
|
func (s *authServer) GetActiveDevices(ctx context.Context, req *pbauth.GetActiveDevicesReq) (*pbauth.GetActiveDevicesResp, error) {
|
||||||
|
if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var devices []*pbauth.DeviceInfo
|
||||||
|
for platformID, platformName := range constant.PlatformID2Name {
|
||||||
|
if int32(platformID) == constant.AdminPlatformID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m, err := s.authDatabase.GetTokensWithoutError(ctx, req.UserID, platformID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, state := range m {
|
||||||
|
if state == constant.NormalToken {
|
||||||
|
devices = append(devices, &pbauth.DeviceInfo{
|
||||||
|
PlatformID: int32(platformID),
|
||||||
|
PlatformName: platformName,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &pbauth.GetActiveDevicesResp{Devices: devices}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KickDevice kicks the specified platform device offline for the given user.
|
||||||
|
// Only the user themselves or an admin can call this.
|
||||||
|
func (s *authServer) KickDevice(ctx context.Context, req *pbauth.KickDeviceReq) (*pbauth.KickDeviceResp, error) {
|
||||||
|
if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.PlatformID == constant.AdminPlatformID {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("cannot kick admin platform")
|
||||||
|
}
|
||||||
|
if _, ok := constant.PlatformID2Name[int(req.PlatformID)]; !ok {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("invalid platformID", "platformID", req.PlatformID)
|
||||||
|
}
|
||||||
|
if err := s.forceKickOff(ctx, req.UserID, req.PlatformID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pbauth.KickDeviceResp{}, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -161,7 +161,7 @@ func (s *server) VerifyCaptcha(ctx context.Context, req *pbcaptcha.VerifyCaptcha
|
|||||||
}
|
}
|
||||||
success := slide.Validate(int(req.X), int(req.Y), doc.X, doc.Y, s.conf.VerifyPadding)
|
success := slide.Validate(int(req.X), int(req.Y), doc.X, doc.Y, s.conf.VerifyPadding)
|
||||||
if !success {
|
if !success {
|
||||||
log.ZWarn(ctx, "captcha validate failed", nil, "captchaID", req.CaptchaID, "x", req.X, "y", req.Y)
|
log.ZError(ctx, "captcha validate failed", nil, "captchaID", req.CaptchaID, "x", req.X, "y", req.Y, "docX", doc.X, "docY", doc.Y)
|
||||||
}
|
}
|
||||||
return &pbcaptcha.VerifyCaptchaResp{Success: success}, nil
|
return &pbcaptcha.VerifyCaptchaResp{Success: success}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,10 @@ func (s *groupServer) groupDB2PB(group *model.Group, ownerUserID string, memberC
|
|||||||
ApplyMemberFriend: group.ApplyMemberFriend,
|
ApplyMemberFriend: group.ApplyMemberFriend,
|
||||||
NotificationUpdateTime: group.NotificationUpdateTime.UnixMilli(),
|
NotificationUpdateTime: group.NotificationUpdateTime.UnixMilli(),
|
||||||
NotificationUserID: group.NotificationUserID,
|
NotificationUserID: group.NotificationUserID,
|
||||||
|
AllowSendMsg: group.AllowSendMsg,
|
||||||
|
AllowPinMsg: group.AllowPinMsg,
|
||||||
|
AllowAddMember: group.AllowAddMember,
|
||||||
|
AllowEditGroupInfo: group.AllowEditGroupInfo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,18 @@ func UpdateGroupInfoMap(ctx context.Context, group *sdkws.GroupInfoForSet) map[s
|
|||||||
if group.Ex != nil {
|
if group.Ex != nil {
|
||||||
m["ex"] = group.Ex.Value
|
m["ex"] = group.Ex.Value
|
||||||
}
|
}
|
||||||
|
if group.AllowSendMsg != nil {
|
||||||
|
m["allow_send_msg"] = group.AllowSendMsg.Value
|
||||||
|
}
|
||||||
|
if group.AllowPinMsg != nil {
|
||||||
|
m["allow_pin_msg"] = group.AllowPinMsg.Value
|
||||||
|
}
|
||||||
|
if group.AllowAddMember != nil {
|
||||||
|
m["allow_add_member"] = group.AllowAddMember.Value
|
||||||
|
}
|
||||||
|
if group.AllowEditGroupInfo != nil {
|
||||||
|
m["allow_edit_group_info"] = group.AllowEditGroupInfo.Value
|
||||||
|
}
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,6 +112,22 @@ func UpdateGroupInfoExMap(ctx context.Context, group *pbgroup.SetGroupInfoExReq)
|
|||||||
m["ex"] = group.Ex.Value
|
m["ex"] = group.Ex.Value
|
||||||
normalFlag = true
|
normalFlag = true
|
||||||
}
|
}
|
||||||
|
if group.AllowSendMsg != nil {
|
||||||
|
m["allow_send_msg"] = group.AllowSendMsg.Value
|
||||||
|
normalFlag = true
|
||||||
|
}
|
||||||
|
if group.AllowPinMsg != nil {
|
||||||
|
m["allow_pin_msg"] = group.AllowPinMsg.Value
|
||||||
|
normalFlag = true
|
||||||
|
}
|
||||||
|
if group.AllowAddMember != nil {
|
||||||
|
m["allow_add_member"] = group.AllowAddMember.Value
|
||||||
|
normalFlag = true
|
||||||
|
}
|
||||||
|
if group.AllowEditGroupInfo != nil {
|
||||||
|
m["allow_edit_group_info"] = group.AllowEditGroupInfo.Value
|
||||||
|
normalFlag = true
|
||||||
|
}
|
||||||
|
|
||||||
return m, normalFlag, groupNameFlag, notificationFlag, nil
|
return m, normalFlag, groupNameFlag, notificationFlag, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -458,6 +458,11 @@ func (s *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.Invite
|
|||||||
if err := s.PopulateGroupMember(ctx, groupMember); err != nil {
|
if err := s.PopulateGroupMember(ctx, groupMember); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// AllowAddMember == 1 时仅群主/管理员可拉人
|
||||||
|
isOwnerOrAdmin := groupMember.RoleLevel == constant.GroupOwner || groupMember.RoleLevel == constant.GroupAdmin
|
||||||
|
if group.AllowAddMember == model.GroupPermAdminOnly && !isOwnerOrAdmin {
|
||||||
|
return nil, errs.ErrNoPermission.WrapMsg("only owner or admin can add members to this group")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
opUserID = mcontext.GetOpUserID(ctx)
|
opUserID = mcontext.GetOpUserID(ctx)
|
||||||
}
|
}
|
||||||
@ -1098,8 +1103,22 @@ func (s *groupServer) SetGroupInfo(ctx context.Context, req *pbgroup.SetGroupInf
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !(opMember.RoleLevel == constant.GroupOwner || opMember.RoleLevel == constant.GroupAdmin) {
|
isOwnerOrAdmin := opMember.RoleLevel == constant.GroupOwner || opMember.RoleLevel == constant.GroupAdmin
|
||||||
return nil, errs.ErrNoPermission.WrapMsg("no group owner or admin")
|
requestsPermField := req.GroupInfoForSet.AllowSendMsg != nil ||
|
||||||
|
req.GroupInfoForSet.AllowPinMsg != nil ||
|
||||||
|
req.GroupInfoForSet.AllowAddMember != nil ||
|
||||||
|
req.GroupInfoForSet.AllowEditGroupInfo != nil
|
||||||
|
if requestsPermField && !isOwnerOrAdmin {
|
||||||
|
return nil, errs.ErrNoPermission.WrapMsg("only owner or admin can change group permission settings")
|
||||||
|
}
|
||||||
|
if !isOwnerOrAdmin {
|
||||||
|
grp, err := s.db.TakeGroup(ctx, req.GroupInfoForSet.GroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if grp.AllowEditGroupInfo == model.GroupPermAdminOnly {
|
||||||
|
return nil, errs.ErrNoPermission.WrapMsg("only owner or admin can edit group info")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := s.PopulateGroupMember(ctx, opMember); err != nil {
|
if err := s.PopulateGroupMember(ctx, opMember); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -1193,9 +1212,24 @@ func (s *groupServer) SetGroupInfoEx(ctx context.Context, req *pbgroup.SetGroupI
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
isOwnerOrAdmin := opMember.RoleLevel == constant.GroupOwner || opMember.RoleLevel == constant.GroupAdmin
|
||||||
if !(opMember.RoleLevel == constant.GroupOwner || opMember.RoleLevel == constant.GroupAdmin) {
|
// 4个群权限字段始终只有群主/管理员可修改
|
||||||
return nil, errs.ErrNoPermission.WrapMsg("no group owner or admin")
|
requestsPermField := req.AllowSendMsg != nil ||
|
||||||
|
req.AllowPinMsg != nil ||
|
||||||
|
req.AllowAddMember != nil ||
|
||||||
|
req.AllowEditGroupInfo != nil
|
||||||
|
if requestsPermField && !isOwnerOrAdmin {
|
||||||
|
return nil, errs.ErrNoPermission.WrapMsg("only owner or admin can change group permission settings")
|
||||||
|
}
|
||||||
|
// 其他字段:按 AllowEditGroupInfo 决定是否允许普通成员操作
|
||||||
|
if !isOwnerOrAdmin {
|
||||||
|
grp, err := s.db.TakeGroup(ctx, req.GroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if grp.AllowEditGroupInfo == model.GroupPermAdminOnly {
|
||||||
|
return nil, errs.ErrNoPermission.WrapMsg("only owner or admin can edit group info")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.PopulateGroupMember(ctx, opMember); err != nil {
|
if err := s.PopulateGroupMember(ctx, opMember); err != nil {
|
||||||
|
|||||||
143
internal/rpc/msg/report.go
Normal file
143
internal/rpc/msg/report.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package msg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/authverify"
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||||
|
"github.com/openimsdk/protocol/msg"
|
||||||
|
"github.com/openimsdk/tools/errs"
|
||||||
|
"github.com/openimsdk/tools/mcontext"
|
||||||
|
"github.com/openimsdk/tools/utils/datautil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func genReportID() string {
|
||||||
|
const dataLen = 12
|
||||||
|
data := make([]byte, dataLen)
|
||||||
|
rand.Read(data)
|
||||||
|
chars := []byte("0123456789abcdefghijklmnopqrstuvwxyz")
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
data[i] = chars[data[i]%byte(len(chars))]
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *msgServer) ReportSpam(ctx context.Context, req *msg.ReportSpamReq) (*msg.ReportSpamResp, error) {
|
||||||
|
if req.ReportedUserID == "" {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("reportedUserID is required")
|
||||||
|
}
|
||||||
|
if req.ReasonType <= 0 {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("reasonType must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
reporterUserID := mcontext.GetOpUserID(ctx)
|
||||||
|
|
||||||
|
report := &model.SpamReport{
|
||||||
|
ReporterUserID: reporterUserID,
|
||||||
|
ReportedUserID: req.ReportedUserID,
|
||||||
|
ConversationID: req.ConversationID,
|
||||||
|
ClientMsgID: req.ClientMsgID,
|
||||||
|
Seq: req.Seq,
|
||||||
|
ReasonType: req.ReasonType,
|
||||||
|
Reason: req.Reason,
|
||||||
|
Status: model.SpamReportStatusPending,
|
||||||
|
CreateTime: time.Now(),
|
||||||
|
Ex: req.Ex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique reportID.
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
id := genReportID()
|
||||||
|
existing, err := m.spamReportDB.Get(ctx, id)
|
||||||
|
if err == nil && existing != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
report.ReportID = id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if report.ReportID == "" {
|
||||||
|
return nil, errs.ErrInternalServer.WrapMsg("failed to generate report ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.spamReportDB.Create(ctx, report); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &msg.ReportSpamResp{ReportID: report.ReportID}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *msgServer) GetSpamReports(ctx context.Context, req *msg.GetSpamReportsReq) (*msg.GetSpamReportsResp, error) {
|
||||||
|
if err := authverify.CheckAdmin(ctx, m.config.Share.IMAdminUserID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var start, end time.Time
|
||||||
|
if req.StartTime > 0 {
|
||||||
|
start = time.UnixMilli(req.StartTime)
|
||||||
|
}
|
||||||
|
if req.EndTime > 0 {
|
||||||
|
end = time.UnixMilli(req.EndTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
total, reports, err := m.spamReportDB.Find(ctx, req.Status, req.ReportedUserID, req.ReporterUserID,
|
||||||
|
start, end, req.Pagination)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pbReports := datautil.Slice(reports, func(r *model.SpamReport) *msg.SpamReportInfo {
|
||||||
|
return &msg.SpamReportInfo{
|
||||||
|
ReportID: r.ReportID,
|
||||||
|
ReporterUserID: r.ReporterUserID,
|
||||||
|
ReportedUserID: r.ReportedUserID,
|
||||||
|
ConversationID: r.ConversationID,
|
||||||
|
ClientMsgID: r.ClientMsgID,
|
||||||
|
Seq: r.Seq,
|
||||||
|
ReasonType: r.ReasonType,
|
||||||
|
Reason: r.Reason,
|
||||||
|
Status: r.Status,
|
||||||
|
CreateTime: r.CreateTime.UnixMilli(),
|
||||||
|
HandleTime: r.HandleTime.UnixMilli(),
|
||||||
|
HandlerUserID: r.HandlerUserID,
|
||||||
|
Ex: r.Ex,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return &msg.GetSpamReportsResp{
|
||||||
|
Reports: pbReports,
|
||||||
|
Total: uint32(total),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *msgServer) HandleSpamReport(ctx context.Context, req *msg.HandleSpamReportReq) (*msg.HandleSpamReportResp, error) {
|
||||||
|
if err := authverify.CheckAdmin(ctx, m.config.Share.IMAdminUserID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.ReportID == "" {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("reportID is required")
|
||||||
|
}
|
||||||
|
if req.Status != model.SpamReportStatusHandled && req.Status != model.SpamReportStatusIgnored {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("status must be 1 (handled) or 2 (ignored)")
|
||||||
|
}
|
||||||
|
|
||||||
|
handlerUserID := mcontext.GetOpUserID(ctx)
|
||||||
|
if err := m.spamReportDB.UpdateStatus(ctx, req.ReportID, req.Status, handlerUserID, time.Now()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &msg.HandleSpamReportResp{}, nil
|
||||||
|
}
|
||||||
@ -158,8 +158,10 @@ func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq
|
|||||||
}
|
}
|
||||||
isSend := true
|
isSend := true
|
||||||
isNotification := msgprocessor.IsNotificationByMsg(req.MsgData)
|
isNotification := msgprocessor.IsNotificationByMsg(req.MsgData)
|
||||||
|
log.ZInfo(ctx, "sendMsgSingleChat", "isNotification", isNotification, "msgdata", req.MsgData)
|
||||||
|
|
||||||
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 +176,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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis"
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo"
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/webhook"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/webhook"
|
||||||
"github.com/openimsdk/protocol/sdkws"
|
"github.com/openimsdk/protocol/sdkws"
|
||||||
@ -69,6 +70,7 @@ type msgServer struct {
|
|||||||
config *Config // Global configuration settings.
|
config *Config // Global configuration settings.
|
||||||
webhookClient *webhook.Client
|
webhookClient *webhook.Client
|
||||||
conversationClient *rpcli.ConversationClient
|
conversationClient *rpcli.ConversationClient
|
||||||
|
spamReportDB database.SpamReport
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *msgServer) addInterceptorHandler(interceptorFunc ...MessageInterceptorFunc) {
|
func (m *msgServer) addInterceptorHandler(interceptorFunc ...MessageInterceptorFunc) {
|
||||||
@ -121,6 +123,10 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conversationClient := rpcli.NewConversationClient(conversationConn)
|
conversationClient := rpcli.NewConversationClient(conversationConn)
|
||||||
|
spamReportDB, err := mgo.NewSpamReportMongo(mgocli.GetDB())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
s := &msgServer{
|
s := &msgServer{
|
||||||
MsgDatabase: msgDatabase,
|
MsgDatabase: msgDatabase,
|
||||||
RegisterCenter: client,
|
RegisterCenter: client,
|
||||||
@ -131,6 +137,7 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
|||||||
config: config,
|
config: config,
|
||||||
webhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL),
|
webhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL),
|
||||||
conversationClient: conversationClient,
|
conversationClient: conversationClient,
|
||||||
|
spamReportDB: spamReportDB,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.notificationSender = notification.NewNotificationSender(&config.NotificationConfig, notification.WithLocalSendMsg(s.SendMsg))
|
s.notificationSender = notification.NewNotificationSender(&config.NotificationConfig, notification.WithLocalSendMsg(s.SendMsg))
|
||||||
|
|||||||
@ -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,37 +53,13 @@ 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...) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if data.MsgData.ContentType <= constant.NotificationEnd &&
|
|
||||||
data.MsgData.ContentType >= constant.NotificationBegin {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := m.webhookBeforeSendSingleMsg(ctx, &m.config.WebhooksConfig.BeforeSendSingleMsg, data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
black, err := m.FriendLocalCache.IsBlack(ctx, data.MsgData.SendID, data.MsgData.RecvID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if black {
|
|
||||||
return servererrs.ErrBlockedByPeer.Wrap()
|
|
||||||
}
|
|
||||||
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
|
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 &&
|
||||||
@ -102,6 +79,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 {
|
||||||
@ -113,6 +93,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 {
|
||||||
@ -124,6 +107,10 @@ func (m *msgServer) messageVerification(ctx context.Context, data *msg.SendMsgRe
|
|||||||
if groupInfo.Status == constant.GroupStatusMuted && groupMemberInfo.RoleLevel != constant.GroupAdmin {
|
if groupInfo.Status == constant.GroupStatusMuted && groupMemberInfo.RoleLevel != constant.GroupAdmin {
|
||||||
return servererrs.ErrMutedGroup.Wrap()
|
return servererrs.ErrMutedGroup.Wrap()
|
||||||
}
|
}
|
||||||
|
// AllowSendMsg == 1 时仅群主/管理员可发消息
|
||||||
|
if groupInfo.AllowSendMsg == 1 && groupMemberInfo.RoleLevel != constant.GroupAdmin {
|
||||||
|
return servererrs.ErrNoPermission.WrapMsg("only owner or admin can send messages in this group")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
@ -183,21 +170,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
|
||||||
|
|||||||
@ -34,12 +34,16 @@ import (
|
|||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/convert"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/convert"
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs"
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller"
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/util/conversationutil"
|
||||||
"github.com/openimsdk/protocol/constant"
|
"github.com/openimsdk/protocol/constant"
|
||||||
|
conversationpb "github.com/openimsdk/protocol/conversation"
|
||||||
"github.com/openimsdk/protocol/relation"
|
"github.com/openimsdk/protocol/relation"
|
||||||
"github.com/openimsdk/protocol/sdkws"
|
"github.com/openimsdk/protocol/sdkws"
|
||||||
|
"github.com/openimsdk/protocol/wrapperspb"
|
||||||
"github.com/openimsdk/tools/db/mongoutil"
|
"github.com/openimsdk/tools/db/mongoutil"
|
||||||
"github.com/openimsdk/tools/discovery"
|
"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"
|
||||||
)
|
)
|
||||||
@ -48,12 +52,14 @@ type friendServer struct {
|
|||||||
relation.UnimplementedFriendServer
|
relation.UnimplementedFriendServer
|
||||||
db controller.FriendDatabase
|
db controller.FriendDatabase
|
||||||
blackDatabase controller.BlackDatabase
|
blackDatabase controller.BlackDatabase
|
||||||
|
globalBlackDB controller.UserGlobalBlackDatabase
|
||||||
notificationSender *FriendNotificationSender
|
notificationSender *FriendNotificationSender
|
||||||
RegisterCenter discovery.SvcDiscoveryRegistry
|
RegisterCenter discovery.SvcDiscoveryRegistry
|
||||||
config *Config
|
config *Config
|
||||||
webhookClient *webhook.Client
|
webhookClient *webhook.Client
|
||||||
queue *memamq.MemoryQueue
|
queue *memamq.MemoryQueue
|
||||||
userClient *rpcli.UserClient
|
userClient *rpcli.UserClient
|
||||||
|
conversationClient *rpcli.ConversationClient
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -93,6 +99,11 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalBlackMongoDB, err := mgo.NewUserGlobalBlackMongo(mgocli.GetDB())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
userConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.User)
|
userConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -101,6 +112,10 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
conversationConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.Conversation)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
userClient := rpcli.NewUserClient(userConn)
|
userClient := rpcli.NewUserClient(userConn)
|
||||||
|
|
||||||
database := controller.NewFriendDatabase(
|
database := controller.NewFriendDatabase(
|
||||||
@ -125,12 +140,14 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
|||||||
blackMongoDB,
|
blackMongoDB,
|
||||||
redis.NewBlackCacheRedis(rdb, &config.LocalCacheConfig, blackMongoDB, redis.GetRocksCacheOptions()),
|
redis.NewBlackCacheRedis(rdb, &config.LocalCacheConfig, blackMongoDB, redis.GetRocksCacheOptions()),
|
||||||
),
|
),
|
||||||
|
globalBlackDB: controller.NewUserGlobalBlackDatabase(globalBlackMongoDB),
|
||||||
notificationSender: notificationSender,
|
notificationSender: notificationSender,
|
||||||
RegisterCenter: client,
|
RegisterCenter: client,
|
||||||
config: config,
|
config: config,
|
||||||
webhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL),
|
webhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL),
|
||||||
queue: memamq.NewMemoryQueue(16, 1024*1024),
|
queue: memamq.NewMemoryQueue(16, 1024*1024),
|
||||||
userClient: userClient,
|
userClient: userClient,
|
||||||
|
conversationClient: rpcli.NewConversationClient(conversationConn),
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -287,6 +304,9 @@ func (s *friendServer) GetFriendInfo(ctx context.Context, req *relation.GetFrien
|
|||||||
if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil {
|
if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := s.checkUsersNotGlobalBlocked(ctx, req.FriendUserIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
friends, err := s.db.FindFriendsWithError(ctx, req.OwnerUserID, req.FriendUserIDs)
|
friends, err := s.db.FindFriendsWithError(ctx, req.OwnerUserID, req.FriendUserIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -302,6 +322,9 @@ func (s *friendServer) GetDesignatedFriends(ctx context.Context, req *relation.G
|
|||||||
if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil {
|
if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := s.checkUsersNotGlobalBlocked(ctx, req.FriendUserIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
friends, err := s.getFriend(ctx, req.OwnerUserID, req.FriendUserIDs)
|
friends, err := s.getFriend(ctx, req.OwnerUserID, req.FriendUserIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -311,6 +334,25 @@ func (s *friendServer) GetDesignatedFriends(ctx context.Context, req *relation.G
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkUsersNotGlobalBlocked returns ErrUserBlocked if any of the given userIDs are in the global blacklist.
|
||||||
|
func (s *friendServer) checkUsersNotGlobalBlocked(ctx context.Context, userIDs []string) error {
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
blocked, err := s.globalBlackDB.FindBlocked(ctx, userIDs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(blocked) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bannedIDs := make([]string, 0, len(blocked))
|
||||||
|
for _, b := range blocked {
|
||||||
|
bannedIDs = append(bannedIDs, b.UserID)
|
||||||
|
}
|
||||||
|
return servererrs.ErrUserBlocked.WrapMsg("user is banned", "userIDs", bannedIDs)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *friendServer) getFriend(ctx context.Context, ownerUserID string, friendUserIDs []string) ([]*sdkws.FriendInfo, error) {
|
func (s *friendServer) getFriend(ctx context.Context, ownerUserID string, friendUserIDs []string) ([]*sdkws.FriendInfo, error) {
|
||||||
if len(friendUserIDs) == 0 {
|
if len(friendUserIDs) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -490,6 +532,9 @@ func (s *friendServer) GetSpecifiedFriendsInfo(ctx context.Context, req *relatio
|
|||||||
OperatorUserID: friend.OperatorUserID,
|
OperatorUserID: friend.OperatorUserID,
|
||||||
Ex: friend.Ex,
|
Ex: friend.Ex,
|
||||||
IsPinned: friend.IsPinned,
|
IsPinned: friend.IsPinned,
|
||||||
|
IsMute: friend.IsMuted,
|
||||||
|
MuteDuration: friend.MuteDuration,
|
||||||
|
MuteEndTime: friend.MuteEndTime,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -545,10 +590,55 @@ func (s *friendServer) UpdateFriends(
|
|||||||
if req.Ex != nil {
|
if req.Ex != nil {
|
||||||
val["ex"] = req.Ex.Value
|
val["ex"] = req.Ex.Value
|
||||||
}
|
}
|
||||||
|
if req.IsMute != nil {
|
||||||
|
val["is_muted"] = req.IsMute.Value
|
||||||
|
}
|
||||||
|
if req.MuteDuration != nil {
|
||||||
|
val["mute_duration"] = req.MuteDuration.Value
|
||||||
|
}
|
||||||
|
if req.MuteEndTime != nil {
|
||||||
|
val["mute_end_time"] = req.MuteEndTime.Value
|
||||||
|
}
|
||||||
if err = s.db.UpdateFriends(ctx, req.OwnerUserID, req.FriendUserIDs, val); err != nil {
|
if err = s.db.UpdateFriends(ctx, req.OwnerUserID, req.FriendUserIDs, val); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.IsPinned != nil {
|
||||||
|
for _, friendUserID := range req.FriendUserIDs {
|
||||||
|
convID := conversationutil.GenConversationIDForSingle(req.OwnerUserID, friendUserID)
|
||||||
|
if err := s.conversationClient.SetConversations(ctx, []string{req.OwnerUserID},
|
||||||
|
&conversationpb.ConversationReq{
|
||||||
|
ConversationID: convID,
|
||||||
|
ConversationType: constant.SingleChatType,
|
||||||
|
UserID: friendUserID,
|
||||||
|
IsPinned: req.IsPinned,
|
||||||
|
}); err != nil {
|
||||||
|
log.ZWarn(ctx, "sync conversation isPinned failed", err,
|
||||||
|
"ownerUserID", req.OwnerUserID, "friendUserID", friendUserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.IsMute != nil {
|
||||||
|
recvMsgOpt := int32(constant.ReceiveNotNotifyMessage)
|
||||||
|
if !req.IsMute.Value {
|
||||||
|
recvMsgOpt = constant.ReceiveMessage
|
||||||
|
}
|
||||||
|
for _, friendUserID := range req.FriendUserIDs {
|
||||||
|
convID := conversationutil.GenConversationIDForSingle(req.OwnerUserID, friendUserID)
|
||||||
|
if err := s.conversationClient.SetConversations(ctx, []string{req.OwnerUserID},
|
||||||
|
&conversationpb.ConversationReq{
|
||||||
|
ConversationID: convID,
|
||||||
|
ConversationType: constant.SingleChatType,
|
||||||
|
UserID: friendUserID,
|
||||||
|
RecvMsgOpt: &wrapperspb.Int32Value{Value: recvMsgOpt},
|
||||||
|
}); err != nil {
|
||||||
|
log.ZWarn(ctx, "sync conversation recvMsgOpt failed", err,
|
||||||
|
"ownerUserID", req.OwnerUserID, "friendUserID", friendUserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp := &relation.UpdateFriendsResp{}
|
resp := &relation.UpdateFriendsResp{}
|
||||||
|
|
||||||
s.notificationSender.FriendsInfoUpdateNotification(ctx, req.OwnerUserID, req.FriendUserIDs)
|
s.notificationSender.FriendsInfoUpdateNotification(ctx, req.OwnerUserID, req.FriendUserIDs)
|
||||||
@ -570,6 +660,17 @@ func (s *friendServer) GetSelfUnhandledApplyCount(ctx context.Context, req *rela
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *friendServer) GetPinnedFriendIDs(ctx context.Context, req *relation.GetPinnedFriendIDsReq) (*relation.GetPinnedFriendIDsResp, error) {
|
||||||
|
if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ids, err := s.db.GetPinnedFriendIDs(ctx, req.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &relation.GetPinnedFriendIDsResp{FriendUserIDs: ids}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *friendServer) getCommonUserMap(ctx context.Context, userIDs []string) (map[string]common_user.CommonUser, error) {
|
func (s *friendServer) getCommonUserMap(ctx context.Context, userIDs []string) (map[string]common_user.CommonUser, error) {
|
||||||
users, err := s.userClient.GetUsersInfo(ctx, userIDs)
|
users, err := s.userClient.GetUsersInfo(ctx, userIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -39,12 +39,13 @@ type Config struct {
|
|||||||
|
|
||||||
type rtcServer struct {
|
type rtcServer struct {
|
||||||
rtc.UnimplementedRtcServiceServer
|
rtc.UnimplementedRtcServiceServer
|
||||||
config *Config
|
config *Config
|
||||||
db controller.RtcDatabase
|
db controller.RtcDatabase
|
||||||
roomClient *lksdk.RoomServiceClient
|
roomClient *lksdk.RoomServiceClient
|
||||||
msgClient *rpcli.MsgClient
|
msgClient *rpcli.MsgClient
|
||||||
userClient *rpcli.UserClient
|
userClient *rpcli.UserClient
|
||||||
tokenExpiry time.Duration
|
relationClient *rpcli.RelationClient
|
||||||
|
tokenExpiry time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start initialises the RTC gRPC service and registers it with the gRPC server.
|
// Start initialises the RTC gRPC service and registers it with the gRPC server.
|
||||||
@ -69,6 +70,11 @@ func Start(ctx context.Context, cfg *Config, client discovery.SvcDiscoveryRegist
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
friendConn, err := client.GetConn(ctx, cfg.Share.RpcRegisterName.Friend)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
lk := cfg.RpcConfig.LiveKit
|
lk := cfg.RpcConfig.LiveKit
|
||||||
roomClient := lksdk.NewRoomServiceClient(lk.InternalAddress, lk.APIKey, lk.APISecret)
|
roomClient := lksdk.NewRoomServiceClient(lk.InternalAddress, lk.APIKey, lk.APISecret)
|
||||||
|
|
||||||
@ -78,12 +84,13 @@ func Start(ctx context.Context, cfg *Config, client discovery.SvcDiscoveryRegist
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := &rtcServer{
|
s := &rtcServer{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
db: controller.NewRtcDatabase(signalDB),
|
db: controller.NewRtcDatabase(signalDB),
|
||||||
roomClient: roomClient,
|
roomClient: roomClient,
|
||||||
msgClient: rpcli.NewMsgClient(msgConn),
|
msgClient: rpcli.NewMsgClient(msgConn),
|
||||||
userClient: rpcli.NewUserClient(userConn),
|
userClient: rpcli.NewUserClient(userConn),
|
||||||
tokenExpiry: tokenExpiry,
|
relationClient: rpcli.NewRelationClient(friendConn),
|
||||||
|
tokenExpiry: tokenExpiry,
|
||||||
}
|
}
|
||||||
|
|
||||||
rtc.RegisterRtcServiceServer(server, s)
|
rtc.RegisterRtcServiceServer(server, s)
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/openimsdk/tools/log"
|
"github.com/openimsdk/tools/log"
|
||||||
"github.com/openimsdk/tools/mcontext"
|
"github.com/openimsdk/tools/mcontext"
|
||||||
"github.com/openimsdk/tools/utils/datautil"
|
"github.com/openimsdk/tools/utils/datautil"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -76,11 +77,10 @@ func (s *rtcServer) SignalMessageAssemble(ctx context.Context, req *rtc.SignalMe
|
|||||||
resp.Payload = &rtc.SignalResp_GetTokenByRoomID{GetTokenByRoomID: r}
|
resp.Payload = &rtc.SignalResp_GetTokenByRoomID{GetTokenByRoomID: r}
|
||||||
respErr = err
|
respErr = err
|
||||||
default:
|
default:
|
||||||
log.ZError(ctx, "SignalMessageAssemble", respErr, "r", respErr.Error())
|
|
||||||
return nil, errs.ErrArgs.WrapMsg("unknown signal payload type")
|
return nil, errs.ErrArgs.WrapMsg("unknown signal payload type")
|
||||||
}
|
}
|
||||||
if respErr != nil {
|
if respErr != nil {
|
||||||
log.ZError(ctx, "SignalMessageAssemble", respErr, "r", respErr.Error())
|
log.ZError(ctx, "SignalMessageAssemble", respErr, "err", respErr.Error())
|
||||||
return nil, respErr
|
return nil, respErr
|
||||||
}
|
}
|
||||||
return &rtc.SignalMessageAssembleResp{SignalResp: &resp}, nil
|
return &rtc.SignalMessageAssembleResp{SignalResp: &resp}, nil
|
||||||
@ -93,12 +93,21 @@ func (s *rtcServer) handleInvite(ctx context.Context, req *rtc.SignalInviteReq,
|
|||||||
log.ZError(ctx, "handleInvite", errs.ErrArgs, "r", "invitation is nil")
|
log.ZError(ctx, "handleInvite", errs.ErrArgs, "r", "invitation is nil")
|
||||||
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
||||||
}
|
}
|
||||||
if inv.RoomID == "" {
|
inv.RoomID = newRoomID()
|
||||||
inv.RoomID = newRoomID()
|
|
||||||
}
|
|
||||||
inv.InviterUserID = req.UserID
|
inv.InviterUserID = req.UserID
|
||||||
inv.InitiateTime = time.Now().UnixMilli()
|
inv.InitiateTime = time.Now().UnixMilli()
|
||||||
|
|
||||||
|
for _, inviteeID := range inv.InviteeUserIDList {
|
||||||
|
allowed, err := s.isCallAllowed(ctx, req.UserID, inviteeID)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(ctx, "handleInvite: isCallAllowed failed", err, "inviteeID", inviteeID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
return nil, errs.ErrNoPermission.WrapMsg("the invitee does not accept calls from you", "inviteeID", inviteeID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := s.roomClient.CreateRoom(ctx, &livekit.CreateRoomRequest{Name: inv.RoomID}); err != nil {
|
if _, err := s.roomClient.CreateRoom(ctx, &livekit.CreateRoomRequest{Name: inv.RoomID}); err != nil {
|
||||||
log.ZError(ctx, "handleInvite", err, "r", err.Error())
|
log.ZError(ctx, "handleInvite", err, "r", err.Error())
|
||||||
return nil, errs.WrapMsg(err, "LiveKit CreateRoom failed", "roomID", inv.RoomID)
|
return nil, errs.WrapMsg(err, "LiveKit CreateRoom failed", "roomID", inv.RoomID)
|
||||||
@ -106,19 +115,33 @@ func (s *rtcServer) handleInvite(ctx context.Context, req *rtc.SignalInviteReq,
|
|||||||
|
|
||||||
token, err := s.genToken(inv.RoomID, req.UserID)
|
token, err := s.genToken(inv.RoomID, req.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ZError(ctx, "handleInvite", err, "r", err.Error())
|
if _, delErr := s.roomClient.DeleteRoom(ctx, &livekit.DeleteRoomRequest{Room: inv.RoomID}); delErr != nil {
|
||||||
|
log.ZWarn(ctx, "handleInvite: rollback DeleteRoom failed", delErr, "roomID", inv.RoomID)
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.CreateInvitation(ctx, invitationToModel(inv, req.OfflinePushInfo)); err != nil {
|
if err := s.db.CreateInvitation(ctx, invitationToModel(inv, req.OfflinePushInfo)); err != nil {
|
||||||
log.ZWarn(ctx, "CreateInvitation failed", err, "roomID", inv.RoomID)
|
if mongo.IsDuplicateKeyError(err) {
|
||||||
|
log.ZWarn(ctx, "handleInvite: duplicate invitation (idempotent retry)", err, "roomID", inv.RoomID)
|
||||||
|
} else {
|
||||||
|
if _, delErr := s.roomClient.DeleteRoom(ctx, &livekit.DeleteRoomRequest{Room: inv.RoomID}); delErr != nil {
|
||||||
|
log.ZWarn(ctx, "handleInvite: rollback DeleteRoom failed", delErr, "roomID", inv.RoomID)
|
||||||
|
}
|
||||||
|
return nil, errs.WrapMsg(err, "CreateInvitation failed", "roomID", inv.RoomID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := marshalSignalReq(signalReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
content := marshalSignalReq(signalReq)
|
|
||||||
for _, inviteeID := range inv.InviteeUserIDList {
|
for _, inviteeID := range inv.InviteeUserIDList {
|
||||||
log.ZInfo(ctx, "sendSignalingNotification to invitee", "sendID", req.UserID, "recvID", inviteeID)
|
log.ZInfo(ctx, "sendSignalingNotification to invitee", "sendID", req.UserID, "recvID", inviteeID)
|
||||||
if err := s.sendSignalingNotification(ctx, req.UserID, inviteeID, int32(constant.SingleChatType), req.OfflinePushInfo, content); err != nil {
|
if err := s.sendSignalingNotification(ctx, req.UserID, inviteeID, int32(constant.SingleChatType), req.OfflinePushInfo, content); err != nil {
|
||||||
log.ZWarn(ctx, "sendSignalingNotification to invitee failed", err, "inviteeID", inviteeID)
|
log.ZError(ctx, "sendSignalingNotification to invitee failed", err, "inviteeID", inviteeID)
|
||||||
|
return nil, errs.WrapMsg(err, "failed to notify invitee", "inviteeID", inviteeID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,9 +159,8 @@ func (s *rtcServer) handleInviteInGroup(ctx context.Context, req *rtc.SignalInvi
|
|||||||
if inv == nil {
|
if inv == nil {
|
||||||
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
||||||
}
|
}
|
||||||
if inv.RoomID == "" {
|
|
||||||
inv.RoomID = newRoomID()
|
inv.RoomID = newRoomID()
|
||||||
}
|
|
||||||
inv.InviterUserID = req.UserID
|
inv.InviterUserID = req.UserID
|
||||||
inv.InitiateTime = time.Now().UnixMilli()
|
inv.InitiateTime = time.Now().UnixMilli()
|
||||||
|
|
||||||
@ -148,15 +170,36 @@ func (s *rtcServer) handleInviteInGroup(ctx context.Context, req *rtc.SignalInvi
|
|||||||
|
|
||||||
token, err := s.genToken(inv.RoomID, req.UserID)
|
token, err := s.genToken(inv.RoomID, req.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if _, delErr := s.roomClient.DeleteRoom(ctx, &livekit.DeleteRoomRequest{Room: inv.RoomID}); delErr != nil {
|
||||||
|
log.ZWarn(ctx, "handleInviteInGroup: rollback DeleteRoom failed", delErr, "roomID", inv.RoomID)
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.CreateInvitation(ctx, invitationToModel(inv, req.OfflinePushInfo)); err != nil {
|
if err := s.db.CreateInvitation(ctx, invitationToModel(inv, req.OfflinePushInfo)); err != nil {
|
||||||
log.ZWarn(ctx, "CreateInvitation failed", err, "roomID", inv.RoomID)
|
if !mongo.IsDuplicateKeyError(err) {
|
||||||
|
if _, delErr := s.roomClient.DeleteRoom(ctx, &livekit.DeleteRoomRequest{Room: inv.RoomID}); delErr != nil {
|
||||||
|
log.ZWarn(ctx, "handleInviteInGroup: rollback DeleteRoom failed", delErr, "roomID", inv.RoomID)
|
||||||
|
}
|
||||||
|
return nil, errs.WrapMsg(err, "CreateInvitation failed", "roomID", inv.RoomID)
|
||||||
|
}
|
||||||
|
log.ZWarn(ctx, "handleInviteInGroup: duplicate invitation (idempotent retry)", err, "roomID", inv.RoomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
content := marshalSignalReq(signalReq)
|
content, err := marshalSignalReq(signalReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
for _, inviteeID := range inv.InviteeUserIDList {
|
for _, inviteeID := range inv.InviteeUserIDList {
|
||||||
|
allowed, err := s.isCallAllowed(ctx, req.UserID, inviteeID)
|
||||||
|
if err != nil {
|
||||||
|
log.ZWarn(ctx, "handleInviteInGroup: isCallAllowed failed, skipping invitee", err, "inviteeID", inviteeID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
log.ZInfo(ctx, "handleInviteInGroup: skipping invitee (call setting blocked)", "inviteeID", inviteeID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err := s.sendSignalingNotification(ctx, req.UserID, inviteeID, int32(constant.ReadGroupChatType), req.OfflinePushInfo, content); err != nil {
|
if err := s.sendSignalingNotification(ctx, req.UserID, inviteeID, int32(constant.ReadGroupChatType), req.OfflinePushInfo, content); err != nil {
|
||||||
log.ZWarn(ctx, "sendSignalingNotification to group invitee failed", err, "inviteeID", inviteeID)
|
log.ZWarn(ctx, "sendSignalingNotification to group invitee failed", err, "inviteeID", inviteeID)
|
||||||
}
|
}
|
||||||
@ -169,57 +212,110 @@ func (s *rtcServer) handleInviteInGroup(ctx context.Context, req *rtc.SignalInvi
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAccept processes a call acceptance.
|
// isCallAllowed 判断 inviterID 是否被允许向 inviteeID 发起音视频通话。
|
||||||
|
// 规则:
|
||||||
|
// - CallAcceptSettingPublic(0) → 所有人均可
|
||||||
|
// - CallAcceptSettingFriends(1) → 仅当 inviterID 在 inviteeID 好友列表中
|
||||||
|
// - CallAcceptSettingNobody(2) → 任何人均不可
|
||||||
|
func (s *rtcServer) isCallAllowed(ctx context.Context, inviterID, inviteeID string) (bool, error) {
|
||||||
|
userInfo, err := s.userClient.GetUserInfo(ctx, inviteeID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
switch userInfo.CallAcceptSetting {
|
||||||
|
case model.CallAcceptSettingNobody:
|
||||||
|
return false, nil
|
||||||
|
case model.CallAcceptSettingFriends:
|
||||||
|
isFriend, err := s.relationClient.IsFriend(ctx, inviteeID, inviterID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return isFriend, nil
|
||||||
|
default: // CallAcceptSettingPublic
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *rtcServer) handleAccept(ctx context.Context, req *rtc.SignalAcceptReq, signalReq *rtc.SignalReq) (*rtc.SignalAcceptResp, error) {
|
func (s *rtcServer) handleAccept(ctx context.Context, req *rtc.SignalAcceptReq, signalReq *rtc.SignalReq) (*rtc.SignalAcceptResp, error) {
|
||||||
inv := req.Invitation
|
if req.Invitation == nil {
|
||||||
if inv == nil {
|
|
||||||
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := s.genToken(inv.RoomID, req.UserID)
|
// 从 DB 获取权威邀请数据,验证邀请存在且 userID 在被邀请人列表中
|
||||||
|
dbInv, err := s.db.GetInvitationByRoomID(ctx, req.Invitation.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.WrapMsg(err, "invitation not found or expired", "roomID", req.Invitation.RoomID)
|
||||||
|
}
|
||||||
|
if !datautil.Contain(req.UserID, dbInv.InviteeUserIDList...) {
|
||||||
|
return nil, errs.ErrNoPermission.WrapMsg("user not in invitee list", "userID", req.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := s.genToken(dbInv.RoomID, req.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionType := int32(constant.SingleChatType)
|
sessionType := int32(constant.SingleChatType)
|
||||||
if inv.GroupID != "" {
|
if dbInv.GroupID != "" {
|
||||||
sessionType = int32(constant.ReadGroupChatType)
|
sessionType = int32(constant.ReadGroupChatType)
|
||||||
}
|
}
|
||||||
content := marshalSignalReq(signalReq)
|
|
||||||
if err := s.sendSignalingNotification(ctx, req.UserID, inv.InviterUserID, sessionType, req.OfflinePushInfo, content); err != nil {
|
content, err := marshalSignalReq(signalReq)
|
||||||
log.ZWarn(ctx, "sendSignalingNotification accept to inviter failed", err, "inviterID", inv.InviterUserID)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.sendSignalingNotification(ctx, req.UserID, dbInv.InviterUserID, sessionType, req.OfflinePushInfo, content); err != nil {
|
||||||
|
log.ZWarn(ctx, "sendSignalingNotification accept to inviter failed", err, "inviterID", dbInv.InviterUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 群通话可通过 RemoveInvitee 实现精细化状态管理
|
||||||
|
if dbInv.GroupID == "" {
|
||||||
|
if err := s.db.DeleteInvitation(ctx, dbInv.RoomID); err != nil {
|
||||||
|
log.ZWarn(ctx, "handleAccept: DeleteInvitation failed (non-fatal)", err, "roomID", dbInv.RoomID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &rtc.SignalAcceptResp{
|
return &rtc.SignalAcceptResp{
|
||||||
Token: token,
|
Token: token,
|
||||||
RoomID: inv.RoomID,
|
RoomID: dbInv.RoomID,
|
||||||
LiveURL: s.config.RpcConfig.LiveKit.ExternalAddress,
|
LiveURL: s.config.RpcConfig.LiveKit.ExternalAddress,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleReject processes a call rejection.
|
// handleReject processes a call rejection.
|
||||||
func (s *rtcServer) handleReject(ctx context.Context, req *rtc.SignalRejectReq, signalReq *rtc.SignalReq) (*rtc.SignalRejectResp, error) {
|
func (s *rtcServer) handleReject(ctx context.Context, req *rtc.SignalRejectReq, signalReq *rtc.SignalReq) (*rtc.SignalRejectResp, error) {
|
||||||
inv := req.Invitation
|
if req.Invitation == nil {
|
||||||
if inv == nil {
|
|
||||||
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionType := int32(constant.SingleChatType)
|
dbInv, err := s.db.GetInvitationByRoomID(ctx, req.Invitation.RoomID)
|
||||||
if inv.GroupID != "" {
|
if err != nil {
|
||||||
sessionType = int32(constant.ReadGroupChatType)
|
return nil, errs.WrapMsg(err, "invitation not found or expired", "roomID", req.Invitation.RoomID)
|
||||||
}
|
}
|
||||||
content := marshalSignalReq(signalReq)
|
if !datautil.Contain(req.UserID, dbInv.InviteeUserIDList...) {
|
||||||
if err := s.sendSignalingNotification(ctx, req.UserID, inv.InviterUserID, sessionType, req.OfflinePushInfo, content); err != nil {
|
return nil, errs.ErrNoPermission.WrapMsg("user not in invitee list", "userID", req.UserID)
|
||||||
log.ZWarn(ctx, "sendSignalingNotification reject to inviter failed", err, "inviterID", inv.InviterUserID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if inv.GroupID != "" {
|
sessionType := int32(constant.SingleChatType)
|
||||||
if err := s.db.RemoveInvitee(ctx, inv.RoomID, req.UserID); err != nil {
|
if dbInv.GroupID != "" {
|
||||||
log.ZWarn(ctx, "RemoveInvitee failed", err, "roomID", inv.RoomID, "userID", req.UserID)
|
sessionType = int32(constant.ReadGroupChatType)
|
||||||
|
}
|
||||||
|
content, err := marshalSignalReq(signalReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := s.sendSignalingNotification(ctx, req.UserID, dbInv.InviterUserID, sessionType, req.OfflinePushInfo, content); err != nil {
|
||||||
|
log.ZWarn(ctx, "sendSignalingNotification reject to inviter failed", err, "inviterID", dbInv.InviterUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbInv.GroupID != "" {
|
||||||
|
if err := s.db.RemoveInvitee(ctx, dbInv.RoomID, req.UserID); err != nil {
|
||||||
|
log.ZWarn(ctx, "RemoveInvitee failed", err, "roomID", dbInv.RoomID, "userID", req.UserID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := s.db.DeleteInvitation(ctx, inv.RoomID); err != nil {
|
if err := s.db.DeleteInvitation(ctx, dbInv.RoomID); err != nil {
|
||||||
log.ZWarn(ctx, "DeleteInvitation failed", err, "roomID", inv.RoomID)
|
log.ZWarn(ctx, "DeleteInvitation failed", err, "roomID", dbInv.RoomID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,24 +324,34 @@ func (s *rtcServer) handleReject(ctx context.Context, req *rtc.SignalRejectReq,
|
|||||||
|
|
||||||
// handleCancel processes a call cancellation.
|
// handleCancel processes a call cancellation.
|
||||||
func (s *rtcServer) handleCancel(ctx context.Context, req *rtc.SignalCancelReq, signalReq *rtc.SignalReq) (*rtc.SignalCancelResp, error) {
|
func (s *rtcServer) handleCancel(ctx context.Context, req *rtc.SignalCancelReq, signalReq *rtc.SignalReq) (*rtc.SignalCancelResp, error) {
|
||||||
inv := req.Invitation
|
if req.Invitation == nil {
|
||||||
if inv == nil {
|
|
||||||
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbInv, err := s.db.GetInvitationByRoomID(ctx, req.Invitation.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.WrapMsg(err, "invitation not found or expired", "roomID", req.Invitation.RoomID)
|
||||||
|
}
|
||||||
|
if req.UserID != dbInv.InviterUserID {
|
||||||
|
return nil, errs.ErrNoPermission.WrapMsg("only the inviter can cancel", "userID", req.UserID, "inviterUserID", dbInv.InviterUserID)
|
||||||
|
}
|
||||||
|
|
||||||
sessionType := int32(constant.SingleChatType)
|
sessionType := int32(constant.SingleChatType)
|
||||||
if inv.GroupID != "" {
|
if dbInv.GroupID != "" {
|
||||||
sessionType = int32(constant.ReadGroupChatType)
|
sessionType = int32(constant.ReadGroupChatType)
|
||||||
}
|
}
|
||||||
content := marshalSignalReq(signalReq)
|
content, err := marshalSignalReq(signalReq)
|
||||||
for _, inviteeID := range inv.InviteeUserIDList {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, inviteeID := range dbInv.InviteeUserIDList {
|
||||||
if err := s.sendSignalingNotification(ctx, req.UserID, inviteeID, sessionType, req.OfflinePushInfo, content); err != nil {
|
if err := s.sendSignalingNotification(ctx, req.UserID, inviteeID, sessionType, req.OfflinePushInfo, content); err != nil {
|
||||||
log.ZWarn(ctx, "sendSignalingNotification cancel to invitee failed", err, "inviteeID", inviteeID)
|
log.ZWarn(ctx, "sendSignalingNotification cancel to invitee failed", err, "inviteeID", inviteeID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.DeleteInvitation(ctx, inv.RoomID); err != nil {
|
if err := s.db.DeleteInvitation(ctx, dbInv.RoomID); err != nil {
|
||||||
log.ZWarn(ctx, "DeleteInvitation failed", err, "roomID", inv.RoomID)
|
log.ZWarn(ctx, "DeleteInvitation failed", err, "roomID", dbInv.RoomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &rtc.SignalCancelResp{}, nil
|
return &rtc.SignalCancelResp{}, nil
|
||||||
@ -253,29 +359,40 @@ func (s *rtcServer) handleCancel(ctx context.Context, req *rtc.SignalCancelReq,
|
|||||||
|
|
||||||
// handleHungUp processes a call hang-up.
|
// handleHungUp processes a call hang-up.
|
||||||
func (s *rtcServer) handleHungUp(ctx context.Context, req *rtc.SignalHungUpReq, signalReq *rtc.SignalReq) (*rtc.SignalHungUpResp, error) {
|
func (s *rtcServer) handleHungUp(ctx context.Context, req *rtc.SignalHungUpReq, signalReq *rtc.SignalReq) (*rtc.SignalHungUpResp, error) {
|
||||||
inv := req.Invitation
|
if req.Invitation == nil {
|
||||||
if inv == nil {
|
|
||||||
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
return nil, errs.ErrArgs.WrapMsg("invitation is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbInv, err := s.db.GetInvitationByRoomID(ctx, req.Invitation.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.WrapMsg(err, "invitation not found or expired", "roomID", req.Invitation.RoomID)
|
||||||
|
}
|
||||||
|
if req.UserID != dbInv.InviterUserID && !datautil.Contain(req.UserID, dbInv.InviteeUserIDList...) {
|
||||||
|
return nil, errs.ErrNoPermission.WrapMsg("user is not a participant of this call", "userID", req.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
sessionType := int32(constant.SingleChatType)
|
sessionType := int32(constant.SingleChatType)
|
||||||
if inv.GroupID != "" {
|
if dbInv.GroupID != "" {
|
||||||
sessionType = int32(constant.ReadGroupChatType)
|
sessionType = int32(constant.ReadGroupChatType)
|
||||||
}
|
}
|
||||||
content := marshalSignalReq(signalReq)
|
content, err := marshalSignalReq(signalReq)
|
||||||
for _, peerID := range hungUpPeerIDs(inv, req.UserID) {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 使用 DB 中的参与者列表,不信任客户端传入的 InviteeUserIDList
|
||||||
|
for _, peerID := range hungUpPeerIDsFromDB(dbInv, req.UserID) {
|
||||||
if err := s.sendSignalingNotification(ctx, req.UserID, peerID, sessionType, req.OfflinePushInfo, content); err != nil {
|
if err := s.sendSignalingNotification(ctx, req.UserID, peerID, sessionType, req.OfflinePushInfo, content); err != nil {
|
||||||
log.ZWarn(ctx, "sendSignalingNotification hungUp to peer failed", err, "peerID", peerID)
|
log.ZWarn(ctx, "sendSignalingNotification hungUp to peer failed", err, "peerID", peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terminate the LiveKit room
|
// Terminate the LiveKit room
|
||||||
if _, err := s.roomClient.DeleteRoom(ctx, &livekit.DeleteRoomRequest{Room: inv.RoomID}); err != nil {
|
if _, err := s.roomClient.DeleteRoom(ctx, &livekit.DeleteRoomRequest{Room: dbInv.RoomID}); err != nil {
|
||||||
log.ZWarn(ctx, "LiveKit DeleteRoom failed", err, "roomID", inv.RoomID)
|
log.ZWarn(ctx, "LiveKit DeleteRoom failed", err, "roomID", dbInv.RoomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.DeleteInvitation(ctx, inv.RoomID); err != nil {
|
if err := s.db.DeleteInvitation(ctx, dbInv.RoomID); err != nil {
|
||||||
log.ZWarn(ctx, "DeleteInvitation failed", err, "roomID", inv.RoomID)
|
log.ZWarn(ctx, "DeleteInvitation failed", err, "roomID", dbInv.RoomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &rtc.SignalHungUpResp{}, nil
|
return &rtc.SignalHungUpResp{}, nil
|
||||||
@ -283,6 +400,14 @@ func (s *rtcServer) handleHungUp(ctx context.Context, req *rtc.SignalHungUpReq,
|
|||||||
|
|
||||||
// handleGetTokenByRoomID returns a LiveKit token for an existing room.
|
// handleGetTokenByRoomID returns a LiveKit token for an existing room.
|
||||||
func (s *rtcServer) handleGetTokenByRoomID(ctx context.Context, req *rtc.SignalGetTokenByRoomIDReq) (*rtc.SignalGetTokenByRoomIDResp, error) {
|
func (s *rtcServer) handleGetTokenByRoomID(ctx context.Context, req *rtc.SignalGetTokenByRoomIDReq) (*rtc.SignalGetTokenByRoomIDResp, error) {
|
||||||
|
dbInv, err := s.db.GetInvitationByRoomID(ctx, req.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.WrapMsg(err, "room not found or expired", "roomID", req.RoomID)
|
||||||
|
}
|
||||||
|
if req.UserID != dbInv.InviterUserID && !datautil.Contain(req.UserID, dbInv.InviteeUserIDList...) {
|
||||||
|
return nil, errs.ErrNoPermission.WrapMsg("user is not a participant of this room", "userID", req.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
token, err := s.genToken(req.RoomID, req.UserID)
|
token, err := s.genToken(req.RoomID, req.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -306,7 +431,16 @@ func (s *rtcServer) SignalGetRoomByGroupID(ctx context.Context, req *rtc.SignalG
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SignalGetTokenByRoomID returns a token for joining a room directly (HTTP API path).
|
// SignalGetTokenByRoomID returns a token for joining a room directly (HTTP API path).
|
||||||
|
// Fix P0(安全): 同 handleGetTokenByRoomID,添加参与者身份校验。
|
||||||
func (s *rtcServer) SignalGetTokenByRoomID(ctx context.Context, req *rtc.SignalGetTokenByRoomIDReq) (*rtc.SignalGetTokenByRoomIDResp, error) {
|
func (s *rtcServer) SignalGetTokenByRoomID(ctx context.Context, req *rtc.SignalGetTokenByRoomIDReq) (*rtc.SignalGetTokenByRoomIDResp, error) {
|
||||||
|
dbInv, err := s.db.GetInvitationByRoomID(ctx, req.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.WrapMsg(err, "room not found or expired", "roomID", req.RoomID)
|
||||||
|
}
|
||||||
|
if req.UserID != dbInv.InviterUserID && !datautil.Contain(req.UserID, dbInv.InviteeUserIDList...) {
|
||||||
|
return nil, errs.ErrNoPermission.WrapMsg("user is not a participant of this room", "userID", req.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
token, err := s.genToken(req.RoomID, req.UserID)
|
token, err := s.genToken(req.RoomID, req.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -376,10 +510,14 @@ func (s *rtcServer) SignalSendCustomSignal(ctx context.Context, req *rtc.SignalS
|
|||||||
return &rtc.SignalSendCustomSignalResp{}, nil
|
return &rtc.SignalSendCustomSignalResp{}, nil
|
||||||
}
|
}
|
||||||
opUserID := mcontext.GetOpUserID(ctx)
|
opUserID := mcontext.GetOpUserID(ctx)
|
||||||
content, _ := json.Marshal(map[string]any{
|
// Fix P3: 处理 json.Marshal 错误
|
||||||
|
content, err := json.Marshal(map[string]any{
|
||||||
"roomID": req.RoomID,
|
"roomID": req.RoomID,
|
||||||
"customInfo": req.CustomInfo,
|
"customInfo": req.CustomInfo,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.WrapMsg(err, "marshal custom signal content failed")
|
||||||
|
}
|
||||||
recipients := make([]string, 0, len(inv.InviteeUserIDList)+1)
|
recipients := make([]string, 0, len(inv.InviteeUserIDList)+1)
|
||||||
recipients = append(recipients, inv.InviteeUserIDList...)
|
recipients = append(recipients, inv.InviteeUserIDList...)
|
||||||
recipients = append(recipients, inv.InviterUserID)
|
recipients = append(recipients, inv.InviterUserID)
|
||||||
@ -447,6 +585,29 @@ func (s *rtcServer) genToken(roomID, userID string) (string, error) {
|
|||||||
return at.ToJWT()
|
return at.ToJWT()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// signalingMsgOptions 返回信令通知消息应设置的 Options。
|
||||||
|
//
|
||||||
|
// Fix P2+P2(安全): 原代码传 make(map[string]bool) 空 map,导致:
|
||||||
|
// 1. IsNotificationByMsg 将信令消息误判为普通聊天消息,触发黑名单/好友关系等权限拦截
|
||||||
|
// 2. IsHistory/IsPersistent 默认为 true,信令消息被写入历史记录占用存储
|
||||||
|
// 3. IsUnreadCount/IsConversationUpdate 默认 true,污染未读数和会话列表
|
||||||
|
//
|
||||||
|
// 信令消息应走 Notification 通道(对话 ID 前缀 "n_"),绕过聊天消息权限校验,
|
||||||
|
// 且不写历史、不计未读、不更新会话。离线推送根据 offlinePushInfo 控制,此处不强制关闭。
|
||||||
|
func signalingMsgOptions() map[string]bool {
|
||||||
|
opts := make(map[string]bool, 8)
|
||||||
|
// IsNotNotification=false 表示"这是通知消息",让 IsNotificationByMsg 返回 true
|
||||||
|
// 从而跳过 modifyMessageByUserMessageReceiveOpt 中的黑名单/好友关系等校验
|
||||||
|
datautil.SetSwitchFromOptions(opts, constant.IsNotNotification, false)
|
||||||
|
datautil.SetSwitchFromOptions(opts, constant.IsHistory, false)
|
||||||
|
datautil.SetSwitchFromOptions(opts, constant.IsPersistent, false)
|
||||||
|
datautil.SetSwitchFromOptions(opts, constant.IsUnreadCount, false)
|
||||||
|
datautil.SetSwitchFromOptions(opts, constant.IsConversationUpdate, false)
|
||||||
|
datautil.SetSwitchFromOptions(opts, constant.IsSenderConversationUpdate, false)
|
||||||
|
datautil.SetSwitchFromOptions(opts, constant.IsSenderSync, false)
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
// sendSignalingNotification sends a SignalingNotification message to a user via the msg service.
|
// sendSignalingNotification sends a SignalingNotification message to a user via the msg service.
|
||||||
func (s *rtcServer) sendSignalingNotification(ctx context.Context, sendID, recvID string, sessionType int32, offlinePush *sdkws.OfflinePushInfo, content []byte) error {
|
func (s *rtcServer) sendSignalingNotification(ctx context.Context, sendID, recvID string, sessionType int32, offlinePush *sdkws.OfflinePushInfo, content []byte) error {
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
@ -461,7 +622,7 @@ func (s *rtcServer) sendSignalingNotification(ctx context.Context, sendID, recvI
|
|||||||
SendTime: now,
|
SendTime: now,
|
||||||
ServerMsgID: uuid.New().String(),
|
ServerMsgID: uuid.New().String(),
|
||||||
ClientMsgID: uuid.New().String(),
|
ClientMsgID: uuid.New().String(),
|
||||||
Options: make(map[string]bool),
|
Options: signalingMsgOptions(),
|
||||||
}
|
}
|
||||||
if offlinePush != nil {
|
if offlinePush != nil {
|
||||||
msgData.OfflinePushInfo = offlinePush
|
msgData.OfflinePushInfo = offlinePush
|
||||||
@ -491,15 +652,20 @@ func (s *rtcServer) sendCustomSignalNotification(ctx context.Context, sendID, re
|
|||||||
SendTime: now,
|
SendTime: now,
|
||||||
ServerMsgID: uuid.New().String(),
|
ServerMsgID: uuid.New().String(),
|
||||||
ClientMsgID: uuid.New().String(),
|
ClientMsgID: uuid.New().String(),
|
||||||
Options: make(map[string]bool),
|
Options: signalingMsgOptions(),
|
||||||
}
|
}
|
||||||
_, err := s.msgClient.MsgClient.SendMsg(ctx, &pbmsg.SendMsgReq{MsgData: msgData})
|
_, err := s.msgClient.MsgClient.SendMsg(ctx, &pbmsg.SendMsgReq{MsgData: msgData})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshalSignalReq(req *rtc.SignalReq) []byte {
|
// marshalSignalReq serializes a SignalReq to bytes.
|
||||||
b, _ := proto.Marshal(req)
|
// Fix P2: 原代码使用 _ 吞掉错误,序列化失败时返回 nil,导致被叫收到空 Content 消息,来电通知丢失。
|
||||||
return b
|
func marshalSignalReq(req *rtc.SignalReq) ([]byte, error) {
|
||||||
|
b, err := proto.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.WrapMsg(err, "marshal SignalReq failed")
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newRoomID generates a unique room ID.
|
// newRoomID generates a unique room ID.
|
||||||
@ -509,6 +675,7 @@ func newRoomID() string {
|
|||||||
|
|
||||||
// invitationToModel converts a proto InvitationInfo to the database model.
|
// invitationToModel converts a proto InvitationInfo to the database model.
|
||||||
func invitationToModel(inv *rtc.InvitationInfo, push *sdkws.OfflinePushInfo) *model.SignalInvitation {
|
func invitationToModel(inv *rtc.InvitationInfo, push *sdkws.OfflinePushInfo) *model.SignalInvitation {
|
||||||
|
now := time.Now()
|
||||||
m := &model.SignalInvitation{
|
m := &model.SignalInvitation{
|
||||||
RoomID: inv.RoomID,
|
RoomID: inv.RoomID,
|
||||||
InviterUserID: inv.InviterUserID,
|
InviterUserID: inv.InviterUserID,
|
||||||
@ -521,7 +688,8 @@ func invitationToModel(inv *rtc.InvitationInfo, push *sdkws.OfflinePushInfo) *mo
|
|||||||
SessionType: inv.SessionType,
|
SessionType: inv.SessionType,
|
||||||
InitiateTime: inv.InitiateTime,
|
InitiateTime: inv.InitiateTime,
|
||||||
BusyLineUserIDList: inv.BusyLineUserIDList,
|
BusyLineUserIDList: inv.BusyLineUserIDList,
|
||||||
CreateTime: time.Now().UnixMilli(),
|
CreateTime: now.UnixMilli(),
|
||||||
|
ExpireAt: now.Add(time.Duration(inv.Timeout+30) * time.Second),
|
||||||
}
|
}
|
||||||
if push != nil {
|
if push != nil {
|
||||||
m.OfflinePushTitle = push.Title
|
m.OfflinePushTitle = push.Title
|
||||||
@ -551,8 +719,8 @@ func modelToInvitationInfo(m *model.SignalInvitation) *rtc.InvitationInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hungUpPeerIDs returns the IDs that should receive hang-up notification.
|
// hungUpPeerIDsFromDB returns IDs that should receive hang-up notification, based on authoritative DB data.
|
||||||
func hungUpPeerIDs(inv *rtc.InvitationInfo, callerID string) []string {
|
func hungUpPeerIDsFromDB(inv *model.SignalInvitation, callerID string) []string {
|
||||||
if callerID == inv.InviterUserID {
|
if callerID == inv.InviterUserID {
|
||||||
return inv.InviteeUserIDList
|
return inv.InviteeUserIDList
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,9 +18,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/openimsdk/open-im-server/v3/internal/rpc/relation"
|
"github.com/openimsdk/open-im-server/v3/internal/rpc/relation"
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
|
||||||
@ -35,6 +37,7 @@ import (
|
|||||||
"github.com/openimsdk/protocol/group"
|
"github.com/openimsdk/protocol/group"
|
||||||
friendpb "github.com/openimsdk/protocol/relation"
|
friendpb "github.com/openimsdk/protocol/relation"
|
||||||
"github.com/openimsdk/tools/db/redisutil"
|
"github.com/openimsdk/tools/db/redisutil"
|
||||||
|
"github.com/openimsdk/tools/mcontext"
|
||||||
|
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/authverify"
|
"github.com/openimsdk/open-im-server/v3/pkg/authverify"
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/convert"
|
"github.com/openimsdk/open-im-server/v3/pkg/common/convert"
|
||||||
@ -47,10 +50,15 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// phoneRe 仅校验手机号的基本数字格式,不强制区号/国家码前缀。
|
||||||
|
// 规则:纯数字,长度 5-20 位,允许可选的 + 前缀(如 +86...)。
|
||||||
|
var phoneRe = regexp.MustCompile(`^\+?\d{5,20}$`)
|
||||||
|
|
||||||
type userServer struct {
|
type userServer struct {
|
||||||
pbuser.UnimplementedUserServer
|
pbuser.UnimplementedUserServer
|
||||||
online cache.OnlineCache
|
online cache.OnlineCache
|
||||||
@ -62,6 +70,7 @@ type userServer struct {
|
|||||||
webhookClient *webhook.Client
|
webhookClient *webhook.Client
|
||||||
groupClient *rpcli.GroupClient
|
groupClient *rpcli.GroupClient
|
||||||
relationClient *rpcli.RelationClient
|
relationClient *rpcli.RelationClient
|
||||||
|
globalBlackDB controller.UserGlobalBlackDatabase
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -109,6 +118,10 @@ func Start(ctx context.Context, config *Config, client registry.SvcDiscoveryRegi
|
|||||||
msgClient := rpcli.NewMsgClient(msgConn)
|
msgClient := rpcli.NewMsgClient(msgConn)
|
||||||
userCache := redis.NewUserCacheRedis(rdb, &config.LocalCacheConfig, userDB, redis.GetRocksCacheOptions())
|
userCache := redis.NewUserCacheRedis(rdb, &config.LocalCacheConfig, userDB, redis.GetRocksCacheOptions())
|
||||||
database := controller.NewUserDatabase(userDB, userCache, mgocli.GetTx())
|
database := controller.NewUserDatabase(userDB, userCache, mgocli.GetTx())
|
||||||
|
globalBlackMgo, err := mgo.NewUserGlobalBlackMongo(mgocli.GetDB())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
localcache.InitLocalCache(&config.LocalCacheConfig)
|
localcache.InitLocalCache(&config.LocalCacheConfig)
|
||||||
u := &userServer{
|
u := &userServer{
|
||||||
online: redis.NewUserOnline(rdb),
|
online: redis.NewUserOnline(rdb),
|
||||||
@ -121,6 +134,7 @@ func Start(ctx context.Context, config *Config, client registry.SvcDiscoveryRegi
|
|||||||
|
|
||||||
groupClient: rpcli.NewGroupClient(groupConn),
|
groupClient: rpcli.NewGroupClient(groupConn),
|
||||||
relationClient: rpcli.NewRelationClient(friendConn),
|
relationClient: rpcli.NewRelationClient(friendConn),
|
||||||
|
globalBlackDB: controller.NewUserGlobalBlackDatabase(globalBlackMgo),
|
||||||
}
|
}
|
||||||
pbuser.RegisterUserServer(server, u)
|
pbuser.RegisterUserServer(server, u)
|
||||||
return u.db.InitOnce(context.Background(), users)
|
return u.db.InitOnce(context.Background(), users)
|
||||||
@ -130,13 +144,71 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.UsersInfo = convert.UsersDB2Pb(users)
|
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
|
||||||
|
} else if len(blocked) > 0 {
|
||||||
|
bannedIDs := make([]string, 0, len(blocked))
|
||||||
|
for _, b := range blocked {
|
||||||
|
bannedIDs = append(bannedIDs, b.UserID)
|
||||||
|
}
|
||||||
|
return nil, servererrs.ErrUserBlocked.WrapMsg("user is banned", "userIDs", bannedIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pbUsers := convert.UsersDB2Pb(users)
|
||||||
|
viewerID := mcontext.GetOpUserID(ctx)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
resp.UsersInfo = pbUsers
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applyPhoneVisibility 根据 phone_visibility 和好友关系决定是否下发明文手机号。
|
||||||
|
// pbUsers 与 dbUsers 下标一一对应。
|
||||||
|
func (s *userServer) applyPhoneVisibility(ctx context.Context, viewerID string, pbUsers []*sdkws.UserInfo, dbUsers []*tablerelation.User) error {
|
||||||
|
for i, db := range dbUsers {
|
||||||
|
pb := pbUsers[i]
|
||||||
|
if db.Phone == "" {
|
||||||
|
// 未设置手机号,直接跳过
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch db.PhoneVisibility {
|
||||||
|
case tablerelation.PhoneVisibilityPublic:
|
||||||
|
// 所有人可见,保留 phone 字段(已由 UserDB2Pb 填充)
|
||||||
|
case tablerelation.PhoneVisibilityHidden:
|
||||||
|
// 完全隐藏:即使本人也不通过此接口暴露,客户端自行从个人设置接口获取
|
||||||
|
pb.Phone = ""
|
||||||
|
case tablerelation.PhoneVisibilityFriends:
|
||||||
|
// 仅好友可见
|
||||||
|
if viewerID == db.UserID {
|
||||||
|
// 本人始终可见
|
||||||
|
break
|
||||||
|
}
|
||||||
|
isFriend, err := s.relationClient.IsFriend(ctx, viewerID, db.UserID)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(ctx, "applyPhoneVisibility: IsFriend failed", err,
|
||||||
|
"viewerID", viewerID, "targetUserID", db.UserID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isFriend {
|
||||||
|
pb.Phone = ""
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
pb.Phone = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// deprecated:
|
// deprecated:
|
||||||
// UpdateUserInfo
|
// UpdateUserInfo
|
||||||
func (s *userServer) UpdateUserInfo(ctx context.Context, req *pbuser.UpdateUserInfoReq) (resp *pbuser.UpdateUserInfoResp, err error) {
|
func (s *userServer) UpdateUserInfo(ctx context.Context, req *pbuser.UpdateUserInfoReq) (resp *pbuser.UpdateUserInfoResp, err error) {
|
||||||
@ -221,6 +293,220 @@ func (s *userServer) SetGlobalRecvMessageOpt(ctx context.Context, req *pbuser.Se
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPhoneVisibility 设置手机号及其可见性(0=所有人,1=仅好友,2=隐藏)。
|
||||||
|
// 只允许本人或管理员操作。
|
||||||
|
func (s *userServer) SetPhoneVisibility(ctx context.Context, req *pbuser.SetPhoneVisibilityReq) (*pbuser.SetPhoneVisibilityResp, error) {
|
||||||
|
if req.UserID == "" {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("userID is required")
|
||||||
|
}
|
||||||
|
if req.PhoneVisibility < 0 || req.PhoneVisibility > 2 {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("phoneVisibility must be 0, 1 or 2")
|
||||||
|
}
|
||||||
|
if req.Phone != "" && !phoneRe.MatchString(req.Phone) {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("phone must contain digits only (5-20 digits), optionally prefixed with +")
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
m := map[string]any{
|
||||||
|
"phone_visibility": req.PhoneVisibility,
|
||||||
|
}
|
||||||
|
if req.Phone != "" {
|
||||||
|
m["phone"] = req.Phone
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserID)
|
||||||
|
return &pbuser.SetPhoneVisibilityResp{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCallAcceptSetting 设置音视频通话接受权限(0=所有人,1=仅好友,2=不接受任何通话)。
|
||||||
|
// 只允许本人或管理员操作。
|
||||||
|
func (s *userServer) SetCallAcceptSetting(ctx context.Context, req *pbuser.SetCallAcceptSettingReq) (*pbuser.SetCallAcceptSettingResp, error) {
|
||||||
|
if req.UserID == "" {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("userID is required")
|
||||||
|
}
|
||||||
|
if req.CallAcceptSetting < 0 || req.CallAcceptSetting > 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 {
|
||||||
|
log.ZWarn(ctx, "SetCallAcceptSetting: access denied", err,
|
||||||
|
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if err := s.db.UpdateByMap(ctx, req.UserID, map[string]any{
|
||||||
|
"call_accept_setting": req.CallAcceptSetting,
|
||||||
|
}); err != nil {
|
||||||
|
log.ZError(ctx, "SetCallAcceptSetting: UpdateByMap failed", err,
|
||||||
|
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID,
|
||||||
|
"callAcceptSetting", req.CallAcceptSetting)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserID)
|
||||||
|
return &pbuser.SetCallAcceptSettingResp{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMsgReceiveSetting 设置会话消息接收权限(0=所有人,1=仅好友,2=所有人不可发送)。
|
||||||
|
// 只允许本人或管理员操作。
|
||||||
|
func (s *userServer) SetMsgReceiveSetting(ctx context.Context, req *pbuser.SetMsgReceiveSettingReq) (*pbuser.SetMsgReceiveSettingResp, error) {
|
||||||
|
if req.UserID == "" {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("userID is required")
|
||||||
|
}
|
||||||
|
if req.MsgReceiveSetting < 0 || req.MsgReceiveSetting > 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 {
|
||||||
|
log.ZWarn(ctx, "SetMsgReceiveSetting: access denied", err,
|
||||||
|
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if err := s.db.UpdateByMap(ctx, req.UserID, map[string]any{
|
||||||
|
"msg_receive_setting": req.MsgReceiveSetting,
|
||||||
|
}); err != nil {
|
||||||
|
log.ZError(ctx, "SetMsgReceiveSetting: UpdateByMap failed", err,
|
||||||
|
"opUserID", mcontext.GetOpUserID(ctx), "targetUserID", req.UserID,
|
||||||
|
"msgReceiveSetting", req.MsgReceiveSetting)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserID)
|
||||||
|
return &pbuser.SetMsgReceiveSettingResp{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByPhone 根据精确手机号查询用户。
|
||||||
|
//
|
||||||
|
// phoneSearchVisibility=false(默认)时忽略 phone_visibility,任何人均可搜到。
|
||||||
|
// phoneSearchVisibility=true 时按 phone_visibility 过滤:
|
||||||
|
// - Hidden(2) → 非管理员不可搜到
|
||||||
|
// - Friends(1) → 仅好友/管理员可搜到
|
||||||
|
// - Public(0) → 任何人均可搜到
|
||||||
|
//
|
||||||
|
// 返回空 userInfo 并不代表错误,调用方应以 nil userInfo 判断"未找到"。
|
||||||
|
func (s *userServer) GetUserByPhone(ctx context.Context, req *pbuser.GetUserByPhoneReq) (*pbuser.GetUserByPhoneResp, error) {
|
||||||
|
if req.Phone == "" {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("phone is required")
|
||||||
|
}
|
||||||
|
if !phoneRe.MatchString(req.Phone) {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("phone must contain digits only (5-20 digits), optionally prefixed with +")
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUser, err := s.db.FindByPhone(ctx, req.Phone)
|
||||||
|
if err != nil {
|
||||||
|
if errs.ErrRecordNotFound.Is(err) {
|
||||||
|
// 手机号未注册,返回空响应而非错误,避免枚举攻击
|
||||||
|
return &pbuser.GetUserByPhoneResp{}, nil
|
||||||
|
}
|
||||||
|
log.ZError(ctx, "GetUserByPhone: FindByPhone failed", err,
|
||||||
|
"opUserID", mcontext.GetOpUserID(ctx), "phone", req.Phone)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅在 phoneSearchVisibility=true 时才按 phone_visibility 过滤,默认跳过
|
||||||
|
if s.config.RpcConfig.PhoneSearchVisibility {
|
||||||
|
callerID := mcontext.GetOpUserID(ctx)
|
||||||
|
isAdmin := datautil.Contain(callerID, s.config.Share.IMAdminUserID...)
|
||||||
|
|
||||||
|
switch dbUser.PhoneVisibility {
|
||||||
|
case tablerelation.PhoneVisibilityHidden:
|
||||||
|
// 完全隐藏:非管理员无法通过手机号搜到该用户
|
||||||
|
if !isAdmin {
|
||||||
|
return &pbuser.GetUserByPhoneResp{}, nil
|
||||||
|
}
|
||||||
|
case tablerelation.PhoneVisibilityFriends:
|
||||||
|
// 仅好友可搜索
|
||||||
|
if !isAdmin && callerID != dbUser.UserID {
|
||||||
|
isFriend, err := s.relationClient.IsFriend(ctx, callerID, dbUser.UserID)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(ctx, "GetUserByPhone: IsFriend failed", err,
|
||||||
|
"callerID", callerID, "targetUserID", dbUser.UserID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isFriend {
|
||||||
|
return &pbuser.GetUserByPhoneResp{}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pbUser := convert.UserDB2Pb(dbUser)
|
||||||
|
return &pbuser.GetUserByPhoneResp{UserInfo: pbUser}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsersByNickname 按昵称精确匹配查询普通用户(app_manger_level 与分页拉取用户一致)。
|
||||||
|
// 全局黑名单用户会被过滤;手机号字段按 phone_visibility 与 getDesignateUsers 相同规则处理。
|
||||||
|
func (s *userServer) GetUsersByNickname(ctx context.Context, req *pbuser.GetUsersByNicknameReq) (*pbuser.GetUsersByNicknameResp, error) {
|
||||||
|
nickname := strings.TrimSpace(req.Nickname)
|
||||||
|
if nickname == "" {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("nickname is required")
|
||||||
|
}
|
||||||
|
if n := utf8.RuneCountInString(nickname); n < 1 || n > 64 {
|
||||||
|
return nil, errs.ErrArgs.WrapMsg("nickname length must be 1-64 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
users, err := s.db.FindOrdinaryUsersByNickname(ctx, constant.IMOrdinaryUser, constant.AppOrdinaryUsers, nickname)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(ctx, "GetUsersByNickname: FindOrdinaryUsersByNickname failed", err,
|
||||||
|
"opUserID", mcontext.GetOpUserID(ctx), "nickname", nickname)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(users) == 0 {
|
||||||
|
return &pbuser.GetUsersByNicknameResp{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDs := datautil.Slice(users, func(u *tablerelation.User) string { return u.UserID })
|
||||||
|
blocked, err := s.globalBlackDB.FindBlocked(ctx, userIDs)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(ctx, "GetUsersByNickname: FindBlocked failed", err,
|
||||||
|
"opUserID", mcontext.GetOpUserID(ctx), "count", len(userIDs))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(blocked) > 0 {
|
||||||
|
banned := make(map[string]struct{}, len(blocked))
|
||||||
|
for _, b := range blocked {
|
||||||
|
banned[b.UserID] = struct{}{}
|
||||||
|
}
|
||||||
|
filtered := make([]*tablerelation.User, 0, len(users))
|
||||||
|
for _, u := range users {
|
||||||
|
if _, ok := banned[u.UserID]; !ok {
|
||||||
|
filtered = append(filtered, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
users = filtered
|
||||||
|
}
|
||||||
|
if len(users) == 0 {
|
||||||
|
return &pbuser.GetUsersByNicknameResp{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pbUsers := convert.UsersDB2Pb(users)
|
||||||
|
viewerID := mcontext.GetOpUserID(ctx)
|
||||||
|
if err := s.applyPhoneVisibility(ctx, viewerID, pbUsers, users); err != nil {
|
||||||
|
log.ZError(ctx, "GetUsersByNickname: applyPhoneVisibility failed", err,
|
||||||
|
"opUserID", viewerID, "count", len(users))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pbuser.GetUsersByNicknameResp{UsersInfo: pbUsers}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *userServer) AccountCheck(ctx context.Context, req *pbuser.AccountCheckReq) (resp *pbuser.AccountCheckResp, err error) {
|
func (s *userServer) AccountCheck(ctx context.Context, req *pbuser.AccountCheckReq) (resp *pbuser.AccountCheckResp, err error) {
|
||||||
resp = &pbuser.AccountCheckResp{}
|
resp = &pbuser.AccountCheckResp{}
|
||||||
if datautil.Duplicate(req.CheckUserIDs) {
|
if datautil.Duplicate(req.CheckUserIDs) {
|
||||||
|
|||||||
@ -364,6 +364,10 @@ type User struct {
|
|||||||
Ports []int `mapstructure:"ports"`
|
Ports []int `mapstructure:"ports"`
|
||||||
} `mapstructure:"rpc"`
|
} `mapstructure:"rpc"`
|
||||||
Prometheus Prometheus `mapstructure:"prometheus"`
|
Prometheus Prometheus `mapstructure:"prometheus"`
|
||||||
|
// PhoneSearchVisibility 控制 GetUserByPhone 是否尊重 phone_visibility 设置。
|
||||||
|
// false(默认):任何人均可通过手机号搜到用户,忽略 phone_visibility;
|
||||||
|
// true:按 phone_visibility 过滤(Hidden 不可搜,Friends 仅好友可搜)。
|
||||||
|
PhoneSearchVisibility bool `mapstructure:"phoneSearchVisibility"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Redis struct {
|
type Redis struct {
|
||||||
|
|||||||
@ -80,6 +80,9 @@ func FriendsDB2Pb(ctx context.Context, friendsDB []*model.Friend, getUsers func(
|
|||||||
friendPb.FriendUser.Ex = users[friend.FriendUserID].Ex
|
friendPb.FriendUser.Ex = users[friend.FriendUserID].Ex
|
||||||
friendPb.CreateTime = friend.CreateTime.Unix()
|
friendPb.CreateTime = friend.CreateTime.Unix()
|
||||||
friendPb.IsPinned = friend.IsPinned
|
friendPb.IsPinned = friend.IsPinned
|
||||||
|
friendPb.IsMute = friend.IsMuted
|
||||||
|
friendPb.MuteDuration = friend.MuteDuration
|
||||||
|
friendPb.MuteEndTime = friend.MuteEndTime
|
||||||
friendsPb = append(friendsPb, friendPb)
|
friendsPb = append(friendsPb, friendPb)
|
||||||
}
|
}
|
||||||
return friendsPb, nil
|
return friendsPb, nil
|
||||||
@ -96,6 +99,9 @@ func FriendOnlyDB2PbOnly(friendsDB []*model.Friend) []*relation.FriendInfoOnly {
|
|||||||
OperatorUserID: f.OperatorUserID,
|
OperatorUserID: f.OperatorUserID,
|
||||||
Ex: f.Ex,
|
Ex: f.Ex,
|
||||||
IsPinned: f.IsPinned,
|
IsPinned: f.IsPinned,
|
||||||
|
IsMute: f.IsMuted,
|
||||||
|
MuteDuration: f.MuteDuration,
|
||||||
|
MuteEndTime: f.MuteEndTime,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,10 @@ func UserDB2Pb(user *relationtb.User) *sdkws.UserInfo {
|
|||||||
CreateTime: user.CreateTime.UnixMilli(),
|
CreateTime: user.CreateTime.UnixMilli(),
|
||||||
AppMangerLevel: user.AppMangerLevel,
|
AppMangerLevel: user.AppMangerLevel,
|
||||||
GlobalRecvMsgOpt: user.GlobalRecvMsgOpt,
|
GlobalRecvMsgOpt: user.GlobalRecvMsgOpt,
|
||||||
|
Phone: user.Phone,
|
||||||
|
PhoneVisibility: user.PhoneVisibility,
|
||||||
|
CallAcceptSetting: user.CallAcceptSetting,
|
||||||
|
MsgReceiveSetting: user.MsgReceiveSetting,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +94,18 @@ func UserPb2DBMapEx(user *sdkws.UserInfoWithEx) map[string]any {
|
|||||||
if user.GlobalRecvMsgOpt != nil {
|
if user.GlobalRecvMsgOpt != nil {
|
||||||
val["global_recv_msg_opt"] = user.GlobalRecvMsgOpt.Value
|
val["global_recv_msg_opt"] = user.GlobalRecvMsgOpt.Value
|
||||||
}
|
}
|
||||||
|
if user.Phone != nil {
|
||||||
|
val["phone"] = user.Phone.Value
|
||||||
|
}
|
||||||
|
if user.PhoneVisibility != nil {
|
||||||
|
val["phone_visibility"] = user.PhoneVisibility.Value
|
||||||
|
}
|
||||||
|
if user.CallAcceptSetting != nil {
|
||||||
|
val["call_accept_setting"] = user.CallAcceptSetting.Value
|
||||||
|
}
|
||||||
|
if user.MsgReceiveSetting != nil {
|
||||||
|
val["msg_receive_setting"] = user.MsgReceiveSetting.Value
|
||||||
|
}
|
||||||
|
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,6 +76,7 @@ const (
|
|||||||
MutedInGroup = 1402 // Member muted in the group
|
MutedInGroup = 1402 // Member muted in the group
|
||||||
MutedGroup = 1403 // Group is muted
|
MutedGroup = 1403 // Group is muted
|
||||||
MsgAlreadyRevoke = 1404 // Message already revoked
|
MsgAlreadyRevoke = 1404 // Message already revoked
|
||||||
|
MsgReceiveNotAllowed = 1405 // Recipient does not allow receiving messages from this sender
|
||||||
|
|
||||||
// Token error codes.
|
// Token error codes.
|
||||||
TokenExpiredError = 1501
|
TokenExpiredError = 1501
|
||||||
|
|||||||
@ -59,6 +59,7 @@ var (
|
|||||||
ErrMutedInGroup = errs.NewCodeError(MutedInGroup, "MutedInGroup")
|
ErrMutedInGroup = errs.NewCodeError(MutedInGroup, "MutedInGroup")
|
||||||
ErrMutedGroup = errs.NewCodeError(MutedGroup, "MutedGroup")
|
ErrMutedGroup = errs.NewCodeError(MutedGroup, "MutedGroup")
|
||||||
ErrMsgAlreadyRevoke = errs.NewCodeError(MsgAlreadyRevoke, "MsgAlreadyRevoke")
|
ErrMsgAlreadyRevoke = errs.NewCodeError(MsgAlreadyRevoke, "MsgAlreadyRevoke")
|
||||||
|
ErrMsgReceiveNotAllowed = errs.NewCodeError(MsgReceiveNotAllowed, "MsgReceiveNotAllowed")
|
||||||
|
|
||||||
ErrConnOverMaxNumLimit = errs.NewCodeError(ConnOverMaxNumLimit, "ConnOverMaxNumLimit")
|
ErrConnOverMaxNumLimit = errs.NewCodeError(ConnOverMaxNumLimit, "ConnOverMaxNumLimit")
|
||||||
|
|
||||||
|
|||||||
@ -90,6 +90,8 @@ type FriendDatabase interface {
|
|||||||
OwnerIncrVersion(ctx context.Context, ownerUserID string, friendUserIDs []string, state int32) error
|
OwnerIncrVersion(ctx context.Context, ownerUserID string, friendUserIDs []string, state int32) error
|
||||||
|
|
||||||
GetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error)
|
GetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error)
|
||||||
|
|
||||||
|
GetPinnedFriendIDs(ctx context.Context, ownerUserID string) ([]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type friendDatabase struct {
|
type friendDatabase struct {
|
||||||
@ -402,3 +404,7 @@ func (f *friendDatabase) OwnerIncrVersion(ctx context.Context, ownerUserID strin
|
|||||||
func (f *friendDatabase) GetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error) {
|
func (f *friendDatabase) GetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error) {
|
||||||
return f.friendRequest.GetUnhandledCount(ctx, userID, ts)
|
return f.friendRequest.GetUnhandledCount(ctx, userID, ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *friendDatabase) GetPinnedFriendIDs(ctx context.Context, ownerUserID string) ([]string, error) {
|
||||||
|
return f.friend.FindPinnedFriendUserIDs(ctx, ownerUserID)
|
||||||
|
}
|
||||||
|
|||||||
@ -37,6 +37,11 @@ type UserDatabase interface {
|
|||||||
Find(ctx context.Context, userIDs []string) (users []*model.User, err error)
|
Find(ctx context.Context, userIDs []string) (users []*model.User, err error)
|
||||||
// Find userInfo By Nickname
|
// Find userInfo By Nickname
|
||||||
FindByNickname(ctx context.Context, nickname string) (users []*model.User, err error)
|
FindByNickname(ctx context.Context, nickname string) (users []*model.User, err error)
|
||||||
|
// FindOrdinaryUsersByNickname 昵称精确匹配,仅普通用户(与分页拉取用户 level 一致)
|
||||||
|
FindOrdinaryUsersByNickname(ctx context.Context, level1 int64, level2 int64, nickname string) (users []*model.User, err error)
|
||||||
|
// FindByPhone looks up a single user by exact phone number.
|
||||||
|
// Returns errs.ErrRecordNotFound if no user has the given phone.
|
||||||
|
FindByPhone(ctx context.Context, phone string) (user *model.User, err error)
|
||||||
// Find notificationAccounts
|
// Find notificationAccounts
|
||||||
FindNotification(ctx context.Context, level int64) (users []*model.User, err error)
|
FindNotification(ctx context.Context, level int64) (users []*model.User, err error)
|
||||||
// Create Insert multiple external guarantees that the userID is not repeated and does not exist in the storage
|
// Create Insert multiple external guarantees that the userID is not repeated and does not exist in the storage
|
||||||
@ -135,6 +140,14 @@ func (u *userDatabase) FindByNickname(ctx context.Context, nickname string) (use
|
|||||||
return u.userDB.TakeByNickname(ctx, nickname)
|
return u.userDB.TakeByNickname(ctx, nickname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *userDatabase) FindOrdinaryUsersByNickname(ctx context.Context, level1, level2 int64, nickname string) ([]*model.User, error) {
|
||||||
|
return u.userDB.FindOrdinaryUsersByNickname(ctx, level1, level2, nickname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userDatabase) FindByPhone(ctx context.Context, phone string) (*model.User, error) {
|
||||||
|
return u.userDB.FindByPhone(ctx, phone)
|
||||||
|
}
|
||||||
|
|
||||||
func (u *userDatabase) FindNotification(ctx context.Context, level int64) (users []*model.User, err error) {
|
func (u *userDatabase) FindNotification(ctx context.Context, level int64) (users []*model.User, err error) {
|
||||||
return u.userDB.TakeNotification(ctx, level)
|
return u.userDB.TakeNotification(ctx, level)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,8 @@ type UserGlobalBlackDatabase interface {
|
|||||||
RemoveBlack(ctx context.Context, userIDs []string) error
|
RemoveBlack(ctx context.Context, userIDs []string) error
|
||||||
// IsBlocked 检查用户是否在全局黑名单
|
// IsBlocked 检查用户是否在全局黑名单
|
||||||
IsBlocked(ctx context.Context, userID string) (bool, error)
|
IsBlocked(ctx context.Context, userID string) (bool, error)
|
||||||
|
// FindBlocked 批量查询哪些 userID 在全局黑名单中,返回被封禁的记录
|
||||||
|
FindBlocked(ctx context.Context, userIDs []string) ([]*model.UserGlobalBlack, error)
|
||||||
// GetBlackList 分页获取黑名单列表
|
// GetBlackList 分页获取黑名单列表
|
||||||
GetBlackList(ctx context.Context, pagination pagination.Pagination) (count int64, blacks []*model.UserGlobalBlack, err error)
|
GetBlackList(ctx context.Context, pagination pagination.Pagination) (count int64, blacks []*model.UserGlobalBlack, err error)
|
||||||
}
|
}
|
||||||
@ -43,3 +45,7 @@ func (u *userGlobalBlackDatabase) IsBlocked(ctx context.Context, userID string)
|
|||||||
func (u *userGlobalBlackDatabase) GetBlackList(ctx context.Context, pagination pagination.Pagination) (int64, []*model.UserGlobalBlack, error) {
|
func (u *userGlobalBlackDatabase) GetBlackList(ctx context.Context, pagination pagination.Pagination) (int64, []*model.UserGlobalBlack, error) {
|
||||||
return u.db.Page(ctx, pagination)
|
return u.db.Page(ctx, pagination)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *userGlobalBlackDatabase) FindBlocked(ctx context.Context, userIDs []string) ([]*model.UserGlobalBlack, error) {
|
||||||
|
return u.db.Find(ctx, userIDs)
|
||||||
|
}
|
||||||
|
|||||||
@ -57,4 +57,6 @@ type Friend interface {
|
|||||||
FindOwnerFriendUserIds(ctx context.Context, ownerUserID string, limit int) ([]string, error)
|
FindOwnerFriendUserIds(ctx context.Context, ownerUserID string, limit int) ([]string, error)
|
||||||
|
|
||||||
IncrVersion(ctx context.Context, ownerUserID string, friendUserIDs []string, state int32) error
|
IncrVersion(ctx context.Context, ownerUserID string, friendUserIDs []string, state int32) error
|
||||||
|
|
||||||
|
FindPinnedFriendUserIDs(ctx context.Context, ownerUserID string) ([]string, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,6 +47,17 @@ func NewFriendMongo(db *mongo.Database) (database.Friend, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Compound index to support efficient sorted pagination: pinned friends first, then by _id.
|
||||||
|
_, err = coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||||
|
Keys: bson.D{
|
||||||
|
{Key: "owner_user_id", Value: 1},
|
||||||
|
{Key: "is_pinned", Value: -1},
|
||||||
|
{Key: "_id", Value: 1},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
owner, err := NewVersionLog(db.Collection(database.FriendVersionName))
|
owner, err := NewVersionLog(db.Collection(database.FriendVersionName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -268,3 +279,10 @@ func (f *FriendMgo) IsUpdateIsPinned(data map[string]any) bool {
|
|||||||
_, ok := data["is_pinned"]
|
_, ok := data["is_pinned"]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FriendMgo) FindPinnedFriendUserIDs(ctx context.Context, ownerUserID string) ([]string, error) {
|
||||||
|
return mongoutil.Find[string](ctx, f.coll, bson.M{
|
||||||
|
"owner_user_id": ownerUserID,
|
||||||
|
"is_pinned": true,
|
||||||
|
}, options.Find().SetProjection(bson.M{"_id": 0, "friend_user_id": 1}))
|
||||||
|
}
|
||||||
|
|||||||
@ -39,6 +39,12 @@ func NewSignalMongo(db *mongo.Database) (database.SignalDatabase, error) {
|
|||||||
{
|
{
|
||||||
Keys: bson.D{{Key: "create_time", Value: -1}},
|
Keys: bson.D{{Key: "create_time", Value: -1}},
|
||||||
},
|
},
|
||||||
|
// Fix P1(TTL): expire_at 字段为 BSON Date,MongoDB 后台每 60s 扫描一次并自动删除过期文档。
|
||||||
|
// 覆盖场景:被叫网络断开、主叫 App 被杀、任何异常中断导致没有 Cancel/Reject/HungUp 的情况。
|
||||||
|
{
|
||||||
|
Keys: bson.D{{Key: "expire_at", Value: 1}},
|
||||||
|
Options: options.Index().SetExpireAfterSeconds(0),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
110
pkg/common/storage/database/mgo/spam_report.go
Normal file
110
pkg/common/storage/database/mgo/spam_report.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package mgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||||
|
"github.com/openimsdk/tools/db/mongoutil"
|
||||||
|
"github.com/openimsdk/tools/db/pagination"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSpamReportMongo(db *mongo.Database) (database.SpamReport, error) {
|
||||||
|
coll := db.Collection(database.SpamReportName)
|
||||||
|
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||||
|
{
|
||||||
|
Keys: bson.D{{Key: "report_id", Value: 1}},
|
||||||
|
Options: options.Index().SetUnique(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Keys: bson.D{
|
||||||
|
{Key: "reporter_user_id", Value: 1},
|
||||||
|
{Key: "create_time", Value: -1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Keys: bson.D{
|
||||||
|
{Key: "reported_user_id", Value: 1},
|
||||||
|
{Key: "create_time", Value: -1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Keys: bson.D{
|
||||||
|
{Key: "status", Value: 1},
|
||||||
|
{Key: "create_time", Value: -1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &SpamReportMgo{coll: coll}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpamReportMgo struct {
|
||||||
|
coll *mongo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpamReportMgo) Create(ctx context.Context, report *model.SpamReport) error {
|
||||||
|
return mongoutil.InsertOne(ctx, s.coll, report)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpamReportMgo) Find(ctx context.Context, status int32, reportedUserID, reporterUserID string,
|
||||||
|
start, end time.Time, pagination pagination.Pagination) (int64, []*model.SpamReport, error) {
|
||||||
|
filter := bson.M{}
|
||||||
|
if status >= 0 {
|
||||||
|
filter["status"] = status
|
||||||
|
}
|
||||||
|
if reportedUserID != "" {
|
||||||
|
filter["reported_user_id"] = reportedUserID
|
||||||
|
}
|
||||||
|
if reporterUserID != "" {
|
||||||
|
filter["reporter_user_id"] = reporterUserID
|
||||||
|
}
|
||||||
|
if !start.IsZero() || !end.IsZero() {
|
||||||
|
timeFilter := bson.M{}
|
||||||
|
if !start.IsZero() {
|
||||||
|
timeFilter["$gte"] = start
|
||||||
|
}
|
||||||
|
if !end.IsZero() {
|
||||||
|
timeFilter["$lte"] = end
|
||||||
|
}
|
||||||
|
filter["create_time"] = timeFilter
|
||||||
|
}
|
||||||
|
return mongoutil.FindPage[*model.SpamReport](ctx, s.coll, filter, pagination,
|
||||||
|
options.Find().SetSort(bson.D{{Key: "create_time", Value: -1}}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpamReportMgo) UpdateStatus(ctx context.Context, reportID string, status int32, handlerUserID string, handleTime time.Time) error {
|
||||||
|
return mongoutil.UpdateOne(ctx, s.coll,
|
||||||
|
bson.M{"report_id": reportID},
|
||||||
|
bson.M{"$set": bson.M{
|
||||||
|
"status": status,
|
||||||
|
"handler_user_id": handlerUserID,
|
||||||
|
"handle_time": handleTime,
|
||||||
|
}},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpamReportMgo) Get(ctx context.Context, reportID string) (*model.SpamReport, error) {
|
||||||
|
return mongoutil.FindOne[*model.SpamReport](ctx, s.coll, bson.M{"report_id": reportID})
|
||||||
|
}
|
||||||
@ -32,13 +32,20 @@ import (
|
|||||||
|
|
||||||
func NewUserMongo(db *mongo.Database) (database.User, error) {
|
func NewUserMongo(db *mongo.Database) (database.User, error) {
|
||||||
coll := db.Collection(database.UserName)
|
coll := db.Collection(database.UserName)
|
||||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
indexes := []mongo.IndexModel{
|
||||||
Keys: bson.D{
|
{
|
||||||
{Key: "user_id", Value: 1},
|
Keys: bson.D{{Key: "user_id", Value: 1}},
|
||||||
|
Options: options.Index().SetUnique(true),
|
||||||
},
|
},
|
||||||
Options: options.Index().SetUnique(true),
|
{
|
||||||
})
|
Keys: bson.D{{Key: "phone", Value: 1}},
|
||||||
if err != nil {
|
Options: options.Index().SetSparse(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Keys: bson.D{{Key: "nickname", Value: 1}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, err := coll.Indexes().CreateMany(context.Background(), indexes); err != nil {
|
||||||
return nil, errs.Wrap(err)
|
return nil, errs.Wrap(err)
|
||||||
}
|
}
|
||||||
return &UserMgo{coll: coll}, nil
|
return &UserMgo{coll: coll}, nil
|
||||||
@ -75,6 +82,18 @@ func (u *UserMgo) TakeByNickname(ctx context.Context, nickname string) (user []*
|
|||||||
return mongoutil.Find[*model.User](ctx, u.coll, bson.M{"nickname": nickname})
|
return mongoutil.Find[*model.User](ctx, u.coll, bson.M{"nickname": nickname})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UserMgo) FindOrdinaryUsersByNickname(ctx context.Context, level1, level2 int64, nickname string) ([]*model.User, error) {
|
||||||
|
query := bson.M{
|
||||||
|
"nickname": nickname,
|
||||||
|
"app_manger_level": bson.M{"$in": []int64{level1, level2}},
|
||||||
|
}
|
||||||
|
return mongoutil.Find[*model.User](ctx, u.coll, query, options.Find().SetLimit(100))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserMgo) FindByPhone(ctx context.Context, phone string) (*model.User, error) {
|
||||||
|
return mongoutil.FindOne[*model.User](ctx, u.coll, bson.M{"phone": phone})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UserMgo) Page(ctx context.Context, pagination pagination.Pagination) (count int64, users []*model.User, err error) {
|
func (u *UserMgo) Page(ctx context.Context, pagination pagination.Pagination) (count int64, users []*model.User, err error) {
|
||||||
return mongoutil.FindPage[*model.User](ctx, u.coll, bson.M{}, pagination)
|
return mongoutil.FindPage[*model.User](ctx, u.coll, bson.M{}, pagination)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,4 +21,5 @@ const (
|
|||||||
PhoneSNInfoName = "phone_sn_info"
|
PhoneSNInfoName = "phone_sn_info"
|
||||||
SignalInvitationName = "signal_invitation"
|
SignalInvitationName = "signal_invitation"
|
||||||
SignalRecordName = "signal_record"
|
SignalRecordName = "signal_record"
|
||||||
|
SpamReportName = "spam_report"
|
||||||
)
|
)
|
||||||
|
|||||||
35
pkg/common/storage/database/spam_report.go
Normal file
35
pkg/common/storage/database/spam_report.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||||
|
"github.com/openimsdk/tools/db/pagination"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpamReport interface {
|
||||||
|
// Create inserts a new spam report record.
|
||||||
|
Create(ctx context.Context, report *model.SpamReport) error
|
||||||
|
// Find queries spam reports with optional filters, returns total count and records.
|
||||||
|
Find(ctx context.Context, status int32, reportedUserID, reporterUserID string,
|
||||||
|
start, end time.Time, pagination pagination.Pagination) (int64, []*model.SpamReport, error)
|
||||||
|
// UpdateStatus updates the handling status of a spam report.
|
||||||
|
UpdateStatus(ctx context.Context, reportID string, status int32, handlerUserID string, handleTime time.Time) error
|
||||||
|
// Get retrieves a single spam report by its reportID.
|
||||||
|
Get(ctx context.Context, reportID string) (*model.SpamReport, error)
|
||||||
|
}
|
||||||
@ -29,6 +29,9 @@ type User interface {
|
|||||||
Take(ctx context.Context, userID string) (user *model.User, err error)
|
Take(ctx context.Context, userID string) (user *model.User, err error)
|
||||||
TakeNotification(ctx context.Context, level int64) (user []*model.User, err error)
|
TakeNotification(ctx context.Context, level int64) (user []*model.User, err error)
|
||||||
TakeByNickname(ctx context.Context, nickname string) (user []*model.User, err error)
|
TakeByNickname(ctx context.Context, nickname string) (user []*model.User, err error)
|
||||||
|
// FindOrdinaryUsersByNickname 按昵称精确匹配,且 app_manger_level 为普通用户范围(与分页拉取用户一致)
|
||||||
|
FindOrdinaryUsersByNickname(ctx context.Context, level1 int64, level2 int64, nickname string) (users []*model.User, err error)
|
||||||
|
FindByPhone(ctx context.Context, phone string) (user *model.User, err error)
|
||||||
Page(ctx context.Context, pagination pagination.Pagination) (count int64, users []*model.User, err error)
|
Page(ctx context.Context, pagination pagination.Pagination) (count int64, users []*model.User, err error)
|
||||||
PageFindUser(ctx context.Context, level1 int64, level2 int64, pagination pagination.Pagination) (count int64, users []*model.User, err error)
|
PageFindUser(ctx context.Context, level1 int64, level2 int64, pagination pagination.Pagination) (count int64, users []*model.User, err error)
|
||||||
PageFindUserWithKeyword(ctx context.Context, level1 int64, level2 int64, userID, nickName string, pagination pagination.Pagination) (count int64, users []*model.User, err error)
|
PageFindUserWithKeyword(ctx context.Context, level1 int64, level2 int64, userID, nickName string, pagination pagination.Pagination) (count int64, users []*model.User, err error)
|
||||||
|
|||||||
@ -30,4 +30,7 @@ type Friend struct {
|
|||||||
OperatorUserID string `bson:"operator_user_id"`
|
OperatorUserID string `bson:"operator_user_id"`
|
||||||
Ex string `bson:"ex"`
|
Ex string `bson:"ex"`
|
||||||
IsPinned bool `bson:"is_pinned"`
|
IsPinned bool `bson:"is_pinned"`
|
||||||
|
IsMuted bool `bson:"is_muted"`
|
||||||
|
MuteDuration int64 `bson:"mute_duration"` // 单位:秒
|
||||||
|
MuteEndTime int64 `bson:"mute_end_time"` // Unix 毫秒时间戳,0 表示永久
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GroupPermission 群组操作权限枚举。
|
||||||
|
// 0=全员可操作(默认),1=仅群主/管理员可操作
|
||||||
|
const (
|
||||||
|
GroupPermAllMember = int32(0) // 全员均可
|
||||||
|
GroupPermAdminOnly = int32(1) // 仅群主/管理员
|
||||||
|
)
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
GroupID string `bson:"group_id"`
|
GroupID string `bson:"group_id"`
|
||||||
GroupName string `bson:"group_name"`
|
GroupName string `bson:"group_name"`
|
||||||
@ -34,4 +41,12 @@ type Group struct {
|
|||||||
ApplyMemberFriend int32 `bson:"apply_member_friend"`
|
ApplyMemberFriend int32 `bson:"apply_member_friend"`
|
||||||
NotificationUpdateTime time.Time `bson:"notification_update_time"`
|
NotificationUpdateTime time.Time `bson:"notification_update_time"`
|
||||||
NotificationUserID string `bson:"notification_user_id"`
|
NotificationUserID string `bson:"notification_user_id"`
|
||||||
|
// AllowSendMsg 0=全员可发消息 1=仅群主/管理员可发消息
|
||||||
|
AllowSendMsg int32 `bson:"allow_send_msg"`
|
||||||
|
// AllowPinMsg 0=全员可置顶消息 1=仅群主/管理员可置顶消息
|
||||||
|
AllowPinMsg int32 `bson:"allow_pin_msg"`
|
||||||
|
// AllowAddMember 0=全员可拉人入群 1=仅群主/管理员可拉人入群
|
||||||
|
AllowAddMember int32 `bson:"allow_add_member"`
|
||||||
|
// AllowEditGroupInfo 0=全员可编辑群资料 1=仅群主/管理员可编辑群资料
|
||||||
|
AllowEditGroupInfo int32 `bson:"allow_edit_group_info"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
// SignalInvitation stores an ongoing or pending signal invitation, keyed by roomID.
|
// SignalInvitation stores an ongoing or pending signal invitation, keyed by roomID.
|
||||||
// It is created when a call is initiated and can be queried when the callee starts the app.
|
// It is created when a call is initiated and can be queried when the callee starts the app.
|
||||||
type SignalInvitation struct {
|
type SignalInvitation struct {
|
||||||
@ -32,6 +34,9 @@ type SignalInvitation struct {
|
|||||||
OfflinePushDesc string `bson:"offline_push_desc"`
|
OfflinePushDesc string `bson:"offline_push_desc"`
|
||||||
OfflinePushEx string `bson:"offline_push_ex"`
|
OfflinePushEx string `bson:"offline_push_ex"`
|
||||||
CreateTime int64 `bson:"create_time"`
|
CreateTime int64 `bson:"create_time"`
|
||||||
|
// ExpireAt 是 MongoDB BSON Date 类型,供 TTL 索引自动清理过期邀请(无人响应/异常中断场景)。
|
||||||
|
// 值 = 创建时间 + Timeout + 30s 缓冲,由 invitationToModel 负责填充。
|
||||||
|
ExpireAt time.Time `bson:"expire_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignalRecord stores a completed call record used for history queries.
|
// SignalRecord stores a completed call record used for history queries.
|
||||||
|
|||||||
53
pkg/common/storage/model/spam_report.go
Normal file
53
pkg/common/storage/model/spam_report.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SpamReport status constants.
|
||||||
|
const (
|
||||||
|
SpamReportStatusPending int32 = 0 // 待处理
|
||||||
|
SpamReportStatusHandled int32 = 1 // 已处理
|
||||||
|
SpamReportStatusIgnored int32 = 2 // 已忽略
|
||||||
|
)
|
||||||
|
|
||||||
|
// SpamReport reason type constants.
|
||||||
|
const (
|
||||||
|
SpamReasonTypeSpam int32 = 1 // 垃圾消息
|
||||||
|
SpamReasonTypePorn int32 = 2 // 色情内容
|
||||||
|
SpamReasonTypeIllegal int32 = 3 // 违法内容
|
||||||
|
SpamReasonTypeOther int32 = 4 // 其他
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpamReport struct {
|
||||||
|
ID primitive.ObjectID `bson:"_id"`
|
||||||
|
ReportID string `bson:"report_id"`
|
||||||
|
ReporterUserID string `bson:"reporter_user_id"`
|
||||||
|
ReportedUserID string `bson:"reported_user_id"`
|
||||||
|
ConversationID string `bson:"conversation_id"` // 举报具体消息时填写
|
||||||
|
ClientMsgID string `bson:"client_msg_id"` // 举报具体消息时填写
|
||||||
|
Seq int64 `bson:"seq"`
|
||||||
|
ReasonType int32 `bson:"reason_type"` // 1垃圾 2色情 3违法 4其他
|
||||||
|
Reason string `bson:"reason"`
|
||||||
|
Status int32 `bson:"status"` // 0待处理 1已处理 2已忽略
|
||||||
|
CreateTime time.Time `bson:"create_time"`
|
||||||
|
HandleTime time.Time `bson:"handle_time"`
|
||||||
|
HandlerUserID string `bson:"handler_user_id"`
|
||||||
|
Ex string `bson:"ex"`
|
||||||
|
}
|
||||||
@ -18,6 +18,30 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PhoneVisibility 手机号可见性枚举。
|
||||||
|
// 0=所有人可见, 1=仅好友可见, 2=完全隐藏
|
||||||
|
const (
|
||||||
|
PhoneVisibilityPublic int32 = 0
|
||||||
|
PhoneVisibilityFriends int32 = 1
|
||||||
|
PhoneVisibilityHidden int32 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// CallAcceptSetting 音视频通话接受权限枚举。
|
||||||
|
// 0=所有人可发起, 1=仅好友可发起, 2=不接受任何通话
|
||||||
|
const (
|
||||||
|
CallAcceptSettingPublic int32 = 0
|
||||||
|
CallAcceptSettingFriends int32 = 1
|
||||||
|
CallAcceptSettingNobody int32 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// MsgReceiveSetting 会话消息接收权限枚举。
|
||||||
|
// 0=所有人可发送, 1=仅好友可发送, 2=所有人不可发送
|
||||||
|
const (
|
||||||
|
MsgReceiveSettingPublic int32 = 0
|
||||||
|
MsgReceiveSettingFriends int32 = 1
|
||||||
|
MsgReceiveSettingNobody int32 = 2
|
||||||
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
UserID string `bson:"user_id"`
|
UserID string `bson:"user_id"`
|
||||||
Nickname string `bson:"nickname"`
|
Nickname string `bson:"nickname"`
|
||||||
@ -26,6 +50,14 @@ type User struct {
|
|||||||
AppMangerLevel int32 `bson:"app_manger_level"`
|
AppMangerLevel int32 `bson:"app_manger_level"`
|
||||||
GlobalRecvMsgOpt int32 `bson:"global_recv_msg_opt"`
|
GlobalRecvMsgOpt int32 `bson:"global_recv_msg_opt"`
|
||||||
CreateTime time.Time `bson:"create_time"`
|
CreateTime time.Time `bson:"create_time"`
|
||||||
|
// Phone 用户手机号(明文,仅服务端留存,下发时按 PhoneVisibility 过滤)
|
||||||
|
Phone string `bson:"phone"`
|
||||||
|
// PhoneVisibility 0=所有人可见 1=仅好友可见 2=隐藏
|
||||||
|
PhoneVisibility int32 `bson:"phone_visibility"`
|
||||||
|
// CallAcceptSetting 0=所有人可发起 1=仅好友可发起 2=不接受任何通话
|
||||||
|
CallAcceptSetting int32 `bson:"call_accept_setting"`
|
||||||
|
// MsgReceiveSetting 0=所有人可发送 1=仅好友可发送 2=所有人不可发送
|
||||||
|
MsgReceiveSetting int32 `bson:"msg_receive_setting"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) GetNickname() string {
|
func (u *User) GetNickname() string {
|
||||||
|
|||||||
@ -21,3 +21,15 @@ func (x *RelationClient) GetFriendsInfo(ctx context.Context, ownerUserID string,
|
|||||||
req := &relation.GetFriendInfoReq{OwnerUserID: ownerUserID, FriendUserIDs: friendUserIDs}
|
req := &relation.GetFriendInfoReq{OwnerUserID: ownerUserID, FriendUserIDs: friendUserIDs}
|
||||||
return extractField(ctx, x.FriendClient.GetFriendInfo, req, (*relation.GetFriendInfoResp).GetFriendInfos)
|
return extractField(ctx, x.FriendClient.GetFriendInfo, req, (*relation.GetFriendInfoResp).GetFriendInfos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsFriend checks whether userID2 is in userID1's friend list.
|
||||||
|
func (x *RelationClient) IsFriend(ctx context.Context, ownerUserID, friendUserID string) (bool, error) {
|
||||||
|
resp, err := x.FriendClient.IsFriend(ctx, &relation.IsFriendReq{
|
||||||
|
UserID1: ownerUserID,
|
||||||
|
UserID2: friendUserID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return resp.InUser1Friends, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
# chmod +x captcha_api_test.sh
|
# chmod +x captcha_api_test.sh
|
||||||
# ./captcha_api_test.sh
|
# ./captcha_api_test.sh
|
||||||
# ./captcha_api_test.sh --host http://127.0.0.1:10002
|
# ./captcha_api_test.sh --host http://127.0.0.1:10002
|
||||||
|
# HOST=http://api.example.com:10002 ./captcha_api_test.sh
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@ -18,17 +19,13 @@ set -euo pipefail
|
|||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# 可配置参数(可通过环境变量覆盖)
|
# 可配置参数(可通过环境变量覆盖)
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
|
# 说明:/captcha/* 在 GinParseToken 白名单中,无需 Header token。
|
||||||
HOST="${HOST:-http://127.0.0.1:10002}"
|
HOST="${HOST:-http://127.0.0.1:10002}"
|
||||||
ADMIN_USER_ID="${ADMIN_USER_ID:-imAdmin}"
|
|
||||||
ADMIN_SECRET="${ADMIN_SECRET:-openIM123}"
|
|
||||||
PLATFORM_ID="${PLATFORM_ID:-1}" # 1=iOS 2=Android 3=Windows ...
|
|
||||||
|
|
||||||
# 命令行参数解析
|
# 命令行参数解析
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--host) HOST="$2"; shift 2 ;;
|
--host) HOST="$2"; shift 2 ;;
|
||||||
--admin-user-id) ADMIN_USER_ID="$2"; shift 2 ;;
|
|
||||||
--admin-secret) ADMIN_SECRET="$2"; shift 2 ;;
|
|
||||||
*) echo "未知参数: $1"; exit 1 ;;
|
*) echo "未知参数: $1"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@ -108,35 +105,12 @@ assert_err_nonzero() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# 前置:获取 Admin Token
|
# 用例 1:生成验证码 —— 正常流程(无需 token,白名单)
|
||||||
# ──────────────────────────────────────────────
|
|
||||||
section "前置:获取 Admin Token"
|
|
||||||
|
|
||||||
TOKEN_RESP=$(curl -s -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "operationID: $(new_op_id)" \
|
|
||||||
-d "{\"secret\":\"${ADMIN_SECRET}\",\"platformID\":${PLATFORM_ID},\"userID\":\"${ADMIN_USER_ID}\"}" \
|
|
||||||
"${HOST}/auth/get_admin_token")
|
|
||||||
|
|
||||||
info "Token 响应: $TOKEN_RESP"
|
|
||||||
|
|
||||||
ERR_CODE=$(echo "$TOKEN_RESP" | jq -r '.errCode // "null"')
|
|
||||||
if [[ "$ERR_CODE" != "0" ]]; then
|
|
||||||
echo -e "${RED}[ERROR]${NC} 获取 Admin Token 失败 (errCode=$ERR_CODE),中止测试"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TOKEN=$(echo "$TOKEN_RESP" | jq -r '.data.token')
|
|
||||||
info "获取到 token: ${TOKEN:0:40}..."
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
|
||||||
# 用例 1:生成验证码 —— 正常流程
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
section "用例 1 / POST /captcha/generate —— 正常生成验证码"
|
section "用例 1 / POST /captcha/generate —— 正常生成验证码"
|
||||||
|
|
||||||
GEN_RESP=$(curl -s -X POST \
|
GEN_RESP=$(curl -s -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "token: ${TOKEN}" \
|
|
||||||
-H "operationID: $(new_op_id)" \
|
-H "operationID: $(new_op_id)" \
|
||||||
-d '{}' \
|
-d '{}' \
|
||||||
"${HOST}/captcha/generate")
|
"${HOST}/captcha/generate")
|
||||||
@ -165,9 +139,9 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# 用例 2:生成验证码 —— 不携带 Token
|
# 用例 2:生成验证码 —— 不携带 Token(白名单,应与用例 1 一致成功)
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
section "用例 2 / POST /captcha/generate —— 无 Token 应被鉴权中间件拦截"
|
section "用例 2 / POST /captcha/generate —— 无 Token(白名单)仍应成功"
|
||||||
|
|
||||||
NO_TOKEN_RESP=$(curl -s -X POST \
|
NO_TOKEN_RESP=$(curl -s -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
@ -175,8 +149,13 @@ NO_TOKEN_RESP=$(curl -s -X POST \
|
|||||||
-d '{}' \
|
-d '{}' \
|
||||||
"${HOST}/captcha/generate")
|
"${HOST}/captcha/generate")
|
||||||
|
|
||||||
info "响应: $NO_TOKEN_RESP"
|
info "响应摘要: $(echo "${NO_TOKEN_RESP}" | jq -c '{errCode,errMsg,data:{captchaID:.data.captchaID}}' 2>/dev/null || echo "$NO_TOKEN_RESP")"
|
||||||
assert_err_nonzero "$NO_TOKEN_RESP" "无 Token 被鉴权中间件拦截"
|
NO_TOKEN_ERR=$(echo "${NO_TOKEN_RESP}" | jq -r '.errCode // "null"')
|
||||||
|
if [[ "${NO_TOKEN_ERR}" == "500" ]]; then
|
||||||
|
info "与用例 1 相同:若 captcha 资源未就绪可能为 500,此处不强制 PASS/FAIL"
|
||||||
|
else
|
||||||
|
assert_err_code "${NO_TOKEN_RESP}" "0" "无 Token 调用 generate errCode 应为 0(白名单)"
|
||||||
|
fi
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# 用例 3:验证验证码 —— 坐标错误(x=999, y=999)
|
# 用例 3:验证验证码 —— 坐标错误(x=999, y=999)
|
||||||
@ -188,7 +167,6 @@ if [[ -z "${CAPTCHA_ID}" ]]; then
|
|||||||
else
|
else
|
||||||
VERIFY_WRONG_RESP=$(curl -s -X POST \
|
VERIFY_WRONG_RESP=$(curl -s -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "token: ${TOKEN}" \
|
|
||||||
-H "operationID: $(new_op_id)" \
|
-H "operationID: $(new_op_id)" \
|
||||||
-d "{\"captchaID\":\"${CAPTCHA_ID}\",\"x\":999,\"y\":999}" \
|
-d "{\"captchaID\":\"${CAPTCHA_ID}\",\"x\":999,\"y\":999}" \
|
||||||
"${HOST}/captcha/verify")
|
"${HOST}/captcha/verify")
|
||||||
@ -209,7 +187,6 @@ if [[ -z "${CAPTCHA_ID}" ]]; then
|
|||||||
else
|
else
|
||||||
VERIFY_REUSE_RESP=$(curl -s -X POST \
|
VERIFY_REUSE_RESP=$(curl -s -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "token: ${TOKEN}" \
|
|
||||||
-H "operationID: $(new_op_id)" \
|
-H "operationID: $(new_op_id)" \
|
||||||
-d "{\"captchaID\":\"${CAPTCHA_ID}\",\"x\":0,\"y\":0}" \
|
-d "{\"captchaID\":\"${CAPTCHA_ID}\",\"x\":0,\"y\":0}" \
|
||||||
"${HOST}/captcha/verify")
|
"${HOST}/captcha/verify")
|
||||||
@ -224,7 +201,6 @@ section "用例 5 / POST /captcha/verify —— captchaID 不存在,应返回
|
|||||||
|
|
||||||
VERIFY_NOTFOUND_RESP=$(curl -s -X POST \
|
VERIFY_NOTFOUND_RESP=$(curl -s -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "token: ${TOKEN}" \
|
|
||||||
-H "operationID: $(new_op_id)" \
|
-H "operationID: $(new_op_id)" \
|
||||||
-d '{"captchaID":"00000000-0000-0000-0000-000000000000","x":10,"y":10}' \
|
-d '{"captchaID":"00000000-0000-0000-0000-000000000000","x":10,"y":10}' \
|
||||||
"${HOST}/captcha/verify")
|
"${HOST}/captcha/verify")
|
||||||
@ -239,7 +215,6 @@ section "用例 6 / POST /captcha/verify —— captchaID 为空字符串,应
|
|||||||
|
|
||||||
VERIFY_EMPTY_RESP=$(curl -s -X POST \
|
VERIFY_EMPTY_RESP=$(curl -s -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "token: ${TOKEN}" \
|
|
||||||
-H "operationID: $(new_op_id)" \
|
-H "operationID: $(new_op_id)" \
|
||||||
-d '{"captchaID":"","x":10,"y":10}' \
|
-d '{"captchaID":"","x":10,"y":10}' \
|
||||||
"${HOST}/captcha/verify")
|
"${HOST}/captcha/verify")
|
||||||
@ -248,18 +223,18 @@ info "响应: $VERIFY_EMPTY_RESP"
|
|||||||
assert_err_nonzero "$VERIFY_EMPTY_RESP" "captchaID 为空时返回错误"
|
assert_err_nonzero "$VERIFY_EMPTY_RESP" "captchaID 为空时返回错误"
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# 用例 7:验证验证码 —— 不携带 Token
|
# 用例 7:验证验证码 —— 不携带 Token(白名单,应到达业务层而非 token 拦截)
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
section "用例 7 / POST /captcha/verify —— 无 Token 应被鉴权中间件拦截"
|
section "用例 7 / POST /captcha/verify —— 无 Token(白名单)随机 captchaID 应返回业务错误"
|
||||||
|
|
||||||
VERIFY_NOTOKEN_RESP=$(curl -s -X POST \
|
VERIFY_NOTOKEN_RESP=$(curl -s -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "operationID: $(new_op_id)" \
|
-H "operationID: $(new_op_id)" \
|
||||||
-d "{\"captchaID\":\"${CAPTCHA_ID:-00000000-0000-0000-0000-000000000000}\",\"x\":10,\"y\":10}" \
|
-d "{\"captchaID\":\"11111111-1111-1111-1111-111111111111\",\"x\":10,\"y\":10}" \
|
||||||
"${HOST}/captcha/verify")
|
"${HOST}/captcha/verify")
|
||||||
|
|
||||||
info "响应: $VERIFY_NOTOKEN_RESP"
|
info "响应: $VERIFY_NOTOKEN_RESP"
|
||||||
assert_err_nonzero "$VERIFY_NOTOKEN_RESP" "无 Token 被鉴权中间件拦截"
|
assert_err_nonzero "$VERIFY_NOTOKEN_RESP" "无 Token 时无效 captchaID 仍返回业务层 errCode!=0(非鉴权拦截)"
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# 用例 8:完整正向链路 —— 新生成 + 用偏差坐标验证
|
# 用例 8:完整正向链路 —— 新生成 + 用偏差坐标验证
|
||||||
@ -271,7 +246,6 @@ section "用例 8 / 完整正向链路 —— 新生成验证码 → 坐标偏
|
|||||||
|
|
||||||
GEN_RESP2=$(curl -s -X POST \
|
GEN_RESP2=$(curl -s -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "token: ${TOKEN}" \
|
|
||||||
-H "operationID: $(new_op_id)" \
|
-H "operationID: $(new_op_id)" \
|
||||||
-d '{}' \
|
-d '{}' \
|
||||||
"${HOST}/captcha/generate")
|
"${HOST}/captcha/generate")
|
||||||
@ -298,7 +272,6 @@ else
|
|||||||
|
|
||||||
VERIFY_LINK_RESP=$(curl -s -X POST \
|
VERIFY_LINK_RESP=$(curl -s -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "token: ${TOKEN}" \
|
|
||||||
-H "operationID: $(new_op_id)" \
|
-H "operationID: $(new_op_id)" \
|
||||||
-d "{\"captchaID\":\"${CAPTCHA_ID2}\",\"x\":0,\"y\":0}" \
|
-d "{\"captchaID\":\"${CAPTCHA_ID2}\",\"x\":0,\"y\":0}" \
|
||||||
"${HOST}/captcha/verify")
|
"${HOST}/captcha/verify")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user