Files
kim.dev.6789 e50142a3b9 复制项目
2026-01-14 22:16:44 +08:00

406 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright © 2023 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"
"encoding/json"
"math/rand"
"regexp"
"strconv"
"time"
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/tools/utils/encrypt"
"github.com/openimsdk/tools/utils/timeutil"
"git.imall.cloud/openim/protocol/constant"
"git.imall.cloud/openim/protocol/msg"
"git.imall.cloud/openim/protocol/sdkws"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
)
var ExcludeContentType = []int{constant.HasReadReceipt}
// URL正则表达式匹配常见的URL格式
// 匹配以下格式的链接:
// 1. http:// 或 https:// 开头的完整URL
// 2. // 开头的协议相对URL如 //s.yam.com/MvQzr
// 3. www. 开头的链接
// 4. 直接包含域名的链接(如 s.yam.com/MvQzr、xxx.cc/csd、t.cn/AX4fYkFZ匹配包含至少一个点的域名格式后面可跟路径
// 域名格式至少包含一个点和一个2位以上的顶级域名如 xxx.com、xxx.cn、s.yam.com、xxx.cc、t.cn 等
// 注意:匹配 // 后面必须跟着域名格式,避免误匹配其他 // 开头的文本
// 修复:支持单字符域名(如 t.cn移除了点之前必须有两个字符的限制
var urlRegex = regexp.MustCompile(`(?i)(https?://[^\s<>"{}|\\^` + "`" + `\[\]]+|//[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}(/[^\s<>"{}|\\^` + "`" + `\[\]]*)?|www\.[^\s<>"{}|\\^` + "`" + `\[\]]+|[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}(/[^\s<>"{}|\\^` + "`" + `\[\]]*)?)`)
type Validator interface {
validate(pb *msg.SendMsgReq) (bool, int32, string)
}
// TextElem 用于解析文本消息内容
type TextElem struct {
Content string `json:"content"`
}
// AtElem 用于解析@消息内容
type AtElem struct {
Text string `json:"text"`
}
// checkMessageContainsLink 检测消息内容中是否包含链接
// userType: 0-普通用户不能发送链接1-特殊用户(可以发送链接)
func (m *msgServer) checkMessageContainsLink(msgData *sdkws.MsgData, userType int32) error {
// userType=1 的用户可以发送链接,不进行检测
if userType == 1 {
return nil
}
// 只检测文本类型的消息
if msgData.ContentType != constant.Text && msgData.ContentType != constant.AtText {
return nil
}
var textContent string
var err error
// 解析消息内容
if msgData.ContentType == constant.Text {
var textElem TextElem
if err = json.Unmarshal(msgData.Content, &textElem); err != nil {
// 如果解析失败,尝试直接使用字符串
textContent = string(msgData.Content)
} else {
textContent = textElem.Content
}
} else if msgData.ContentType == constant.AtText {
var atElem AtElem
if err = json.Unmarshal(msgData.Content, &atElem); err != nil {
// 如果解析失败,尝试直接使用字符串
textContent = string(msgData.Content)
} else {
textContent = atElem.Text
}
}
// 检测是否包含链接
if urlRegex.MatchString(textContent) {
return servererrs.ErrMessageContainsLink.WrapMsg("userType=0的用户不能发送包含链接的消息")
}
return nil
}
type MessageRevoked struct {
RevokerID string `json:"revokerID"`
RevokerRole int32 `json:"revokerRole"`
ClientMsgID string `json:"clientMsgID"`
RevokerNickname string `json:"revokerNickname"`
RevokeTime int64 `json:"revokeTime"`
SourceMessageSendTime int64 `json:"sourceMessageSendTime"`
SourceMessageSendID string `json:"sourceMessageSendID"`
SourceMessageSenderNickname string `json:"sourceMessageSenderNickname"`
SessionType int32 `json:"sessionType"`
Seq uint32 `json:"seq"`
}
func (m *msgServer) messageVerification(ctx context.Context, data *msg.SendMsgReq) error {
webhookCfg := m.webhookConfig()
switch data.MsgData.SessionType {
case constant.SingleChatType:
if datautil.Contain(data.MsgData.SendID, m.adminUserIDs...) {
return nil
}
if data.MsgData.ContentType <= constant.NotificationEnd &&
data.MsgData.ContentType >= constant.NotificationBegin {
return nil
}
if err := m.webhookBeforeSendSingleMsg(ctx, &webhookCfg.BeforeSendSingleMsg, data); err != nil {
return err
}
u, err := m.UserLocalCache.GetUserInfo(ctx, data.MsgData.SendID)
if err != nil {
return err
}
if authverify.CheckSystemAccount(ctx, u.AppMangerLevel) {
return nil
}
// userType=1 的用户可以发送链接和二维码,不进行检测
userType := u.GetUserType()
if userType != 1 {
// 检测userType=0的用户是否发送了包含链接的消息
if err := m.checkMessageContainsLink(data.MsgData, userType); err != nil {
return err
}
// 检测userType=0的用户是否发送了包含二维码的图片
if err := m.checkImageContainsQRCode(ctx, data.MsgData, userType); 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
case constant.ReadGroupChatType:
groupInfo, err := m.GroupLocalCache.GetGroupInfo(ctx, data.MsgData.GroupID)
if err != nil {
return err
}
if groupInfo.Status == constant.GroupStatusDismissed &&
data.MsgData.ContentType != constant.GroupDismissedNotification {
return servererrs.ErrDismissedAlready.Wrap()
}
// 检查是否为系统管理员,系统管理员跳过部分检查
isSystemAdmin := datautil.Contain(data.MsgData.SendID, m.adminUserIDs...)
// 通知消息类型跳过检查
if data.MsgData.ContentType <= constant.NotificationEnd &&
data.MsgData.ContentType >= constant.NotificationBegin {
return nil
}
// SuperGroup 跳过部分检查,但仍需检查文件权限
if groupInfo.GroupType == constant.SuperGroup {
// SuperGroup 也需要检查文件发送权限
if data.MsgData.ContentType == constant.File {
if isSystemAdmin {
// 系统管理员可以发送文件
return nil
}
memberIDs, err := m.GroupLocalCache.GetGroupMemberIDMap(ctx, data.MsgData.GroupID)
if err != nil {
return err
}
if _, ok := memberIDs[data.MsgData.SendID]; !ok {
return servererrs.ErrNotInGroupYet.Wrap()
}
groupMemberInfo, err := m.GroupLocalCache.GetGroupMember(ctx, data.MsgData.GroupID, data.MsgData.SendID)
if err != nil {
if errs.ErrRecordNotFound.Is(err) {
return servererrs.ErrNotInGroupYet.WrapMsg(err.Error())
}
return err
}
u, err := m.UserLocalCache.GetUserInfo(ctx, data.MsgData.SendID)
if err != nil {
return err
}
isGroupOwner := groupMemberInfo.RoleLevel == constant.GroupOwner
isGroupAdmin := groupMemberInfo.RoleLevel == constant.GroupAdmin
canSendFile := u.GetUserType() == 1 || isGroupOwner || isGroupAdmin
if !canSendFile {
return servererrs.ErrNoPermission.WrapMsg("only group owner, admin, or userType=1 can send files in group chat")
}
}
return nil
}
// 先获取用户信息用于检查userType系统管理员和userType=1的用户可以发送文件
memberIDs, err := m.GroupLocalCache.GetGroupMemberIDMap(ctx, data.MsgData.GroupID)
if err != nil {
return err
}
if _, ok := memberIDs[data.MsgData.SendID]; !ok {
return servererrs.ErrNotInGroupYet.Wrap()
}
groupMemberInfo, err := m.GroupLocalCache.GetGroupMember(ctx, data.MsgData.GroupID, data.MsgData.SendID)
if err != nil {
if errs.ErrRecordNotFound.Is(err) {
return servererrs.ErrNotInGroupYet.WrapMsg(err.Error())
}
return err
}
// 获取用户信息以获取userType
u, err := m.UserLocalCache.GetUserInfo(ctx, data.MsgData.SendID)
if err != nil {
return err
}
// 群聊中:检查文件发送权限(优先检查,确保不会被其他逻辑跳过)
// 只有群主、管理员、userType=1的用户可以发送文件
// 系统管理员也可以发送文件
if data.MsgData.ContentType == constant.File {
isGroupOwner := groupMemberInfo.RoleLevel == constant.GroupOwner
isGroupAdmin := groupMemberInfo.RoleLevel == constant.GroupAdmin
userType := u.GetUserType()
// 如果是文件消息且 userType=0 且不是群主/管理员,可能是缓存问题,尝试清除缓存并重新获取
if userType == 0 && !isGroupOwner && !isGroupAdmin && !isSystemAdmin {
// 清除本地缓存
m.UserLocalCache.DelUserInfo(ctx, data.MsgData.SendID)
// 重新获取用户信息
u, err = m.UserLocalCache.GetUserInfo(ctx, data.MsgData.SendID)
if err != nil {
return err
}
userType = u.GetUserType()
}
canSendFile := isSystemAdmin || userType == 1 || isGroupOwner || isGroupAdmin
if !canSendFile {
return servererrs.ErrNoPermission.WrapMsg("only group owner, admin, or userType=1 can send files in group chat")
}
}
if isSystemAdmin {
// 系统管理员跳过大部分检查
return nil
}
// 群聊中userType=1、群主、群管理员可以发送链接
isGroupOwner := groupMemberInfo.RoleLevel == constant.GroupOwner
isGroupAdmin := groupMemberInfo.RoleLevel == constant.GroupAdmin
canSendLink := u.GetUserType() == 1 || isGroupOwner || isGroupAdmin
// 如果不符合发送链接的条件,进行链接检测
if !canSendLink {
if err := m.checkMessageContainsLink(data.MsgData, u.GetUserType()); err != nil {
return err
}
}
// 群聊中检测userType=0的普通成员是否发送了包含二维码的图片
// userType=1、群主、群管理员可以发送包含二维码的图片不进行检测
if !canSendLink {
if err := m.checkImageContainsQRCode(ctx, data.MsgData, u.GetUserType()); err != nil {
return err
}
}
if isGroupOwner {
return nil
} else {
nowUnixMilli := time.Now().UnixMilli()
// 记录禁言检查信息,用于排查自动禁言问题
if groupMemberInfo.MuteEndTime > 0 {
muteEndTime := time.UnixMilli(groupMemberInfo.MuteEndTime)
isMuted := groupMemberInfo.MuteEndTime >= nowUnixMilli
log.ZInfo(ctx, "messageVerification: checking mute status",
"groupID", data.MsgData.GroupID,
"userID", data.MsgData.SendID,
"muteEndTimeTimestamp", groupMemberInfo.MuteEndTime,
"muteEndTime", muteEndTime.Format(time.RFC3339),
"now", time.UnixMilli(nowUnixMilli).Format(time.RFC3339),
"isMuted", isMuted,
"mutedDurationSeconds", (groupMemberInfo.MuteEndTime-nowUnixMilli)/1000,
"roleLevel", groupMemberInfo.RoleLevel)
}
if groupMemberInfo.MuteEndTime >= nowUnixMilli {
return servererrs.ErrMutedInGroup.Wrap()
}
if groupInfo.Status == constant.GroupStatusMuted && !isGroupAdmin {
return servererrs.ErrMutedGroup.Wrap()
}
}
return nil
default:
return nil
}
}
func (m *msgServer) encapsulateMsgData(msg *sdkws.MsgData) {
msg.ServerMsgID = GetMsgID(msg.SendID)
if msg.SendTime == 0 {
msg.SendTime = timeutil.GetCurrentTimestampByMill()
}
switch msg.ContentType {
case constant.Text, constant.Picture, constant.Voice, constant.Video,
constant.File, constant.AtText, constant.Merger, constant.Card,
constant.Location, constant.Custom, constant.Quote, constant.AdvancedText, constant.MarkdownText:
case constant.Revoke:
datautil.SetSwitchFromOptions(msg.Options, constant.IsUnreadCount, false)
datautil.SetSwitchFromOptions(msg.Options, constant.IsOfflinePush, false)
case constant.HasReadReceipt:
datautil.SetSwitchFromOptions(msg.Options, constant.IsConversationUpdate, false)
datautil.SetSwitchFromOptions(msg.Options, constant.IsSenderConversationUpdate, false)
datautil.SetSwitchFromOptions(msg.Options, constant.IsUnreadCount, false)
datautil.SetSwitchFromOptions(msg.Options, constant.IsOfflinePush, false)
case constant.Typing:
datautil.SetSwitchFromOptions(msg.Options, constant.IsHistory, false)
datautil.SetSwitchFromOptions(msg.Options, constant.IsPersistent, false)
datautil.SetSwitchFromOptions(msg.Options, constant.IsSenderSync, false)
datautil.SetSwitchFromOptions(msg.Options, constant.IsConversationUpdate, false)
datautil.SetSwitchFromOptions(msg.Options, constant.IsSenderConversationUpdate, false)
datautil.SetSwitchFromOptions(msg.Options, constant.IsUnreadCount, false)
datautil.SetSwitchFromOptions(msg.Options, constant.IsOfflinePush, false)
}
}
func GetMsgID(sendID string) string {
t := timeutil.GetCurrentTimeFormatted()
return encrypt.Md5(t + "-" + sendID + "-" + strconv.Itoa(rand.Int()))
}
func (m *msgServer) modifyMessageByUserMessageReceiveOpt(ctx context.Context, userID, conversationID string, sessionType int, pb *msg.SendMsgReq) (bool, error) {
opt, err := m.UserLocalCache.GetUserGlobalMsgRecvOpt(ctx, userID)
if err != nil {
return false, err
}
switch opt {
case constant.ReceiveMessage:
case constant.NotReceiveMessage:
return false, nil
case constant.ReceiveNotNotifyMessage:
if pb.MsgData.Options == nil {
pb.MsgData.Options = make(map[string]bool, 10)
}
datautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)
return true, nil
}
singleOpt, err := m.ConversationLocalCache.GetSingleConversationRecvMsgOpt(ctx, userID, conversationID)
if errs.ErrRecordNotFound.Is(err) {
return true, nil
} else if err != nil {
return false, err
}
switch singleOpt {
case constant.ReceiveMessage:
return true, nil
case constant.NotReceiveMessage:
if datautil.Contain(int(pb.MsgData.ContentType), ExcludeContentType...) {
return true, nil
}
return false, nil
case constant.ReceiveNotNotifyMessage:
if pb.MsgData.Options == nil {
pb.MsgData.Options = make(map[string]bool, 10)
}
datautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)
return true, nil
}
return true, nil
}