复制项目

This commit is contained in:
kim.dev.6789
2026-01-14 22:16:44 +08:00
parent e2577b8cee
commit e50142a3b9
691 changed files with 97009 additions and 1 deletions

231
internal/rpc/msg/as_read.go Normal file
View File

@@ -0,0 +1,231 @@
// 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"
"errors"
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
cbapi "git.imall.cloud/openim/open-im-server-deploy/pkg/callbackstruct"
"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"
"github.com/openimsdk/tools/utils/datautil"
"github.com/redis/go-redis/v9"
)
func (m *msgServer) GetConversationsHasReadAndMaxSeq(ctx context.Context, req *msg.GetConversationsHasReadAndMaxSeqReq) (*msg.GetConversationsHasReadAndMaxSeqResp, error) {
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
return nil, err
}
var conversationIDs []string
if len(req.ConversationIDs) == 0 {
var err error
conversationIDs, err = m.ConversationLocalCache.GetConversationIDs(ctx, req.UserID)
if err != nil {
return nil, err
}
} else {
conversationIDs = req.ConversationIDs
}
hasReadSeqs, err := m.MsgDatabase.GetHasReadSeqs(ctx, req.UserID, conversationIDs)
if err != nil {
return nil, err
}
conversations, err := m.ConversationLocalCache.GetConversations(ctx, req.UserID, conversationIDs)
if err != nil {
return nil, err
}
conversationMaxSeqMap := make(map[string]int64)
for _, conversation := range conversations {
if conversation.MaxSeq != 0 {
conversationMaxSeqMap[conversation.ConversationID] = conversation.MaxSeq
}
}
maxSeqs, err := m.MsgDatabase.GetMaxSeqsWithTime(ctx, conversationIDs)
if err != nil {
return nil, err
}
resp := &msg.GetConversationsHasReadAndMaxSeqResp{Seqs: make(map[string]*msg.Seqs)}
if req.ReturnPinned {
pinnedConversationIDs, err := m.ConversationLocalCache.GetPinnedConversationIDs(ctx, req.UserID)
if err != nil {
return nil, err
}
resp.PinnedConversationIDs = pinnedConversationIDs
}
for conversationID, maxSeq := range maxSeqs {
resp.Seqs[conversationID] = &msg.Seqs{
HasReadSeq: hasReadSeqs[conversationID],
MaxSeq: maxSeq.Seq,
MaxSeqTime: maxSeq.Time,
}
if v, ok := conversationMaxSeqMap[conversationID]; ok {
resp.Seqs[conversationID].MaxSeq = v
}
}
return resp, nil
}
func (m *msgServer) SetConversationHasReadSeq(ctx context.Context, req *msg.SetConversationHasReadSeqReq) (*msg.SetConversationHasReadSeqResp, error) {
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
return nil, err
}
maxSeq, err := m.MsgDatabase.GetMaxSeq(ctx, req.ConversationID)
if err != nil {
return nil, err
}
if req.HasReadSeq > maxSeq {
return nil, errs.ErrArgs.WrapMsg("hasReadSeq must not be bigger than maxSeq")
}
if err := m.MsgDatabase.SetHasReadSeq(ctx, req.UserID, req.ConversationID, req.HasReadSeq); err != nil {
return nil, err
}
m.sendMarkAsReadNotification(ctx, req.ConversationID, constant.SingleChatType, req.UserID, req.UserID, nil, req.HasReadSeq)
return &msg.SetConversationHasReadSeqResp{}, nil
}
func (m *msgServer) MarkMsgsAsRead(ctx context.Context, req *msg.MarkMsgsAsReadReq) (*msg.MarkMsgsAsReadResp, error) {
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
return nil, err
}
maxSeq, err := m.MsgDatabase.GetMaxSeq(ctx, req.ConversationID)
if err != nil {
return nil, err
}
hasReadSeq := req.Seqs[len(req.Seqs)-1]
if hasReadSeq > maxSeq {
return nil, errs.ErrArgs.WrapMsg("hasReadSeq must not be bigger than maxSeq")
}
conversation, err := m.ConversationLocalCache.GetConversation(ctx, req.UserID, req.ConversationID)
if err != nil {
return nil, err
}
webhookCfg := m.webhookConfig()
if err := m.MsgDatabase.MarkSingleChatMsgsAsRead(ctx, req.UserID, req.ConversationID, req.Seqs); err != nil {
return nil, err
}
currentHasReadSeq, err := m.MsgDatabase.GetHasReadSeq(ctx, req.UserID, req.ConversationID)
if err != nil && !errors.Is(err, redis.Nil) {
return nil, err
}
if hasReadSeq > currentHasReadSeq {
err = m.MsgDatabase.SetHasReadSeq(ctx, req.UserID, req.ConversationID, hasReadSeq)
if err != nil {
return nil, err
}
}
reqCallback := &cbapi.CallbackSingleMsgReadReq{
ConversationID: conversation.ConversationID,
UserID: req.UserID,
Seqs: req.Seqs,
ContentType: conversation.ConversationType,
}
m.webhookAfterSingleMsgRead(ctx, &webhookCfg.AfterSingleMsgRead, reqCallback)
m.sendMarkAsReadNotification(ctx, req.ConversationID, conversation.ConversationType, req.UserID,
m.conversationAndGetRecvID(conversation, req.UserID), req.Seqs, hasReadSeq)
return &msg.MarkMsgsAsReadResp{}, nil
}
func (m *msgServer) MarkConversationAsRead(ctx context.Context, req *msg.MarkConversationAsReadReq) (*msg.MarkConversationAsReadResp, error) {
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
return nil, err
}
conversation, err := m.ConversationLocalCache.GetConversation(ctx, req.UserID, req.ConversationID)
if err != nil {
return nil, err
}
webhookCfg := m.webhookConfig()
hasReadSeq, err := m.MsgDatabase.GetHasReadSeq(ctx, req.UserID, req.ConversationID)
if err != nil && !errors.Is(err, redis.Nil) {
return nil, err
}
var seqs []int64
log.ZDebug(ctx, "MarkConversationAsRead", "hasReadSeq", hasReadSeq, "req.HasReadSeq", req.HasReadSeq)
if conversation.ConversationType == constant.SingleChatType {
for i := hasReadSeq + 1; i <= req.HasReadSeq; i++ {
seqs = append(seqs, i)
}
// avoid client missed call MarkConversationMessageAsRead by order
for _, val := range req.Seqs {
if !datautil.Contain(val, seqs...) {
seqs = append(seqs, val)
}
}
if len(seqs) > 0 {
log.ZDebug(ctx, "MarkConversationAsRead", "seqs", seqs, "conversationID", req.ConversationID)
if err = m.MsgDatabase.MarkSingleChatMsgsAsRead(ctx, req.UserID, req.ConversationID, seqs); err != nil {
return nil, err
}
}
if req.HasReadSeq > hasReadSeq {
err = m.MsgDatabase.SetHasReadSeq(ctx, req.UserID, req.ConversationID, req.HasReadSeq)
if err != nil {
return nil, err
}
hasReadSeq = req.HasReadSeq
}
m.sendMarkAsReadNotification(ctx, req.ConversationID, conversation.ConversationType, req.UserID,
m.conversationAndGetRecvID(conversation, req.UserID), seqs, hasReadSeq)
} else if conversation.ConversationType == constant.ReadGroupChatType ||
conversation.ConversationType == constant.NotificationChatType {
if req.HasReadSeq > hasReadSeq {
err = m.MsgDatabase.SetHasReadSeq(ctx, req.UserID, req.ConversationID, req.HasReadSeq)
if err != nil {
return nil, err
}
hasReadSeq = req.HasReadSeq
}
m.sendMarkAsReadNotification(ctx, req.ConversationID, constant.SingleChatType, req.UserID,
req.UserID, seqs, hasReadSeq)
}
if conversation.ConversationType == constant.SingleChatType {
reqCall := &cbapi.CallbackSingleMsgReadReq{
ConversationID: conversation.ConversationID,
UserID: conversation.OwnerUserID,
Seqs: req.Seqs,
ContentType: conversation.ConversationType,
}
m.webhookAfterSingleMsgRead(ctx, &webhookCfg.AfterSingleMsgRead, reqCall)
} else if conversation.ConversationType == constant.ReadGroupChatType {
reqCall := &cbapi.CallbackGroupMsgReadReq{
SendID: conversation.OwnerUserID,
ReceiveID: req.UserID,
UnreadMsgNum: req.HasReadSeq,
ContentType: int64(conversation.ConversationType),
}
m.webhookAfterGroupMsgRead(ctx, &webhookCfg.AfterGroupMsgRead, reqCall)
}
return &msg.MarkConversationAsReadResp{}, nil
}
func (m *msgServer) sendMarkAsReadNotification(ctx context.Context, conversationID string, sessionType int32, sendID, recvID string, seqs []int64, hasReadSeq int64) {
tips := &sdkws.MarkAsReadTips{
MarkAsReadUserID: sendID,
ConversationID: conversationID,
Seqs: seqs,
HasReadSeq: hasReadSeq,
}
m.notificationSender.NotificationWithSessionType(ctx, sendID, recvID, constant.HasReadReceipt, sessionType, tips)
}

View File

@@ -0,0 +1,236 @@
// 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/base64"
"encoding/json"
"git.imall.cloud/openim/open-im-server-deploy/pkg/apistruct"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
cbapi "git.imall.cloud/openim/open-im-server-deploy/pkg/callbackstruct"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
"git.imall.cloud/openim/protocol/constant"
pbchat "git.imall.cloud/openim/protocol/msg"
"git.imall.cloud/openim/protocol/sdkws"
"github.com/openimsdk/tools/mcontext"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/tools/utils/stringutil"
"google.golang.org/protobuf/proto"
)
func toCommonCallback(ctx context.Context, msg *pbchat.SendMsgReq, command string) cbapi.CommonCallbackReq {
return cbapi.CommonCallbackReq{
SendID: msg.MsgData.SendID,
ServerMsgID: msg.MsgData.ServerMsgID,
CallbackCommand: command,
ClientMsgID: msg.MsgData.ClientMsgID,
OperationID: mcontext.GetOperationID(ctx),
SenderPlatformID: msg.MsgData.SenderPlatformID,
SenderNickname: msg.MsgData.SenderNickname,
SessionType: msg.MsgData.SessionType,
MsgFrom: msg.MsgData.MsgFrom,
ContentType: msg.MsgData.ContentType,
Status: msg.MsgData.Status,
SendTime: msg.MsgData.SendTime,
CreateTime: msg.MsgData.CreateTime,
AtUserIDList: msg.MsgData.AtUserIDList,
SenderFaceURL: msg.MsgData.SenderFaceURL,
Content: GetContent(msg.MsgData),
Seq: uint32(msg.MsgData.Seq),
Ex: msg.MsgData.Ex,
}
}
func GetContent(msg *sdkws.MsgData) string {
if msg.ContentType >= constant.NotificationBegin && msg.ContentType <= constant.NotificationEnd {
var tips sdkws.TipsComm
_ = proto.Unmarshal(msg.Content, &tips)
content := tips.JsonDetail
return content
} else {
return string(msg.Content)
}
}
func (m *msgServer) webhookBeforeSendSingleMsg(ctx context.Context, before *config.BeforeConfig, msg *pbchat.SendMsgReq) error {
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
if msg.MsgData.ContentType == constant.Typing {
return nil
}
if !filterBeforeMsg(msg, before) {
return nil
}
cbReq := &cbapi.CallbackBeforeSendSingleMsgReq{
CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackBeforeSendSingleMsgCommand),
RecvID: msg.MsgData.RecvID,
}
resp := &cbapi.CallbackBeforeSendSingleMsgResp{}
if err := m.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
return err
}
return nil
})
}
func (m *msgServer) webhookAfterSendSingleMsg(ctx context.Context, after *config.AfterConfig, msg *pbchat.SendMsgReq) {
if msg.MsgData.ContentType == constant.Typing {
return
}
if !filterAfterMsg(msg, after) {
return
}
cbReq := &cbapi.CallbackAfterSendSingleMsgReq{
CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterSendSingleMsgCommand),
RecvID: msg.MsgData.RecvID,
}
m.webhookClient.AsyncPostWithQuery(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterSendSingleMsgResp{}, after, buildKeyMsgDataQuery(msg.MsgData))
}
func (m *msgServer) webhookBeforeSendGroupMsg(ctx context.Context, before *config.BeforeConfig, msg *pbchat.SendMsgReq) error {
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
if !filterBeforeMsg(msg, before) {
return nil
}
if msg.MsgData.ContentType == constant.Typing {
return nil
}
cbReq := &cbapi.CallbackBeforeSendGroupMsgReq{
CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackBeforeSendGroupMsgCommand),
GroupID: msg.MsgData.GroupID,
}
resp := &cbapi.CallbackBeforeSendGroupMsgResp{}
if err := m.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
return err
}
return nil
})
}
func (m *msgServer) webhookAfterSendGroupMsg(ctx context.Context, after *config.AfterConfig, msg *pbchat.SendMsgReq) {
if after == nil {
return
}
if msg.MsgData.ContentType == constant.Typing {
log.ZDebug(ctx, "webhook skipped: typing message", "contentType", msg.MsgData.ContentType)
return
}
log.ZInfo(ctx, "webhook afterSendGroupMsg checking", "enable", after.Enable, "groupID", msg.MsgData.GroupID, "contentType", msg.MsgData.ContentType, "attentionIds", after.AttentionIds, "deniedTypes", after.DeniedTypes)
if !filterAfterMsg(msg, after) {
log.ZDebug(ctx, "webhook filtered out by filterAfterMsg", "groupID", msg.MsgData.GroupID)
return
}
if !after.Enable {
log.ZDebug(ctx, "webhook afterSendGroupMsg disabled, skipping", "enable", after.Enable)
return
}
log.ZInfo(ctx, "webhook afterSendGroupMsg sending", "groupID", msg.MsgData.GroupID, "sendID", msg.MsgData.SendID, "contentType", msg.MsgData.ContentType)
cbReq := &cbapi.CallbackAfterSendGroupMsgReq{
CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterSendGroupMsgCommand),
GroupID: msg.MsgData.GroupID,
}
m.webhookClient.AsyncPostWithQuery(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterSendGroupMsgResp{}, after, buildKeyMsgDataQuery(msg.MsgData))
}
func (m *msgServer) webhookBeforeMsgModify(ctx context.Context, before *config.BeforeConfig, msg *pbchat.SendMsgReq, beforeMsgData **sdkws.MsgData) error {
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
//if msg.MsgData.ContentType != constant.Text {
// return nil
//}
if !filterBeforeMsg(msg, before) {
return nil
}
cbReq := &cbapi.CallbackMsgModifyCommandReq{
CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackBeforeMsgModifyCommand),
}
resp := &cbapi.CallbackMsgModifyCommandResp{}
if err := m.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
return err
}
if beforeMsgData != nil {
*beforeMsgData = proto.Clone(msg.MsgData).(*sdkws.MsgData)
}
if resp.Content != nil {
msg.MsgData.Content = []byte(*resp.Content)
if err := json.Unmarshal(msg.MsgData.Content, &struct{}{}); err != nil {
return errs.ErrArgs.WrapMsg("webhook msg modify content is not json", "content", string(msg.MsgData.Content))
}
}
datautil.NotNilReplace(msg.MsgData.OfflinePushInfo, resp.OfflinePushInfo)
datautil.NotNilReplace(&msg.MsgData.RecvID, resp.RecvID)
datautil.NotNilReplace(&msg.MsgData.GroupID, resp.GroupID)
datautil.NotNilReplace(&msg.MsgData.ClientMsgID, resp.ClientMsgID)
datautil.NotNilReplace(&msg.MsgData.ServerMsgID, resp.ServerMsgID)
datautil.NotNilReplace(&msg.MsgData.SenderPlatformID, resp.SenderPlatformID)
datautil.NotNilReplace(&msg.MsgData.SenderNickname, resp.SenderNickname)
datautil.NotNilReplace(&msg.MsgData.SenderFaceURL, resp.SenderFaceURL)
datautil.NotNilReplace(&msg.MsgData.SessionType, resp.SessionType)
datautil.NotNilReplace(&msg.MsgData.MsgFrom, resp.MsgFrom)
datautil.NotNilReplace(&msg.MsgData.ContentType, resp.ContentType)
datautil.NotNilReplace(&msg.MsgData.Status, resp.Status)
datautil.NotNilReplace(&msg.MsgData.Options, resp.Options)
datautil.NotNilReplace(&msg.MsgData.AtUserIDList, resp.AtUserIDList)
datautil.NotNilReplace(&msg.MsgData.AttachedInfo, resp.AttachedInfo)
datautil.NotNilReplace(&msg.MsgData.Ex, resp.Ex)
return nil
})
}
func (m *msgServer) webhookAfterGroupMsgRead(ctx context.Context, after *config.AfterConfig, req *cbapi.CallbackGroupMsgReadReq) {
req.CallbackCommand = cbapi.CallbackAfterGroupMsgReadCommand
m.webhookClient.AsyncPost(ctx, req.GetCallbackCommand(), req, &cbapi.CallbackGroupMsgReadResp{}, after)
}
func (m *msgServer) webhookAfterSingleMsgRead(ctx context.Context, after *config.AfterConfig, req *cbapi.CallbackSingleMsgReadReq) {
req.CallbackCommand = cbapi.CallbackAfterSingleMsgReadCommand
m.webhookClient.AsyncPost(ctx, req.GetCallbackCommand(), req, &cbapi.CallbackSingleMsgReadResp{}, after)
}
func (m *msgServer) webhookAfterRevokeMsg(ctx context.Context, after *config.AfterConfig, req *pbchat.RevokeMsgReq) {
callbackReq := &cbapi.CallbackAfterRevokeMsgReq{
CallbackCommand: cbapi.CallbackAfterRevokeMsgCommand,
ConversationID: req.ConversationID,
Seq: req.Seq,
UserID: req.UserID,
}
m.webhookClient.AsyncPost(ctx, callbackReq.GetCallbackCommand(), callbackReq, &cbapi.CallbackAfterRevokeMsgResp{}, after)
}
func buildKeyMsgDataQuery(msg *sdkws.MsgData) map[string]string {
keyMsgData := apistruct.KeyMsgData{
SendID: msg.SendID,
RecvID: msg.RecvID,
GroupID: msg.GroupID,
}
return map[string]string{
webhook.Key: base64.StdEncoding.EncodeToString(stringutil.StructToJsonBytes(keyMsgData)),
}
}

61
internal/rpc/msg/clear.go Normal file
View File

@@ -0,0 +1,61 @@
package msg
import (
"context"
"strings"
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
"git.imall.cloud/openim/protocol/msg"
"github.com/openimsdk/tools/log"
)
// DestructMsgs hard delete in Database.
func (m *msgServer) DestructMsgs(ctx context.Context, req *msg.DestructMsgsReq) (*msg.DestructMsgsResp, error) {
if err := authverify.CheckAdmin(ctx); err != nil {
return nil, err
}
docs, err := m.MsgDatabase.GetRandBeforeMsg(ctx, req.Timestamp, int(req.Limit))
if err != nil {
return nil, err
}
for i, doc := range docs {
if err := m.MsgDatabase.DeleteDoc(ctx, doc.DocID); err != nil {
return nil, err
}
log.ZDebug(ctx, "DestructMsgs delete doc", "index", i, "docID", doc.DocID)
index := strings.LastIndex(doc.DocID, ":")
if index < 0 {
continue
}
var minSeq int64
for _, model := range doc.Msg {
if model.Msg == nil {
continue
}
if model.Msg.Seq > minSeq {
minSeq = model.Msg.Seq
}
}
if minSeq <= 0 {
continue
}
conversationID := doc.DocID[:index]
if conversationID == "" {
continue
}
minSeq++
if err := m.MsgDatabase.SetMinSeq(ctx, conversationID, minSeq); err != nil {
return nil, err
}
log.ZDebug(ctx, "DestructMsgs delete doc set min seq", "index", i, "docID", doc.DocID, "conversationID", conversationID, "setMinSeq", minSeq)
}
return &msg.DestructMsgsResp{Count: int32(len(docs))}, nil
}
func (m *msgServer) GetLastMessageSeqByTime(ctx context.Context, req *msg.GetLastMessageSeqByTimeReq) (*msg.GetLastMessageSeqByTimeResp, error) {
seq, err := m.MsgDatabase.GetLastMessageSeqByTime(ctx, req.ConversationID, req.Time)
if err != nil {
return nil, err
}
return &msg.GetLastMessageSeqByTimeResp{Seq: seq}, nil
}

251
internal/rpc/msg/delete.go Normal file
View File

@@ -0,0 +1,251 @@
// 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"
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
"git.imall.cloud/openim/protocol/constant"
"git.imall.cloud/openim/protocol/conversation"
"git.imall.cloud/openim/protocol/msg"
"git.imall.cloud/openim/protocol/sdkws"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/tools/utils/timeutil"
)
func (m *msgServer) getMinSeqs(maxSeqs map[string]int64) map[string]int64 {
minSeqs := make(map[string]int64)
for k, v := range maxSeqs {
minSeqs[k] = v + 1
}
return minSeqs
}
func (m *msgServer) validateDeleteSyncOpt(opt *msg.DeleteSyncOpt) (isSyncSelf, isSyncOther bool) {
if opt == nil {
return
}
return opt.IsSyncSelf, opt.IsSyncOther
}
func (m *msgServer) ClearConversationsMsg(ctx context.Context, req *msg.ClearConversationsMsgReq) (*msg.ClearConversationsMsgResp, error) {
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
return nil, err
}
if err := m.clearConversation(ctx, req.ConversationIDs, req.UserID, req.DeleteSyncOpt); err != nil {
return nil, err
}
return &msg.ClearConversationsMsgResp{}, nil
}
func (m *msgServer) UserClearAllMsg(ctx context.Context, req *msg.UserClearAllMsgReq) (*msg.UserClearAllMsgResp, error) {
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
return nil, err
}
conversationIDs, err := m.ConversationLocalCache.GetConversationIDs(ctx, req.UserID)
if err != nil {
return nil, err
}
if err := m.clearConversation(ctx, conversationIDs, req.UserID, req.DeleteSyncOpt); err != nil {
return nil, err
}
return &msg.UserClearAllMsgResp{}, nil
}
func (m *msgServer) DeleteMsgs(ctx context.Context, req *msg.DeleteMsgsReq) (*msg.DeleteMsgsResp, error) {
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
return nil, err
}
// 获取要删除的消息信息,用于权限检查
_, _, msgs, err := m.MsgDatabase.GetMsgBySeqs(ctx, req.UserID, req.ConversationID, req.Seqs)
if err != nil {
return nil, err
}
if len(msgs) == 0 {
return nil, errs.ErrRecordNotFound.WrapMsg("messages not found")
}
// 权限检查:如果不是管理员,需要检查删除权限
if !authverify.IsAdmin(ctx) {
// 收集所有消息的发送者ID
sendIDs := make([]string, 0, len(msgs))
for _, msg := range msgs {
if msg != nil && msg.SendID != "" {
sendIDs = append(sendIDs, msg.SendID)
}
}
sendIDs = datautil.Distinct(sendIDs)
// 检查第一条消息的会话类型(假设所有消息来自同一会话)
sessionType := msgs[0].SessionType
switch sessionType {
case constant.SingleChatType:
// 单聊:只能删除自己发送的消息
for _, msg := range msgs {
if msg != nil && msg.SendID != req.UserID {
return nil, errs.ErrNoPermission.WrapMsg("can only delete own messages in single chat")
}
}
case constant.ReadGroupChatType:
// 群聊:检查权限
groupID := msgs[0].GroupID
if groupID == "" {
return nil, errs.ErrArgs.WrapMsg("groupID is empty")
}
// 获取操作者和所有消息发送者的群成员信息
allUserIDs := append([]string{req.UserID}, sendIDs...)
members, err := m.GroupLocalCache.GetGroupMemberInfoMap(ctx, groupID, datautil.Distinct(allUserIDs))
if err != nil {
return nil, err
}
// 检查操作者的角色
opMember, ok := members[req.UserID]
if !ok {
return nil, errs.ErrNoPermission.WrapMsg("user not in group")
}
// 检查每条消息的删除权限
for _, msg := range msgs {
if msg == nil || msg.SendID == "" {
continue
}
// 如果是自己发送的消息,可以删除
if msg.SendID == req.UserID {
continue
}
// 如果不是自己发送的消息,需要检查权限
switch opMember.RoleLevel {
case constant.GroupOwner:
// 群主可以删除任何人的消息
case constant.GroupAdmin:
// 管理员只能删除普通成员的消息
sendMember, ok := members[msg.SendID]
if !ok {
return nil, errs.ErrNoPermission.WrapMsg("message sender not in group")
}
if sendMember.RoleLevel != constant.GroupOrdinaryUsers {
return nil, errs.ErrNoPermission.WrapMsg("group admin can only delete messages from ordinary members")
}
default:
// 普通成员只能删除自己的消息
return nil, errs.ErrNoPermission.WrapMsg("can only delete own messages")
}
}
default:
return nil, errs.ErrInternalServer.WrapMsg("sessionType not supported", "sessionType", sessionType)
}
}
isSyncSelf, isSyncOther := m.validateDeleteSyncOpt(req.DeleteSyncOpt)
if isSyncOther {
if err := m.MsgDatabase.DeleteMsgsPhysicalBySeqs(ctx, req.ConversationID, req.Seqs); err != nil {
return nil, err
}
conv, err := m.conversationClient.GetConversationsByConversationID(ctx, req.ConversationID)
if err != nil {
return nil, err
}
tips := &sdkws.DeleteMsgsTips{UserID: req.UserID, ConversationID: req.ConversationID, Seqs: req.Seqs}
m.notificationSender.NotificationWithSessionType(ctx, req.UserID, m.conversationAndGetRecvID(conv, req.UserID),
constant.DeleteMsgsNotification, conv.ConversationType, tips)
} else {
if err := m.MsgDatabase.DeleteUserMsgsBySeqs(ctx, req.UserID, req.ConversationID, req.Seqs); err != nil {
return nil, err
}
if isSyncSelf {
tips := &sdkws.DeleteMsgsTips{UserID: req.UserID, ConversationID: req.ConversationID, Seqs: req.Seqs}
m.notificationSender.NotificationWithSessionType(ctx, req.UserID, req.UserID, constant.DeleteMsgsNotification, constant.SingleChatType, tips)
}
}
return &msg.DeleteMsgsResp{}, nil
}
func (m *msgServer) DeleteMsgPhysicalBySeq(ctx context.Context, req *msg.DeleteMsgPhysicalBySeqReq) (*msg.DeleteMsgPhysicalBySeqResp, error) {
if err := authverify.CheckAdmin(ctx); err != nil {
return nil, err
}
err := m.MsgDatabase.DeleteMsgsPhysicalBySeqs(ctx, req.ConversationID, req.Seqs)
if err != nil {
return nil, err
}
return &msg.DeleteMsgPhysicalBySeqResp{}, nil
}
func (m *msgServer) DeleteMsgPhysical(ctx context.Context, req *msg.DeleteMsgPhysicalReq) (*msg.DeleteMsgPhysicalResp, error) {
if err := authverify.CheckAdmin(ctx); err != nil {
return nil, err
}
remainTime := timeutil.GetCurrentTimestampBySecond() - req.Timestamp
if _, err := m.DestructMsgs(ctx, &msg.DestructMsgsReq{Timestamp: remainTime, Limit: 9999}); err != nil {
return nil, err
}
return &msg.DeleteMsgPhysicalResp{}, nil
}
func (m *msgServer) clearConversation(ctx context.Context, conversationIDs []string, userID string, deleteSyncOpt *msg.DeleteSyncOpt) error {
conversations, err := m.conversationClient.GetConversationsByConversationIDs(ctx, conversationIDs)
if err != nil {
return err
}
var existConversations []*conversation.Conversation
var existConversationIDs []string
for _, conversation := range conversations {
existConversations = append(existConversations, conversation)
existConversationIDs = append(existConversationIDs, conversation.ConversationID)
}
log.ZDebug(ctx, "ClearConversationsMsg", "existConversationIDs", existConversationIDs)
maxSeqs, err := m.MsgDatabase.GetMaxSeqs(ctx, existConversationIDs)
if err != nil {
return err
}
isSyncSelf, isSyncOther := m.validateDeleteSyncOpt(deleteSyncOpt)
if !isSyncOther {
setSeqs := m.getMinSeqs(maxSeqs)
if err := m.MsgDatabase.SetUserConversationsMinSeqs(ctx, userID, setSeqs); err != nil {
return err
}
ownerUserIDs := []string{userID}
for conversationID, seq := range setSeqs {
if err := m.conversationClient.SetConversationMinSeq(ctx, conversationID, ownerUserIDs, seq); err != nil {
return err
}
}
// notification 2 self
if isSyncSelf {
tips := &sdkws.ClearConversationTips{UserID: userID, ConversationIDs: existConversationIDs}
m.notificationSender.NotificationWithSessionType(ctx, userID, userID, constant.ClearConversationNotification, constant.SingleChatType, tips)
}
} else {
if err := m.MsgDatabase.SetMinSeqs(ctx, m.getMinSeqs(maxSeqs)); err != nil {
return err
}
for _, conversation := range existConversations {
tips := &sdkws.ClearConversationTips{UserID: userID, ConversationIDs: []string{conversation.ConversationID}}
m.notificationSender.NotificationWithSessionType(ctx, userID, m.conversationAndGetRecvID(conversation, userID), constant.ClearConversationNotification, conversation.ConversationType, tips)
}
}
if err := m.MsgDatabase.UserSetHasReadSeqs(ctx, userID, maxSeqs); err != nil {
return err
}
return nil
}

106
internal/rpc/msg/filter.go Normal file
View File

@@ -0,0 +1,106 @@
package msg
import (
"strconv"
"strings"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
"git.imall.cloud/openim/protocol/constant"
pbchat "git.imall.cloud/openim/protocol/msg"
"github.com/openimsdk/tools/utils/datautil"
)
const (
separator = "-"
)
func filterAfterMsg(msg *pbchat.SendMsgReq, after *config.AfterConfig) bool {
result := filterMsg(msg, after.AttentionIds, after.DeniedTypes)
// 添加调试日志
if !result {
// 只在过滤掉时记录,避免日志过多
}
return result
}
func filterBeforeMsg(msg *pbchat.SendMsgReq, before *config.BeforeConfig) bool {
return filterMsg(msg, nil, before.DeniedTypes)
}
func filterMsg(msg *pbchat.SendMsgReq, attentionIds []string, deniedTypes []int32) bool {
// According to the attentionIds configuration, only some users are sent
// 注意对于群消息应该检查GroupID而不是RecvID
if len(attentionIds) != 0 {
// 单聊消息检查RecvID群聊消息检查GroupID
if msg.MsgData.SessionType == constant.SingleChatType {
if !datautil.Contain(msg.MsgData.RecvID, attentionIds...) {
return false
}
} else if msg.MsgData.SessionType == constant.ReadGroupChatType {
if !datautil.Contain(msg.MsgData.GroupID, attentionIds...) {
return false
}
}
}
if defaultDeniedTypes(msg.MsgData.ContentType) {
return false
}
if len(deniedTypes) != 0 && datautil.Contain(msg.MsgData.ContentType, deniedTypes...) {
return false
}
//if len(allowedTypes) != 0 && !isInInterval(msg.MsgData.ContentType, allowedTypes) {
// return false
//}
//if len(deniedTypes) != 0 && isInInterval(msg.MsgData.ContentType, deniedTypes) {
// return false
//}
return true
}
func defaultDeniedTypes(contentType int32) bool {
if contentType >= constant.NotificationBegin && contentType <= constant.NotificationEnd {
return true
}
if contentType == constant.Typing {
return true
}
return false
}
// isInInterval if data is in interval
// Supports two formats: a single type or a range. The range is defined by the lower and upper bounds connected with a hyphen ("-")
// e.g. [1, 100, 200-500, 600-700] means that only data within the range
// {1, 100} [200, 500] [600, 700] will return true.
func isInInterval(data int32, interval []string) bool {
for _, v := range interval {
if strings.Contains(v, separator) {
// is interval
bounds := strings.Split(v, separator)
if len(bounds) != 2 {
continue
}
bottom, err := strconv.Atoi(bounds[0])
if err != nil {
continue
}
top, err := strconv.Atoi(bounds[1])
if err != nil {
continue
}
if datautil.BetweenEq(int(data), bottom, top) {
return true
}
} else {
iv, err := strconv.Atoi(v)
if err != nil {
continue
}
if int(data) == iv {
return true
}
}
}
return false
}

View File

@@ -0,0 +1,44 @@
// 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"
"git.imall.cloud/openim/protocol/constant"
pbmsg "git.imall.cloud/openim/protocol/msg"
"github.com/openimsdk/tools/mcontext"
)
func (m *msgServer) SetSendMsgStatus(ctx context.Context, req *pbmsg.SetSendMsgStatusReq) (*pbmsg.SetSendMsgStatusResp, error) {
resp := &pbmsg.SetSendMsgStatusResp{}
if err := m.MsgDatabase.SetSendMsgStatus(ctx, mcontext.GetOperationID(ctx), req.Status); err != nil {
return nil, err
}
return resp, nil
}
func (m *msgServer) GetSendMsgStatus(ctx context.Context, req *pbmsg.GetSendMsgStatusReq) (*pbmsg.GetSendMsgStatusResp, error) {
resp := &pbmsg.GetSendMsgStatusResp{}
status, err := m.MsgDatabase.GetSendMsgStatus(ctx, mcontext.GetOperationID(ctx))
if IsNotFound(err) {
resp.Status = constant.MsgStatusNotExist
return resp, nil
} else if err != nil {
return nil, err
}
resp.Status = status
return resp, nil
}

View File

@@ -0,0 +1,50 @@
// 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"
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification"
"git.imall.cloud/openim/protocol/constant"
"git.imall.cloud/openim/protocol/sdkws"
)
type MsgNotificationSender struct {
*notification.NotificationSender
}
func NewMsgNotificationSender(config *Config, opts ...notification.NotificationSenderOptions) *MsgNotificationSender {
return &MsgNotificationSender{notification.NewNotificationSender(&config.NotificationConfig, opts...)}
}
func (m *MsgNotificationSender) UserDeleteMsgsNotification(ctx context.Context, userID, conversationID string, seqs []int64) {
tips := sdkws.DeleteMsgsTips{
UserID: userID,
ConversationID: conversationID,
Seqs: seqs,
}
m.Notification(ctx, userID, userID, constant.DeleteMsgsNotification, &tips)
}
func (m *MsgNotificationSender) MarkAsReadNotification(ctx context.Context, conversationID string, sessionType int32, sendID, recvID string, seqs []int64, hasReadSeq int64) {
tips := &sdkws.MarkAsReadTips{
MarkAsReadUserID: sendID,
ConversationID: conversationID,
Seqs: seqs,
HasReadSeq: hasReadSeq,
}
m.NotificationWithSessionType(ctx, sendID, recvID, constant.HasReadReceipt, sessionType, tips)
}

View File

@@ -0,0 +1,971 @@
// 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"
"fmt"
"image"
"image/color"
_ "image/jpeg"
_ "image/png"
"math"
"os"
"sync"
"github.com/makiuchi-d/gozxing"
"github.com/makiuchi-d/gozxing/qrcode"
"github.com/openimsdk/tools/log"
)
// openImageFile 打开图片文件
func openImageFile(imagePath string) (*os.File, error) {
file, err := os.Open(imagePath)
if err != nil {
return nil, fmt.Errorf("无法打开文件: %v", err)
}
return file, nil
}
// QRDecoder 二维码解码器接口
type QRDecoder interface {
Name() string
Decode(ctx context.Context, imagePath string, logPrefix string) (bool, error) // 返回是否检测到二维码
}
// ============================================================================
// QuircDecoder - Quirc解码器包装
// ============================================================================
// QuircDecoder 使用 Quirc 库的解码器
type QuircDecoder struct {
detectFunc func([]uint8, int, int) (bool, error)
}
// NewQuircDecoder 创建Quirc解码器
func NewQuircDecoder(detectFunc func([]uint8, int, int) (bool, error)) *QuircDecoder {
return &QuircDecoder{detectFunc: detectFunc}
}
func (d *QuircDecoder) Name() string {
return "quirc"
}
func (d *QuircDecoder) Decode(ctx context.Context, imagePath string, logPrefix string) (bool, error) {
if d.detectFunc == nil {
return false, fmt.Errorf("quirc 解码器未启用")
}
// 打开并解码图片
file, err := openImageFile(imagePath)
if err != nil {
log.ZError(ctx, "打开图片文件失败", err, "imagePath", imagePath)
return false, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
log.ZError(ctx, "解码图片失败", err, "imagePath", imagePath)
return false, fmt.Errorf("无法解码图片: %v", err)
}
bounds := img.Bounds()
width := bounds.Dx()
height := bounds.Dy()
// 转换为灰度图
grayData := convertToGrayscale(img, width, height)
// 调用Quirc检测
hasQRCode, err := d.detectFunc(grayData, width, height)
if err != nil {
log.ZError(ctx, "Quirc检测失败", err, "width", width, "height", height)
return false, err
}
return hasQRCode, nil
}
// ============================================================================
// CustomQRDecoder - 自定义解码器(兼容圆形角)
// ============================================================================
// CustomQRDecoder 自定义二维码解码器,兼容圆形角等特殊格式
type CustomQRDecoder struct{}
func (d *CustomQRDecoder) Name() string {
return "custom (圆形角兼容)"
}
// Decode 解码二维码,返回是否检测到二维码
func (d *CustomQRDecoder) Decode(ctx context.Context, imagePath string, logPrefix string) (bool, error) {
file, err := openImageFile(imagePath)
if err != nil {
log.ZError(ctx, "打开图片文件失败", err, "imagePath", imagePath)
return false, fmt.Errorf("无法打开文件: %v", err)
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
log.ZError(ctx, "解码图片失败", err, "imagePath", imagePath)
return false, fmt.Errorf("无法解码图片: %v", err)
}
bounds := img.Bounds()
width := bounds.Dx()
height := bounds.Dy()
reader := qrcode.NewQRCodeReader()
hints := make(map[gozxing.DecodeHintType]interface{})
hints[gozxing.DecodeHintType_TRY_HARDER] = true
hints[gozxing.DecodeHintType_PURE_BARCODE] = false
hints[gozxing.DecodeHintType_CHARACTER_SET] = "UTF-8"
// 尝试直接解码
bitmap, err := gozxing.NewBinaryBitmapFromImage(img)
if err == nil {
if _, err := reader.Decode(bitmap, hints); err == nil {
return true, nil
}
// 尝试不使用PURE_BARCODE
delete(hints, gozxing.DecodeHintType_PURE_BARCODE)
if _, err := reader.Decode(bitmap, hints); err == nil {
return true, nil
}
hints[gozxing.DecodeHintType_PURE_BARCODE] = false
}
// 尝试多尺度缩放
scales := []float64{1.0, 1.5, 2.0, 0.75, 0.5}
for _, scale := range scales {
scaledImg := scaleImage(img, width, height, scale)
if scaledImg == nil {
continue
}
scaledBitmap, err := gozxing.NewBinaryBitmapFromImage(scaledImg)
if err == nil {
if _, err := reader.Decode(scaledBitmap, hints); err == nil {
return true, nil
}
}
}
// 转换为灰度图进行预处理
grayData := convertToGrayscale(img, width, height)
// 尝试多种预处理方法
preprocessMethods := []struct {
name string
fn func([]byte, int, int) []byte
}{
{"Otsu二值化", enhanceImageOtsu},
{"标准增强", enhanceImage},
{"强对比度", enhanceImageStrong},
{"圆形角处理", enhanceImageForRoundedCorners},
{"去噪+锐化", enhanceImageDenoiseSharpen},
{"高斯模糊+锐化", enhanceImageGaussianSharpen},
}
scalesForPreprocessed := []float64{1.0, 2.0, 1.5, 1.2, 0.8}
for _, method := range preprocessMethods {
processed := method.fn(grayData, width, height)
// 快速检测定位图案
corners := detectCornersFast(processed, width, height)
if len(corners) < 2 {
// 如果没有检测到足够的定位图案,仍然尝试解码
}
processedImg := createImageFromGrayscale(processed, width, height)
bitmap2, err := gozxing.NewBinaryBitmapFromImage(processedImg)
if err == nil {
if _, err := reader.Decode(bitmap2, hints); err == nil {
return true, nil
}
delete(hints, gozxing.DecodeHintType_PURE_BARCODE)
if _, err := reader.Decode(bitmap2, hints); err == nil {
return true, nil
}
hints[gozxing.DecodeHintType_PURE_BARCODE] = false
}
// 对预处理后的图像进行多尺度缩放
for _, scale := range scalesForPreprocessed {
scaledProcessed := scaleGrayscaleImage(processed, width, height, scale)
if scaledProcessed == nil {
continue
}
scaledImg := createImageFromGrayscale(scaledProcessed.data, scaledProcessed.width, scaledProcessed.height)
scaledBitmap, err := gozxing.NewBinaryBitmapFromImage(scaledImg)
if err == nil {
if _, err := reader.Decode(scaledBitmap, hints); err == nil {
return true, nil
}
delete(hints, gozxing.DecodeHintType_PURE_BARCODE)
if _, err := reader.Decode(scaledBitmap, hints); err == nil {
return true, nil
}
hints[gozxing.DecodeHintType_PURE_BARCODE] = false
}
}
}
return false, nil
}
// ============================================================================
// ParallelQRDecoder - 并行解码器
// ============================================================================
// ParallelQRDecoder 并行解码器,同时运行 quirc 和 custom 解码器
type ParallelQRDecoder struct {
quircDecoder QRDecoder
customDecoder QRDecoder
}
// NewParallelQRDecoder 创建并行解码器
func NewParallelQRDecoder(quircDecoder, customDecoder QRDecoder) *ParallelQRDecoder {
return &ParallelQRDecoder{
quircDecoder: quircDecoder,
customDecoder: customDecoder,
}
}
func (d *ParallelQRDecoder) Name() string {
return "parallel (quirc + custom)"
}
// Decode 并行解码:同时运行 quirc 和 custom任一成功立即返回
func (d *ParallelQRDecoder) Decode(ctx context.Context, imagePath string, logPrefix string) (bool, error) {
type decodeResult struct {
hasQRCode bool
err error
name string
}
resultChan := make(chan decodeResult, 2)
var wg sync.WaitGroup
var mu sync.Mutex
var quircErr error
var customErr error
// 启动Quirc解码
if d.quircDecoder != nil {
wg.Add(1)
go func() {
defer wg.Done()
hasQRCode, err := d.quircDecoder.Decode(ctx, imagePath, logPrefix)
mu.Lock()
if err != nil {
quircErr = err
}
mu.Unlock()
resultChan <- decodeResult{
hasQRCode: hasQRCode,
err: err,
name: d.quircDecoder.Name(),
}
}()
}
// 启动Custom解码
if d.customDecoder != nil {
wg.Add(1)
go func() {
defer wg.Done()
hasQRCode, err := d.customDecoder.Decode(ctx, imagePath, logPrefix)
mu.Lock()
if err != nil {
customErr = err
}
mu.Unlock()
resultChan <- decodeResult{
hasQRCode: hasQRCode,
err: err,
name: d.customDecoder.Name(),
}
}()
}
// 等待第一个结果
var firstResult decodeResult
var secondResult decodeResult
firstResult = <-resultChan
if firstResult.hasQRCode {
// 如果检测到二维码,立即返回
go func() {
<-resultChan
wg.Wait()
}()
return true, nil
}
// 等待第二个结果
if d.quircDecoder != nil && d.customDecoder != nil {
secondResult = <-resultChan
if secondResult.hasQRCode {
wg.Wait()
return true, nil
}
}
wg.Wait()
// 如果都失败,返回错误
if firstResult.err != nil && secondResult.err != nil {
log.ZError(ctx, "并行解码失败,两个解码器都失败", fmt.Errorf("quirc错误=%v, custom错误=%v", quircErr, customErr),
"quircError", quircErr,
"customError", customErr)
return false, fmt.Errorf("quirc 和 custom 都解码失败: quirc错误=%v, custom错误=%v", quircErr, customErr)
}
return false, nil
}
// ============================================================================
// 辅助函数
// ============================================================================
// convertToGrayscale 转换为灰度图
func convertToGrayscale(img image.Image, width, height int) []byte {
grayData := make([]byte, width*height)
bounds := img.Bounds()
if ycbcr, ok := img.(*image.YCbCr); ok {
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
yi := ycbcr.YOffset(x+bounds.Min.X, y+bounds.Min.Y)
grayData[y*width+x] = ycbcr.Y[yi]
}
}
return grayData
}
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
r, g, b, _ := img.At(x+bounds.Min.X, y+bounds.Min.Y).RGBA()
r8 := uint8(r >> 8)
g8 := uint8(g >> 8)
b8 := uint8(b >> 8)
gray := byte((uint16(r8)*299 + uint16(g8)*587 + uint16(b8)*114) / 1000)
grayData[y*width+x] = gray
}
}
return grayData
}
// enhanceImage 图像增强(标准方法)
func enhanceImage(data []byte, width, height int) []byte {
enhanced := make([]byte, len(data))
copy(enhanced, data)
minVal := uint8(255)
maxVal := uint8(0)
for _, v := range data {
if v < minVal {
minVal = v
}
if v > maxVal {
maxVal = v
}
}
if maxVal-minVal < 50 {
rangeVal := maxVal - minVal
if rangeVal == 0 {
rangeVal = 1
}
for i, v := range data {
stretched := uint8((uint16(v-minVal) * 255) / uint16(rangeVal))
enhanced[i] = stretched
}
}
return enhanced
}
// enhanceImageStrong 强对比度增强
func enhanceImageStrong(data []byte, width, height int) []byte {
enhanced := make([]byte, len(data))
histogram := make([]int, 256)
for _, v := range data {
histogram[v]++
}
cdf := make([]int, 256)
cdf[0] = histogram[0]
for i := 1; i < 256; i++ {
cdf[i] = cdf[i-1] + histogram[i]
}
total := len(data)
for i, v := range data {
if total > 0 {
enhanced[i] = uint8((cdf[v] * 255) / total)
}
}
return enhanced
}
// enhanceImageForRoundedCorners 针对圆形角的特殊处理
func enhanceImageForRoundedCorners(data []byte, width, height int) []byte {
enhanced := make([]byte, len(data))
copy(enhanced, data)
minVal := uint8(255)
maxVal := uint8(0)
for _, v := range data {
if v < minVal {
minVal = v
}
if v > maxVal {
maxVal = v
}
}
if maxVal-minVal < 100 {
rangeVal := maxVal - minVal
if rangeVal == 0 {
rangeVal = 1
}
for i, v := range data {
stretched := uint8((uint16(v-minVal) * 255) / uint16(rangeVal))
enhanced[i] = stretched
}
}
// 形态学操作:先腐蚀后膨胀
dilated := make([]byte, len(enhanced))
kernelSize := 3
halfKernel := kernelSize / 2
// 腐蚀(最小值滤波)
for y := halfKernel; y < height-halfKernel; y++ {
for x := halfKernel; x < width-halfKernel; x++ {
minVal := uint8(255)
for ky := -halfKernel; ky <= halfKernel; ky++ {
for kx := -halfKernel; kx <= halfKernel; kx++ {
idx := (y+ky)*width + (x + kx)
if enhanced[idx] < minVal {
minVal = enhanced[idx]
}
}
}
dilated[y*width+x] = minVal
}
}
// 膨胀(最大值滤波)
for y := halfKernel; y < height-halfKernel; y++ {
for x := halfKernel; x < width-halfKernel; x++ {
maxVal := uint8(0)
for ky := -halfKernel; ky <= halfKernel; ky++ {
for kx := -halfKernel; kx <= halfKernel; kx++ {
idx := (y+ky)*width + (x + kx)
if dilated[idx] > maxVal {
maxVal = dilated[idx]
}
}
}
enhanced[y*width+x] = maxVal
}
}
// 边界保持原值
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
if y < halfKernel || y >= height-halfKernel || x < halfKernel || x >= width-halfKernel {
enhanced[y*width+x] = data[y*width+x]
}
}
}
return enhanced
}
// enhanceImageDenoiseSharpen 去噪+锐化处理
func enhanceImageDenoiseSharpen(data []byte, width, height int) []byte {
denoised := medianFilter(data, width, height, 3)
sharpened := sharpenImage(denoised, width, height)
return sharpened
}
// medianFilter 中值滤波去噪
func medianFilter(data []byte, width, height, kernelSize int) []byte {
filtered := make([]byte, len(data))
halfKernel := kernelSize / 2
kernelArea := kernelSize * kernelSize
values := make([]byte, kernelArea)
for y := halfKernel; y < height-halfKernel; y++ {
for x := halfKernel; x < width-halfKernel; x++ {
idx := 0
for ky := -halfKernel; ky <= halfKernel; ky++ {
for kx := -halfKernel; kx <= halfKernel; kx++ {
values[idx] = data[(y+ky)*width+(x+kx)]
idx++
}
}
filtered[y*width+x] = quickSelectMedian(values)
}
}
// 边界保持原值
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
if y < halfKernel || y >= height-halfKernel || x < halfKernel || x >= width-halfKernel {
filtered[y*width+x] = data[y*width+x]
}
}
}
return filtered
}
// quickSelectMedian 快速选择中值
func quickSelectMedian(arr []byte) byte {
n := len(arr)
if n <= 7 {
// 小数组使用插入排序
for i := 1; i < n; i++ {
key := arr[i]
j := i - 1
for j >= 0 && arr[j] > key {
arr[j+1] = arr[j]
j--
}
arr[j+1] = key
}
return arr[n/2]
}
return quickSelect(arr, 0, n-1, n/2)
}
// quickSelect 快速选择第k小的元素
func quickSelect(arr []byte, left, right, k int) byte {
if left == right {
return arr[left]
}
pivotIndex := partition(arr, left, right)
if k == pivotIndex {
return arr[k]
} else if k < pivotIndex {
return quickSelect(arr, left, pivotIndex-1, k)
}
return quickSelect(arr, pivotIndex+1, right, k)
}
func partition(arr []byte, left, right int) int {
pivot := arr[right]
i := left
for j := left; j < right; j++ {
if arr[j] <= pivot {
arr[i], arr[j] = arr[j], arr[i]
i++
}
}
arr[i], arr[right] = arr[right], arr[i]
return i
}
// sharpenImage 锐化处理
func sharpenImage(data []byte, width, height int) []byte {
sharpened := make([]byte, len(data))
kernel := []int{0, -1, 0, -1, 5, -1, 0, -1, 0}
for y := 1; y < height-1; y++ {
for x := 1; x < width-1; x++ {
sum := 0
idx := 0
for ky := -1; ky <= 1; ky++ {
for kx := -1; kx <= 1; kx++ {
pixelIdx := (y+ky)*width + (x + kx)
sum += int(data[pixelIdx]) * kernel[idx]
idx++
}
}
if sum < 0 {
sum = 0
}
if sum > 255 {
sum = 255
}
sharpened[y*width+x] = uint8(sum)
}
}
// 边界保持原值
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
if y == 0 || y == height-1 || x == 0 || x == width-1 {
sharpened[y*width+x] = data[y*width+x]
}
}
}
return sharpened
}
// enhanceImageOtsu Otsu自适应阈值二值化
func enhanceImageOtsu(data []byte, width, height int) []byte {
threshold := calculateOtsuThreshold(data)
binary := make([]byte, len(data))
for i := range data {
if data[i] > threshold {
binary[i] = 255
}
}
return binary
}
// calculateOtsuThreshold 计算Otsu自适应阈值
func calculateOtsuThreshold(data []byte) uint8 {
histogram := make([]int, 256)
for _, v := range data {
histogram[v]++
}
total := len(data)
if total == 0 {
return 128
}
var threshold uint8
var maxVar float64
var sum int
for i := 0; i < 256; i++ {
sum += i * histogram[i]
}
var sum1 int
var wB int
for i := 0; i < 256; i++ {
wB += histogram[i]
if wB == 0 {
continue
}
wF := total - wB
if wF == 0 {
break
}
sum1 += i * histogram[i]
mB := float64(sum1) / float64(wB)
mF := float64(sum-sum1) / float64(wF)
varBetween := float64(wB) * float64(wF) * (mB - mF) * (mB - mF)
if varBetween > maxVar {
maxVar = varBetween
threshold = uint8(i)
}
}
return threshold
}
// enhanceImageGaussianSharpen 高斯模糊+锐化
func enhanceImageGaussianSharpen(data []byte, width, height int) []byte {
blurred := gaussianBlur(data, width, height, 1.0)
sharpened := sharpenImage(blurred, width, height)
enhanced := enhanceImage(sharpened, width, height)
return enhanced
}
// gaussianBlur 高斯模糊
func gaussianBlur(data []byte, width, height int, sigma float64) []byte {
blurred := make([]byte, len(data))
kernelSize := 5
halfKernel := kernelSize / 2
kernel := make([]float64, kernelSize*kernelSize)
sum := 0.0
for y := -halfKernel; y <= halfKernel; y++ {
for x := -halfKernel; x <= halfKernel; x++ {
idx := (y+halfKernel)*kernelSize + (x + halfKernel)
val := math.Exp(-(float64(x*x+y*y) / (2 * sigma * sigma)))
kernel[idx] = val
sum += val
}
}
for i := range kernel {
kernel[i] /= sum
}
for y := halfKernel; y < height-halfKernel; y++ {
for x := halfKernel; x < width-halfKernel; x++ {
var val float64
idx := 0
for ky := -halfKernel; ky <= halfKernel; ky++ {
for kx := -halfKernel; kx <= halfKernel; kx++ {
pixelIdx := (y+ky)*width + (x + kx)
val += float64(data[pixelIdx]) * kernel[idx]
idx++
}
}
blurred[y*width+x] = uint8(val)
}
}
// 边界保持原值
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
if y < halfKernel || y >= height-halfKernel || x < halfKernel || x >= width-halfKernel {
blurred[y*width+x] = data[y*width+x]
}
}
}
return blurred
}
// createImageFromGrayscale 从灰度数据创建图像
func createImageFromGrayscale(data []byte, width, height int) image.Image {
img := image.NewGray(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
rowStart := y * width
rowEnd := rowStart + width
if rowEnd > len(data) {
rowEnd = len(data)
}
copy(img.Pix[y*img.Stride:], data[rowStart:rowEnd])
}
return img
}
// scaledImage 缩放后的图像数据
type scaledImage struct {
data []byte
width int
height int
}
// scaleImage 缩放图像
func scaleImage(img image.Image, origWidth, origHeight int, scale float64) image.Image {
if scale == 1.0 {
return img
}
newWidth := int(float64(origWidth) * scale)
newHeight := int(float64(origHeight) * scale)
if newWidth < 50 || newHeight < 50 {
return nil
}
if newWidth > 1500 || newHeight > 1500 {
return nil
}
scaled := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
bounds := img.Bounds()
for y := 0; y < newHeight; y++ {
for x := 0; x < newWidth; x++ {
srcX := float64(x) / scale
srcY := float64(y) / scale
x1 := int(srcX)
y1 := int(srcY)
x2 := x1 + 1
y2 := y1 + 1
if x2 >= bounds.Dx() {
x2 = bounds.Dx() - 1
}
if y2 >= bounds.Dy() {
y2 = bounds.Dy() - 1
}
fx := srcX - float64(x1)
fy := srcY - float64(y1)
c11 := getPixelColor(img, bounds.Min.X+x1, bounds.Min.Y+y1)
c12 := getPixelColor(img, bounds.Min.X+x1, bounds.Min.Y+y2)
c21 := getPixelColor(img, bounds.Min.X+x2, bounds.Min.Y+y1)
c22 := getPixelColor(img, bounds.Min.X+x2, bounds.Min.Y+y2)
r := uint8(float64(c11.R)*(1-fx)*(1-fy) + float64(c21.R)*fx*(1-fy) + float64(c12.R)*(1-fx)*fy + float64(c22.R)*fx*fy)
g := uint8(float64(c11.G)*(1-fx)*(1-fy) + float64(c21.G)*fx*(1-fy) + float64(c12.G)*(1-fx)*fy + float64(c22.G)*fx*fy)
b := uint8(float64(c11.B)*(1-fx)*(1-fy) + float64(c21.B)*fx*(1-fy) + float64(c12.B)*(1-fx)*fy + float64(c22.B)*fx*fy)
scaled.Set(x, y, color.RGBA{R: r, G: g, B: b, A: 255})
}
}
return scaled
}
// getPixelColor 获取像素颜色
func getPixelColor(img image.Image, x, y int) color.RGBA {
r, g, b, _ := img.At(x, y).RGBA()
return color.RGBA{
R: uint8(r >> 8),
G: uint8(g >> 8),
B: uint8(b >> 8),
A: 255,
}
}
// scaleGrayscaleImage 缩放灰度图像
func scaleGrayscaleImage(data []byte, origWidth, origHeight int, scale float64) *scaledImage {
if scale == 1.0 {
return &scaledImage{data: data, width: origWidth, height: origHeight}
}
newWidth := int(float64(origWidth) * scale)
newHeight := int(float64(origHeight) * scale)
if newWidth < 21 || newHeight < 21 || newWidth > 2000 || newHeight > 2000 {
return nil
}
scaled := make([]byte, newWidth*newHeight)
for y := 0; y < newHeight; y++ {
for x := 0; x < newWidth; x++ {
srcX := float64(x) / scale
srcY := float64(y) / scale
x1 := int(srcX)
y1 := int(srcY)
x2 := x1 + 1
y2 := y1 + 1
if x2 >= origWidth {
x2 = origWidth - 1
}
if y2 >= origHeight {
y2 = origHeight - 1
}
fx := srcX - float64(x1)
fy := srcY - float64(y1)
v11 := float64(data[y1*origWidth+x1])
v12 := float64(data[y2*origWidth+x1])
v21 := float64(data[y1*origWidth+x2])
v22 := float64(data[y2*origWidth+x2])
val := v11*(1-fx)*(1-fy) + v21*fx*(1-fy) + v12*(1-fx)*fy + v22*fx*fy
scaled[y*newWidth+x] = uint8(val)
}
}
return &scaledImage{data: scaled, width: newWidth, height: newHeight}
}
// Point 表示一个点
type Point struct {
X, Y int
}
// Corner 表示检测到的定位图案角点
type Corner struct {
Center Point
Size int
Type int
}
// detectCornersFast 快速检测定位图案
func detectCornersFast(data []byte, width, height int) []Corner {
var corners []Corner
scanStep := max(2, min(width, height)/80)
if scanStep < 1 {
scanStep = 1
}
for y := scanStep * 3; y < height-scanStep*3; y += scanStep {
for x := scanStep * 3; x < width-scanStep*3; x += scanStep {
if isFinderPatternFast(data, width, height, x, y) {
corners = append(corners, Corner{
Center: Point{X: x, Y: y},
Size: 20,
})
if len(corners) >= 3 {
return corners
}
}
}
}
return corners
}
// isFinderPatternFast 快速检测定位图案
func isFinderPatternFast(data []byte, width, height, x, y int) bool {
centerIdx := y*width + x
if centerIdx < 0 || centerIdx >= len(data) {
return false
}
if data[centerIdx] > 180 {
return false
}
radius := min(width, height) / 15
if radius < 3 {
radius = 3
}
if radius > 30 {
radius = 30
}
directions := []struct{ dx, dy int }{{radius, 0}, {-radius, 0}, {0, radius}, {0, -radius}}
blackCount := 0
whiteCount := 0
for _, dir := range directions {
nx := x + dir.dx
ny := y + dir.dy
if nx >= 0 && nx < width && ny >= 0 && ny < height {
idx := ny*width + nx
if idx >= 0 && idx < len(data) {
if data[idx] < 128 {
blackCount++
} else {
whiteCount++
}
}
}
}
return blackCount >= 2 && whiteCount >= 2
}
// 辅助函数
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@@ -0,0 +1,191 @@
// 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"
"fmt"
_ "image/jpeg"
_ "image/png"
"io"
"net/http"
"os"
"time"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
"git.imall.cloud/openim/protocol/constant"
"git.imall.cloud/openim/protocol/sdkws"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
)
// PictureElem 用于解析图片消息内容
type PictureElem struct {
SourcePicture struct {
URL string `json:"url"`
} `json:"sourcePicture"`
BigPicture struct {
URL string `json:"url"`
} `json:"bigPicture"`
SnapshotPicture struct {
URL string `json:"url"`
} `json:"snapshotPicture"`
}
// checkImageContainsQRCode 检测图片中是否包含二维码
// userType: 0-普通用户不能发送包含二维码的图片1-特殊用户(可以发送)
func (m *msgServer) checkImageContainsQRCode(ctx context.Context, msgData *sdkws.MsgData, userType int32) error {
// userType=1 的用户可以发送包含二维码的图片,不进行检测
if userType == 1 {
return nil
}
// 只检测图片类型的消息
if msgData.ContentType != constant.Picture {
return nil
}
// 解析图片消息内容
var pictureElem PictureElem
if err := json.Unmarshal(msgData.Content, &pictureElem); err != nil {
// 如果解析失败,记录警告但不拦截
log.ZWarn(ctx, "failed to parse picture message", err, "content", string(msgData.Content))
return nil
}
// 获取图片URL优先使用原图如果没有则使用大图
imageURL := pictureElem.SourcePicture.URL
if imageURL == "" {
imageURL = pictureElem.BigPicture.URL
}
if imageURL == "" {
imageURL = pictureElem.SnapshotPicture.URL
}
if imageURL == "" {
// 没有有效的图片URL无法检测
log.ZWarn(ctx, "no valid image URL found in picture message", nil, "pictureElem", pictureElem)
return nil
}
// 下载图片并检测二维码
hasQRCode, err := m.detectQRCodeInImage(ctx, imageURL, "")
if err != nil {
// 检测失败时,记录错误但不拦截(避免误拦截)
log.ZWarn(ctx, "QR code detection failed", err, "imageURL", imageURL)
return nil
}
if hasQRCode {
log.ZWarn(ctx, "检测到二维码,拒绝发送", nil, "imageURL", imageURL, "userType", userType)
return servererrs.ErrImageContainsQRCode.WrapMsg("userType=0的用户不能发送包含二维码的图片")
}
return nil
}
// detectQRCodeInImage 下载图片并检测是否包含二维码
func (m *msgServer) detectQRCodeInImage(ctx context.Context, imageURL string, logPrefix string) (bool, error) {
// 创建带超时的HTTP客户端
client := &http.Client{
Timeout: 5 * time.Second,
}
// 下载图片
req, err := http.NewRequestWithContext(ctx, http.MethodGet, imageURL, nil)
if err != nil {
log.ZError(ctx, "创建HTTP请求失败", err, "imageURL", imageURL)
return false, errs.WrapMsg(err, "failed to create request")
}
resp, err := client.Do(req)
if err != nil {
log.ZError(ctx, "下载图片失败", err, "imageURL", imageURL)
return false, errs.WrapMsg(err, "failed to download image")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.ZError(ctx, "下载图片状态码异常", nil, "statusCode", resp.StatusCode, "imageURL", imageURL)
return false, errs.WrapMsg(fmt.Errorf("unexpected status code: %d", resp.StatusCode), "failed to download image")
}
// 限制图片大小最大10MB
const maxImageSize = 10 * 1024 * 1024
limitedReader := io.LimitReader(resp.Body, maxImageSize+1)
// 创建临时文件
tmpFile, err := os.CreateTemp("", "qrcode_detect_*.tmp")
if err != nil {
log.ZError(ctx, "创建临时文件失败", err)
return false, errs.WrapMsg(err, "failed to create temp file")
}
tmpFilePath := tmpFile.Name()
// 确保检测完成后删除临时文件(无论成功还是失败)
defer func() {
// 确保文件已关闭后再删除
if tmpFile != nil {
_ = tmpFile.Close()
}
// 删除临时文件,忽略文件不存在的错误
if err := os.Remove(tmpFilePath); err != nil && !os.IsNotExist(err) {
log.ZWarn(ctx, "删除临时文件失败", err, "tmpFile", tmpFilePath)
}
}()
// 保存图片到临时文件
written, err := io.Copy(tmpFile, limitedReader)
if err != nil {
log.ZError(ctx, "保存图片到临时文件失败", err, "tmpFile", tmpFilePath)
return false, errs.WrapMsg(err, "failed to save image")
}
// 关闭文件以便后续读取
if err := tmpFile.Close(); err != nil {
log.ZError(ctx, "关闭临时文件失败", err, "tmpFile", tmpFilePath)
return false, errs.WrapMsg(err, "failed to close temp file")
}
// 检查文件大小
if written > maxImageSize {
log.ZWarn(ctx, "图片过大", nil, "size", written, "maxSize", maxImageSize)
return false, errs.WrapMsg(fmt.Errorf("image too large: %d bytes", written), "image size exceeds limit")
}
// 使用优化的并行解码器检测二维码
hasQRCode, err := m.detectQRCodeWithDecoder(ctx, tmpFilePath, "")
if err != nil {
log.ZError(ctx, "二维码检测失败", err, "tmpFile", tmpFilePath)
return false, err
}
return hasQRCode, nil
}
// detectQRCodeWithDecoder 使用优化的解码器检测二维码
func (m *msgServer) detectQRCodeWithDecoder(ctx context.Context, imagePath string, logPrefix string) (bool, error) {
// 使用Custom解码器已移除Quirc解码器依赖
customDecoder := &CustomQRDecoder{}
// 执行解码
hasQRCode, err := customDecoder.Decode(ctx, imagePath, logPrefix)
if err != nil {
log.ZError(ctx, "解码器检测失败", err, "decoder", customDecoder.Name())
return false, err
}
return hasQRCode, nil
}

134
internal/rpc/msg/revoke.go Normal file
View File

@@ -0,0 +1,134 @@
// 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"
"time"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
"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"
"github.com/openimsdk/tools/mcontext"
"github.com/openimsdk/tools/utils/datautil"
)
func (m *msgServer) RevokeMsg(ctx context.Context, req *msg.RevokeMsgReq) (*msg.RevokeMsgResp, error) {
if req.UserID == "" {
return nil, errs.ErrArgs.WrapMsg("user_id is empty")
}
if req.ConversationID == "" {
return nil, errs.ErrArgs.WrapMsg("conversation_id is empty")
}
if req.Seq < 0 {
return nil, errs.ErrArgs.WrapMsg("seq is invalid")
}
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
return nil, err
}
user, err := m.UserLocalCache.GetUserInfo(ctx, req.UserID)
if err != nil {
return nil, err
}
_, _, msgs, err := m.MsgDatabase.GetMsgBySeqs(ctx, req.UserID, req.ConversationID, []int64{req.Seq})
if err != nil {
return nil, err
}
if len(msgs) == 0 || msgs[0] == nil {
return nil, errs.ErrRecordNotFound.WrapMsg("msg not found")
}
if msgs[0].ContentType == constant.MsgRevokeNotification {
return nil, servererrs.ErrMsgAlreadyRevoke.WrapMsg("msg already revoke")
}
data, _ := json.Marshal(msgs[0])
log.ZDebug(ctx, "GetMsgBySeqs", "conversationID", req.ConversationID, "seq", req.Seq, "msg", string(data))
var role int32
if !authverify.IsAdmin(ctx) {
sessionType := msgs[0].SessionType
switch sessionType {
case constant.SingleChatType:
if err := authverify.CheckAccess(ctx, msgs[0].SendID); err != nil {
return nil, err
}
role = user.AppMangerLevel
case constant.ReadGroupChatType:
members, err := m.GroupLocalCache.GetGroupMemberInfoMap(ctx, msgs[0].GroupID, datautil.Distinct([]string{req.UserID, msgs[0].SendID}))
if err != nil {
return nil, err
}
if req.UserID != msgs[0].SendID {
switch members[req.UserID].RoleLevel {
case constant.GroupOwner:
case constant.GroupAdmin:
if sendMember, ok := members[msgs[0].SendID]; ok {
if sendMember.RoleLevel != constant.GroupOrdinaryUsers {
return nil, errs.ErrNoPermission.WrapMsg("no permission")
}
}
default:
return nil, errs.ErrNoPermission.WrapMsg("no permission")
}
}
if member := members[req.UserID]; member != nil {
role = member.RoleLevel
}
default:
return nil, errs.ErrInternalServer.WrapMsg("msg sessionType not supported", "sessionType", sessionType)
}
}
now := time.Now().UnixMilli()
err = m.MsgDatabase.RevokeMsg(ctx, req.ConversationID, req.Seq, &model.RevokeModel{
Role: role,
UserID: req.UserID,
Nickname: user.Nickname,
Time: now,
})
if err != nil {
return nil, err
}
revokerUserID := mcontext.GetOpUserID(ctx)
var flag bool
if len(m.config.Share.IMAdminUser.UserIDs) > 0 {
flag = datautil.Contain(revokerUserID, m.adminUserIDs...)
}
tips := sdkws.RevokeMsgTips{
RevokerUserID: revokerUserID,
ClientMsgID: msgs[0].ClientMsgID,
RevokeTime: now,
Seq: req.Seq,
SesstionType: msgs[0].SessionType,
ConversationID: req.ConversationID,
IsAdminRevoke: flag,
}
var recvID string
if msgs[0].SessionType == constant.ReadGroupChatType {
recvID = msgs[0].GroupID
} else {
recvID = msgs[0].RecvID
}
m.notificationSender.NotificationWithSessionType(ctx, req.UserID, recvID, constant.MsgRevokeNotification, msgs[0].SessionType, &tips)
webhookCfg := m.webhookConfig()
m.webhookAfterRevokeMsg(ctx, &webhookCfg.AfterRevokeMsg, req)
return &msg.RevokeMsgResp{}, nil
}

231
internal/rpc/msg/send.go Normal file
View File

@@ -0,0 +1,231 @@
// 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"
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
"google.golang.org/protobuf/proto"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/prommetrics"
"git.imall.cloud/openim/open-im-server-deploy/pkg/msgprocessor"
"git.imall.cloud/openim/open-im-server-deploy/pkg/util/conversationutil"
"git.imall.cloud/openim/protocol/constant"
pbconv "git.imall.cloud/openim/protocol/conversation"
pbmsg "git.imall.cloud/openim/protocol/msg"
"git.imall.cloud/openim/protocol/sdkws"
"git.imall.cloud/openim/protocol/wrapperspb"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/mcontext"
"github.com/openimsdk/tools/utils/datautil"
)
func (m *msgServer) SendMsg(ctx context.Context, req *pbmsg.SendMsgReq) (*pbmsg.SendMsgResp, error) {
if req.MsgData == nil {
return nil, errs.ErrArgs.WrapMsg("msgData is nil")
}
if err := authverify.CheckAccess(ctx, req.MsgData.SendID); err != nil {
return nil, err
}
before := new(*sdkws.MsgData)
resp, err := m.sendMsg(ctx, req, before)
if err != nil {
return nil, err
}
if *before != nil && proto.Equal(*before, req.MsgData) == false {
resp.Modify = req.MsgData
}
return resp, nil
}
func (m *msgServer) sendMsg(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (*pbmsg.SendMsgResp, error) {
m.encapsulateMsgData(req.MsgData)
switch req.MsgData.SessionType {
case constant.SingleChatType:
return m.sendMsgSingleChat(ctx, req, before)
case constant.NotificationChatType:
return m.sendMsgNotification(ctx, req, before)
case constant.ReadGroupChatType:
return m.sendMsgGroupChat(ctx, req, before)
default:
return nil, errs.ErrArgs.WrapMsg("unknown sessionType")
}
}
func (m *msgServer) sendMsgGroupChat(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (resp *pbmsg.SendMsgResp, err error) {
if err = m.messageVerification(ctx, req); err != nil {
prommetrics.GroupChatMsgProcessFailedCounter.Inc()
return nil, err
}
webhookCfg := m.webhookConfig()
if err = m.webhookBeforeSendGroupMsg(ctx, &webhookCfg.BeforeSendGroupMsg, req); err != nil {
return nil, err
}
if err := m.webhookBeforeMsgModify(ctx, &webhookCfg.BeforeMsgModify, req, before); err != nil {
return nil, err
}
err = m.MsgDatabase.MsgToMQ(ctx, conversationutil.GenConversationUniqueKeyForGroup(req.MsgData.GroupID), req.MsgData)
if err != nil {
return nil, err
}
if req.MsgData.ContentType == constant.AtText {
go m.setConversationAtInfo(ctx, req.MsgData)
}
// 获取webhook配置优先从配置管理器获取
m.webhookAfterSendGroupMsg(ctx, &webhookCfg.AfterSendGroupMsg, req)
prommetrics.GroupChatMsgProcessSuccessCounter.Inc()
resp = &pbmsg.SendMsgResp{}
resp.SendTime = req.MsgData.SendTime
resp.ServerMsgID = req.MsgData.ServerMsgID
resp.ClientMsgID = req.MsgData.ClientMsgID
return resp, nil
}
func (m *msgServer) setConversationAtInfo(nctx context.Context, msg *sdkws.MsgData) {
log.ZDebug(nctx, "setConversationAtInfo", "msg", msg)
defer func() {
if r := recover(); r != nil {
log.ZPanic(nctx, "setConversationAtInfo Panic", errs.ErrPanic(r))
}
}()
ctx := mcontext.NewCtx("@@@" + mcontext.GetOperationID(nctx))
var atUserID []string
conversation := &pbconv.ConversationReq{
ConversationID: msgprocessor.GetConversationIDByMsg(msg),
ConversationType: msg.SessionType,
GroupID: msg.GroupID,
}
memberUserIDList, err := m.GroupLocalCache.GetGroupMemberIDs(ctx, msg.GroupID)
if err != nil {
log.ZWarn(ctx, "GetGroupMemberIDs", err)
return
}
tagAll := datautil.Contain(constant.AtAllString, msg.AtUserIDList...)
if tagAll {
memberUserIDList = datautil.DeleteElems(memberUserIDList, msg.SendID)
atUserID = datautil.Single([]string{constant.AtAllString}, msg.AtUserIDList)
if len(atUserID) == 0 { // just @everyone
conversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.AtAll}
} else { // @Everyone and @other people
conversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.AtAllAtMe}
atUserID = datautil.SliceIntersectFuncs(atUserID, memberUserIDList, func(a string) string { return a }, func(b string) string {
return b
})
if err := m.conversationClient.SetConversations(ctx, atUserID, conversation); err != nil {
log.ZWarn(ctx, "SetConversations", err, "userID", atUserID, "conversation", conversation)
}
memberUserIDList = datautil.Single(atUserID, memberUserIDList)
}
conversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.AtAll}
if err := m.conversationClient.SetConversations(ctx, memberUserIDList, conversation); err != nil {
log.ZWarn(ctx, "SetConversations", err, "userID", memberUserIDList, "conversation", conversation)
}
return
}
atUserID = datautil.SliceIntersectFuncs(msg.AtUserIDList, memberUserIDList, func(a string) string { return a }, func(b string) string {
return b
})
conversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.AtMe}
if err := m.conversationClient.SetConversations(ctx, atUserID, conversation); err != nil {
log.ZWarn(ctx, "SetConversations", err, atUserID, conversation)
}
}
func (m *msgServer) sendMsgNotification(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (resp *pbmsg.SendMsgResp, err error) {
if err := m.MsgDatabase.MsgToMQ(ctx, conversationutil.GenConversationUniqueKeyForSingle(req.MsgData.SendID, req.MsgData.RecvID), req.MsgData); err != nil {
return nil, err
}
resp = &pbmsg.SendMsgResp{
ServerMsgID: req.MsgData.ServerMsgID,
ClientMsgID: req.MsgData.ClientMsgID,
SendTime: req.MsgData.SendTime,
}
return resp, nil
}
func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (resp *pbmsg.SendMsgResp, err error) {
if err := m.messageVerification(ctx, req); err != nil {
return nil, err
}
webhookCfg := m.webhookConfig()
isSend := true
isNotification := msgprocessor.IsNotificationByMsg(req.MsgData)
if !isNotification {
isSend, err = m.modifyMessageByUserMessageReceiveOpt(authverify.WithTempAdmin(ctx), req.MsgData.RecvID, conversationutil.GenConversationIDForSingle(req.MsgData.SendID, req.MsgData.RecvID), constant.SingleChatType, req)
if err != nil {
return nil, err
}
}
if !isSend {
prommetrics.SingleChatMsgProcessFailedCounter.Inc()
return nil, errs.ErrArgs.WrapMsg("message is not sent")
} else {
if err := m.webhookBeforeMsgModify(ctx, &webhookCfg.BeforeMsgModify, req, before); err != nil {
return nil, err
}
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, &webhookCfg.AfterSendSingleMsg, req)
prommetrics.SingleChatMsgProcessSuccessCounter.Inc()
return &pbmsg.SendMsgResp{
ServerMsgID: req.MsgData.ServerMsgID,
ClientMsgID: req.MsgData.ClientMsgID,
SendTime: req.MsgData.SendTime,
}, nil
}
}
func (m *msgServer) SendSimpleMsg(ctx context.Context, req *pbmsg.SendSimpleMsgReq) (*pbmsg.SendSimpleMsgResp, error) {
if req.MsgData == nil {
return nil, errs.ErrArgs.WrapMsg("msg data is nil")
}
sender, err := m.UserLocalCache.GetUserInfo(ctx, req.MsgData.SendID)
if err != nil {
return nil, err
}
req.MsgData.SenderFaceURL = sender.FaceURL
req.MsgData.SenderNickname = sender.Nickname
resp, err := m.SendMsg(ctx, &pbmsg.SendMsgReq{MsgData: req.MsgData})
if err != nil {
return nil, err
}
return &pbmsg.SendSimpleMsgResp{
ServerMsgID: resp.ServerMsgID,
ClientMsgID: resp.ClientMsgID,
SendTime: resp.SendTime,
Modify: resp.Modify,
}, nil
}

105
internal/rpc/msg/seq.go Normal file
View File

@@ -0,0 +1,105 @@
// 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"
"errors"
"sort"
pbmsg "git.imall.cloud/openim/protocol/msg"
"github.com/redis/go-redis/v9"
)
func (m *msgServer) GetConversationMaxSeq(ctx context.Context, req *pbmsg.GetConversationMaxSeqReq) (*pbmsg.GetConversationMaxSeqResp, error) {
maxSeq, err := m.MsgDatabase.GetMaxSeq(ctx, req.ConversationID)
if err != nil && !errors.Is(err, redis.Nil) {
return nil, err
}
return &pbmsg.GetConversationMaxSeqResp{MaxSeq: maxSeq}, nil
}
func (m *msgServer) GetMaxSeqs(ctx context.Context, req *pbmsg.GetMaxSeqsReq) (*pbmsg.SeqsInfoResp, error) {
maxSeqs, err := m.MsgDatabase.GetMaxSeqs(ctx, req.ConversationIDs)
if err != nil {
return nil, err
}
return &pbmsg.SeqsInfoResp{MaxSeqs: maxSeqs}, nil
}
func (m *msgServer) GetHasReadSeqs(ctx context.Context, req *pbmsg.GetHasReadSeqsReq) (*pbmsg.SeqsInfoResp, error) {
hasReadSeqs, err := m.MsgDatabase.GetHasReadSeqs(ctx, req.UserID, req.ConversationIDs)
if err != nil {
return nil, err
}
return &pbmsg.SeqsInfoResp{MaxSeqs: hasReadSeqs}, nil
}
func (m *msgServer) GetMsgByConversationIDs(ctx context.Context, req *pbmsg.GetMsgByConversationIDsReq) (*pbmsg.GetMsgByConversationIDsResp, error) {
Msgs, err := m.MsgDatabase.FindOneByDocIDs(ctx, req.ConversationIDs, req.MaxSeqs)
if err != nil {
return nil, err
}
return &pbmsg.GetMsgByConversationIDsResp{MsgDatas: Msgs}, nil
}
func (m *msgServer) SetUserConversationsMinSeq(ctx context.Context, req *pbmsg.SetUserConversationsMinSeqReq) (*pbmsg.SetUserConversationsMinSeqResp, error) {
for _, userID := range req.UserIDs {
if err := m.MsgDatabase.SetUserConversationsMinSeqs(ctx, userID, map[string]int64{req.ConversationID: req.Seq}); err != nil {
return nil, err
}
}
return &pbmsg.SetUserConversationsMinSeqResp{}, nil
}
func (m *msgServer) GetActiveConversation(ctx context.Context, req *pbmsg.GetActiveConversationReq) (*pbmsg.GetActiveConversationResp, error) {
res, err := m.MsgDatabase.GetCacheMaxSeqWithTime(ctx, req.ConversationIDs)
if err != nil {
return nil, err
}
conversations := make([]*pbmsg.ActiveConversation, 0, len(res))
for conversationID, val := range res {
conversations = append(conversations, &pbmsg.ActiveConversation{
MaxSeq: val.Seq,
LastTime: val.Time,
ConversationID: conversationID,
})
}
if req.Limit > 0 {
sort.Sort(activeConversations(conversations))
if len(conversations) > int(req.Limit) {
conversations = conversations[:req.Limit]
}
}
return &pbmsg.GetActiveConversationResp{Conversations: conversations}, nil
}
func (m *msgServer) SetUserConversationMaxSeq(ctx context.Context, req *pbmsg.SetUserConversationMaxSeqReq) (*pbmsg.SetUserConversationMaxSeqResp, error) {
for _, userID := range req.OwnerUserID {
if err := m.MsgDatabase.SetUserConversationsMaxSeq(ctx, req.ConversationID, userID, req.MaxSeq); err != nil {
return nil, err
}
}
return &pbmsg.SetUserConversationMaxSeqResp{}, nil
}
func (m *msgServer) SetUserConversationMinSeq(ctx context.Context, req *pbmsg.SetUserConversationMinSeqReq) (*pbmsg.SetUserConversationMinSeqResp, error) {
for _, userID := range req.OwnerUserID {
if err := m.MsgDatabase.SetUserConversationsMinSeq(ctx, req.ConversationID, userID, req.MinSeq); err != nil {
return nil, err
}
}
return &pbmsg.SetUserConversationMinSeqResp{}, nil
}

218
internal/rpc/msg/server.go Normal file
View File

@@ -0,0 +1,218 @@
// 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"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/mcache"
"git.imall.cloud/openim/open-im-server-deploy/pkg/dbbuild"
"git.imall.cloud/openim/open-im-server-deploy/pkg/localcache"
"git.imall.cloud/openim/open-im-server-deploy/pkg/mqbuild"
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
"google.golang.org/grpc"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/redis"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/controller"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database/mgo"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification"
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpccache"
"git.imall.cloud/openim/protocol/constant"
"git.imall.cloud/openim/protocol/conversation"
"git.imall.cloud/openim/protocol/msg"
"git.imall.cloud/openim/protocol/sdkws"
"github.com/openimsdk/tools/discovery"
"github.com/openimsdk/tools/log"
)
type MessageInterceptorFunc func(ctx context.Context, globalConfig *Config, req *msg.SendMsgReq) (*sdkws.MsgData, error)
// MessageInterceptorChain defines a chain of message interceptor functions.
type MessageInterceptorChain []MessageInterceptorFunc
type Config struct {
RpcConfig config.Msg
RedisConfig config.Redis
MongodbConfig config.Mongo
KafkaConfig config.Kafka
NotificationConfig config.Notification
Share config.Share
WebhooksConfig config.Webhooks
LocalCacheConfig config.LocalCache
Discovery config.Discovery
}
// MsgServer encapsulates dependencies required for message handling.
type msgServer struct {
msg.UnimplementedMsgServer
RegisterCenter discovery.Conn // Service discovery registry for service registration.
MsgDatabase controller.CommonMsgDatabase // Interface for message database operations.
UserLocalCache *rpccache.UserLocalCache // Local cache for user data.
FriendLocalCache *rpccache.FriendLocalCache // Local cache for friend data.
GroupLocalCache *rpccache.GroupLocalCache // Local cache for group data.
ConversationLocalCache *rpccache.ConversationLocalCache // Local cache for conversation data.
Handlers MessageInterceptorChain // Chain of handlers for processing messages.
notificationSender *notification.NotificationSender // RPC client for sending notifications.
msgNotificationSender *MsgNotificationSender // RPC client for sending msg notifications.
config *Config // Global configuration settings.
webhookClient *webhook.Client
webhookConfigManager *webhook.ConfigManager // Webhook配置管理器支持从数据库读取
conversationClient *rpcli.ConversationClient
redPacketDB database.RedPacket // Database for red packet records.
redPacketReceiveDB database.RedPacketReceive // Database for red packet receive records.
adminUserIDs []string
}
func (m *msgServer) addInterceptorHandler(interceptorFunc ...MessageInterceptorFunc) {
m.Handlers = append(m.Handlers, interceptorFunc...)
}
// webhookConfig returns the latest webhook config from the client/manager with fallback.
func (m *msgServer) webhookConfig() *config.Webhooks {
if m.webhookClient == nil {
return &m.config.WebhooksConfig
}
return m.webhookClient.GetConfig(&m.config.WebhooksConfig)
}
func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error {
builder := mqbuild.NewBuilder(&config.KafkaConfig)
redisProducer, err := builder.GetTopicProducer(ctx, config.KafkaConfig.ToRedisTopic)
if err != nil {
return err
}
dbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig)
mgocli, err := dbb.Mongo(ctx)
if err != nil {
return err
}
rdb, err := dbb.Redis(ctx)
if err != nil {
return err
}
msgDocModel, err := mgo.NewMsgMongo(mgocli.GetDB())
if err != nil {
return err
}
var msgModel cache.MsgCache
if rdb == nil {
cm, err := mgo.NewCacheMgo(mgocli.GetDB())
if err != nil {
return err
}
msgModel = mcache.NewMsgCache(cm, msgDocModel)
} else {
msgModel = redis.NewMsgCache(rdb, msgDocModel)
}
seqConversation, err := mgo.NewSeqConversationMongo(mgocli.GetDB())
if err != nil {
return err
}
seqConversationCache := redis.NewSeqConversationCacheRedis(rdb, seqConversation)
seqUser, err := mgo.NewSeqUserMongo(mgocli.GetDB())
if err != nil {
return err
}
seqUserCache := redis.NewSeqUserCacheRedis(rdb, seqUser)
redPacketDB, err := mgo.NewRedPacketMongo(mgocli.GetDB())
if err != nil {
return err
}
redPacketReceiveDB, err := mgo.NewRedPacketReceiveMongo(mgocli.GetDB())
if err != nil {
return err
}
userConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)
if err != nil {
return err
}
groupConn, err := client.GetConn(ctx, config.Discovery.RpcService.Group)
if err != nil {
return err
}
friendConn, err := client.GetConn(ctx, config.Discovery.RpcService.Friend)
if err != nil {
return err
}
conversationConn, err := client.GetConn(ctx, config.Discovery.RpcService.Conversation)
if err != nil {
return err
}
conversationClient := rpcli.NewConversationClient(conversationConn)
msgDatabase := controller.NewCommonMsgDatabase(msgDocModel, msgModel, seqUserCache, seqConversationCache, redisProducer)
localcache.InitLocalCache(&config.LocalCacheConfig)
// 初始化webhook配置管理器支持从数据库读取配置
var webhookClient *webhook.Client
var webhookConfigManager *webhook.ConfigManager
systemConfigDB, err := mgo.NewSystemConfigMongo(mgocli.GetDB())
if err == nil {
// 如果SystemConfig数据库初始化成功使用配置管理器
webhookConfigManager = webhook.NewConfigManager(systemConfigDB, &config.WebhooksConfig)
if err := webhookConfigManager.Start(ctx); err != nil {
log.ZWarn(ctx, "failed to start webhook config manager, using default config", err)
webhookClient = webhook.NewWebhookClient(config.WebhooksConfig.URL)
} else {
webhookClient = webhook.NewWebhookClientWithManager(webhookConfigManager)
}
} else {
// 如果SystemConfig数据库初始化失败使用默认配置
log.ZWarn(ctx, "failed to init system config db, using default webhook config", err)
webhookClient = webhook.NewWebhookClient(config.WebhooksConfig.URL)
}
s := &msgServer{
MsgDatabase: msgDatabase,
RegisterCenter: client,
UserLocalCache: rpccache.NewUserLocalCache(rpcli.NewUserClient(userConn), &config.LocalCacheConfig, rdb),
GroupLocalCache: rpccache.NewGroupLocalCache(rpcli.NewGroupClient(groupConn), &config.LocalCacheConfig, rdb),
ConversationLocalCache: rpccache.NewConversationLocalCache(conversationClient, &config.LocalCacheConfig, rdb),
FriendLocalCache: rpccache.NewFriendLocalCache(rpcli.NewRelationClient(friendConn), &config.LocalCacheConfig, rdb),
config: config,
webhookClient: webhookClient,
webhookConfigManager: webhookConfigManager,
conversationClient: conversationClient,
redPacketDB: redPacketDB,
redPacketReceiveDB: redPacketReceiveDB,
adminUserIDs: config.Share.IMAdminUser.UserIDs,
}
s.notificationSender = notification.NewNotificationSender(&config.NotificationConfig, notification.WithLocalSendMsg(s.SendMsg))
s.msgNotificationSender = NewMsgNotificationSender(config, notification.WithLocalSendMsg(s.SendMsg))
msg.RegisterMsgServer(server, s)
return nil
}
func (m *msgServer) conversationAndGetRecvID(conversation *conversation.Conversation, userID string) string {
if conversation.ConversationType == constant.SingleChatType ||
conversation.ConversationType == constant.NotificationChatType {
if userID == conversation.OwnerUserID {
return conversation.UserID
} else {
return conversation.OwnerUserID
}
} else if conversation.ConversationType == constant.ReadGroupChatType {
return conversation.GroupID
}
return ""
}

View File

@@ -0,0 +1,107 @@
// 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"
"time"
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
"git.imall.cloud/openim/protocol/msg"
"git.imall.cloud/openim/protocol/sdkws"
"github.com/openimsdk/tools/utils/datautil"
)
func (m *msgServer) GetActiveUser(ctx context.Context, req *msg.GetActiveUserReq) (*msg.GetActiveUserResp, error) {
if err := authverify.CheckAdmin(ctx); err != nil {
return nil, err
}
msgCount, userCount, users, dateCount, err := m.MsgDatabase.RangeUserSendCount(ctx, time.UnixMilli(req.Start), time.UnixMilli(req.End), req.Group, req.Ase, req.Pagination.PageNumber, req.Pagination.ShowNumber)
if err != nil {
return nil, err
}
var pbUsers []*msg.ActiveUser
if len(users) > 0 {
userIDs := datautil.Slice(users, func(e *model.UserCount) string { return e.UserID })
userMap, err := m.UserLocalCache.GetUsersInfoMap(ctx, userIDs)
if err != nil {
return nil, err
}
pbUsers = make([]*msg.ActiveUser, 0, len(users))
for _, user := range users {
pbUser := userMap[user.UserID]
if pbUser == nil {
pbUser = &sdkws.UserInfo{
UserID: user.UserID,
Nickname: user.UserID,
}
}
pbUsers = append(pbUsers, &msg.ActiveUser{
User: pbUser,
Count: user.Count,
})
}
}
return &msg.GetActiveUserResp{
MsgCount: msgCount,
UserCount: userCount,
DateCount: dateCount,
Users: pbUsers,
}, nil
}
func (m *msgServer) GetActiveGroup(ctx context.Context, req *msg.GetActiveGroupReq) (*msg.GetActiveGroupResp, error) {
if err := authverify.CheckAdmin(ctx); err != nil {
return nil, err
}
msgCount, groupCount, groups, dateCount, err := m.MsgDatabase.RangeGroupSendCount(ctx, time.UnixMilli(req.Start), time.UnixMilli(req.End), req.Ase, req.Pagination.PageNumber, req.Pagination.ShowNumber)
if err != nil {
return nil, err
}
var pbgroups []*msg.ActiveGroup
if len(groups) > 0 {
groupIDs := datautil.Slice(groups, func(e *model.GroupCount) string { return e.GroupID })
resp, err := m.GroupLocalCache.GetGroupInfos(ctx, groupIDs)
if err != nil {
return nil, err
}
groupMap := make(map[string]*sdkws.GroupInfo, len(groups))
for i, group := range groups {
groupMap[group.GroupID] = resp[i]
}
pbgroups = make([]*msg.ActiveGroup, 0, len(groups))
for _, group := range groups {
pbgroup := groupMap[group.GroupID]
if pbgroup == nil {
pbgroup = &sdkws.GroupInfo{
GroupID: group.GroupID,
GroupName: group.GroupID,
}
}
pbgroups = append(pbgroups, &msg.ActiveGroup{
Group: pbgroup,
Count: group.Count,
})
}
}
return &msg.GetActiveGroupResp{
MsgCount: msgCount,
GroupCount: groupCount,
DateCount: dateCount,
Groups: pbgroups,
}, nil
}

View File

@@ -0,0 +1,658 @@
// 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"
"time"
"git.imall.cloud/openim/open-im-server-deploy/pkg/apistruct"
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
"git.imall.cloud/openim/open-im-server-deploy/pkg/msgprocessor"
"git.imall.cloud/openim/open-im-server-deploy/pkg/util/conversationutil"
"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"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/tools/utils/jsonutil"
"github.com/openimsdk/tools/utils/timeutil"
)
func (m *msgServer) PullMessageBySeqs(ctx context.Context, req *sdkws.PullMessageBySeqsReq) (*sdkws.PullMessageBySeqsResp, error) {
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
return nil, err
}
// 设置请求超时防止大量数据拉取导致pod异常
// 对于大量大群用户需要更长的超时时间60秒
queryTimeout := 60 * time.Second
queryCtx, cancel := context.WithTimeout(ctx, queryTimeout)
defer cancel()
// 参数验证:限制 SeqRanges 数量和每个范围的大小,防止内存溢出
const maxSeqRanges = 100
const maxSeqRangeSize = 10000
// 限制响应总大小防止pod内存溢出估算每条消息平均1KB最多50MB
const maxTotalMessages = 50000
if len(req.SeqRanges) > maxSeqRanges {
log.ZWarn(ctx, "SeqRanges count exceeds limit", nil, "count", len(req.SeqRanges), "limit", maxSeqRanges)
return nil, errs.ErrArgs.WrapMsg("too many seq ranges", "count", len(req.SeqRanges), "limit", maxSeqRanges)
}
for _, seq := range req.SeqRanges {
// 验证每个 seq range 的合理性
if seq.Begin < 0 || seq.End < 0 {
log.ZWarn(ctx, "invalid seq range: negative values", nil, "begin", seq.Begin, "end", seq.End)
continue
}
if seq.End < seq.Begin {
log.ZWarn(ctx, "invalid seq range: end < begin", nil, "begin", seq.Begin, "end", seq.End)
continue
}
seqRangeSize := seq.End - seq.Begin + 1
if seqRangeSize > maxSeqRangeSize {
log.ZWarn(ctx, "seq range size exceeds limit, will be limited", nil, "conversationID", seq.ConversationID, "begin", seq.Begin, "end", seq.End, "size", seqRangeSize, "limit", maxSeqRangeSize)
}
}
resp := &sdkws.PullMessageBySeqsResp{}
resp.Msgs = make(map[string]*sdkws.PullMsgs)
resp.NotificationMsgs = make(map[string]*sdkws.PullMsgs)
var totalMessages int
for _, seq := range req.SeqRanges {
// 检查总消息数,防止内存溢出
if totalMessages >= maxTotalMessages {
log.ZWarn(ctx, "total messages count exceeds limit, stopping", nil, "totalMessages", totalMessages, "limit", maxTotalMessages)
break
}
if !msgprocessor.IsNotification(seq.ConversationID) {
conversation, err := m.ConversationLocalCache.GetConversation(queryCtx, req.UserID, seq.ConversationID)
if err != nil {
log.ZError(ctx, "GetConversation error", err, "conversationID", seq.ConversationID)
continue
}
minSeq, maxSeq, msgs, err := m.MsgDatabase.GetMsgBySeqsRange(queryCtx, req.UserID, seq.ConversationID,
seq.Begin, seq.End, seq.Num, conversation.MaxSeq)
if err != nil {
// 如果是超时错误,记录更详细的日志
if queryCtx.Err() == context.DeadlineExceeded {
log.ZWarn(ctx, "GetMsgBySeqsRange timeout", err, "conversationID", seq.ConversationID, "seq", seq, "timeout", queryTimeout)
return nil, errs.ErrInternalServer.WrapMsg("message pull timeout, data too large or query too slow")
}
log.ZWarn(ctx, "GetMsgBySeqsRange error", err, "conversationID", seq.ConversationID, "seq", seq)
continue
}
totalMessages += len(msgs)
var isEnd bool
switch req.Order {
case sdkws.PullOrder_PullOrderAsc:
isEnd = maxSeq <= seq.End
case sdkws.PullOrder_PullOrderDesc:
isEnd = seq.Begin <= minSeq
}
if len(msgs) == 0 {
log.ZWarn(ctx, "not have msgs", nil, "conversationID", seq.ConversationID, "seq", seq)
continue
}
// 过滤禁言通知消息(只保留群主、管理员和被禁言成员本人可以看到的)
msgs = m.filterMuteNotificationMsgs(ctx, req.UserID, seq.ConversationID, msgs)
// 填充红包消息的领取信息
msgs = m.enrichRedPacketMessages(ctx, req.UserID, msgs)
resp.Msgs[seq.ConversationID] = &sdkws.PullMsgs{Msgs: msgs, IsEnd: isEnd}
} else {
// 限制通知消息的查询范围,防止内存溢出
const maxNotificationSeqRange = 5000
var seqs []int64
seqRange := seq.End - seq.Begin + 1
if seqRange > maxNotificationSeqRange {
log.ZWarn(ctx, "notification seq range too large, limiting", nil, "conversationID", seq.ConversationID, "begin", seq.Begin, "end", seq.End, "range", seqRange, "limit", maxNotificationSeqRange)
// 只取最后 maxNotificationSeqRange 条
for i := seq.End - maxNotificationSeqRange + 1; i <= seq.End; i++ {
seqs = append(seqs, i)
}
} else {
for i := seq.Begin; i <= seq.End; i++ {
seqs = append(seqs, i)
}
}
minSeq, maxSeq, notificationMsgs, err := m.MsgDatabase.GetMsgBySeqs(queryCtx, req.UserID, seq.ConversationID, seqs)
if err != nil {
// 如果是超时错误,记录更详细的日志
if queryCtx.Err() == context.DeadlineExceeded {
log.ZWarn(ctx, "GetMsgBySeqs timeout", err, "conversationID", seq.ConversationID, "seq", seq, "timeout", queryTimeout)
return nil, errs.ErrInternalServer.WrapMsg("notification message pull timeout, data too large or query too slow")
}
log.ZWarn(ctx, "GetMsgBySeqs error", err, "conversationID", seq.ConversationID, "seq", seq)
continue
}
totalMessages += len(notificationMsgs)
var isEnd bool
switch req.Order {
case sdkws.PullOrder_PullOrderAsc:
isEnd = maxSeq <= seq.End
case sdkws.PullOrder_PullOrderDesc:
isEnd = seq.Begin <= minSeq
}
if len(notificationMsgs) == 0 {
log.ZWarn(ctx, "not have notificationMsgs", nil, "conversationID", seq.ConversationID, "seq", seq)
continue
}
// 过滤禁言通知消息(只保留群主、管理员和被禁言成员本人可以看到的)
notificationMsgs = m.filterMuteNotificationMsgs(ctx, req.UserID, seq.ConversationID, notificationMsgs)
// 填充红包消息的领取信息
notificationMsgs = m.enrichRedPacketMessages(ctx, req.UserID, notificationMsgs)
resp.NotificationMsgs[seq.ConversationID] = &sdkws.PullMsgs{Msgs: notificationMsgs, IsEnd: isEnd}
}
}
return resp, nil
}
func (m *msgServer) GetSeqMessage(ctx context.Context, req *msg.GetSeqMessageReq) (*msg.GetSeqMessageResp, error) {
resp := &msg.GetSeqMessageResp{
Msgs: make(map[string]*sdkws.PullMsgs),
NotificationMsgs: make(map[string]*sdkws.PullMsgs),
}
for _, conv := range req.Conversations {
isEnd, endSeq, msgs, err := m.MsgDatabase.GetMessagesBySeqWithBounds(ctx, req.UserID, conv.ConversationID, conv.Seqs, req.GetOrder())
if err != nil {
return nil, err
}
var pullMsgs *sdkws.PullMsgs
if ok := false; conversationutil.IsNotificationConversationID(conv.ConversationID) {
pullMsgs, ok = resp.NotificationMsgs[conv.ConversationID]
if !ok {
pullMsgs = &sdkws.PullMsgs{}
resp.NotificationMsgs[conv.ConversationID] = pullMsgs
}
} else {
pullMsgs, ok = resp.Msgs[conv.ConversationID]
if !ok {
pullMsgs = &sdkws.PullMsgs{}
resp.Msgs[conv.ConversationID] = pullMsgs
}
}
// 过滤禁言通知消息(只保留群主、管理员和被禁言成员本人可以看到的)
filteredMsgs := m.filterMuteNotificationMsgs(ctx, req.UserID, conv.ConversationID, msgs)
// 填充红包消息的领取信息
filteredMsgs = m.enrichRedPacketMessages(ctx, req.UserID, filteredMsgs)
pullMsgs.Msgs = append(pullMsgs.Msgs, filteredMsgs...)
pullMsgs.IsEnd = isEnd
pullMsgs.EndSeq = endSeq
}
return resp, nil
}
// filterMuteNotificationMsgs 过滤禁言、取消禁言、踢出群聊、退出群聊、进入群聊、群成员信息设置和角色变更通知消息,只保留群主、管理员和相关成员本人可以看到的消息
func (m *msgServer) filterMuteNotificationMsgs(ctx context.Context, userID, conversationID string, msgs []*sdkws.MsgData) []*sdkws.MsgData {
// 如果不是群聊会话,直接返回
if !conversationutil.IsGroupConversationID(conversationID) {
return msgs
}
// 提取群ID
groupID := conversationutil.GetGroupIDFromConversationID(conversationID)
if groupID == "" {
log.ZWarn(ctx, "filterMuteNotificationMsgs: invalid group conversationID", nil, "conversationID", conversationID)
return msgs
}
var filteredMsgs []*sdkws.MsgData
var needCheckPermission bool
// 先检查是否有需要过滤的消息
for _, msg := range msgs {
if msg.ContentType == constant.GroupMemberMutedNotification ||
msg.ContentType == constant.GroupMemberCancelMutedNotification ||
msg.ContentType == constant.MemberKickedNotification ||
msg.ContentType == constant.MemberQuitNotification ||
msg.ContentType == constant.MemberInvitedNotification ||
msg.ContentType == constant.MemberEnterNotification ||
msg.ContentType == constant.GroupMemberInfoSetNotification ||
msg.ContentType == constant.GroupMemberSetToAdminNotification ||
msg.ContentType == constant.GroupMemberSetToOrdinaryUserNotification {
needCheckPermission = true
break
}
}
// 如果没有需要过滤的消息,直接返回
if !needCheckPermission {
return msgs
}
// 对于被踢出的用户,可能无法获取成员信息,需要特殊处理
// 先收集所有被踢出的用户ID以便后续判断
var allKickedUserIDs []string
if needCheckPermission {
for _, msg := range msgs {
if msg.ContentType == constant.MemberKickedNotification {
var tips sdkws.MemberKickedTips
if err := unmarshalNotificationContent(string(msg.Content), &tips); err == nil {
kickedUserIDs := datautil.Slice(tips.KickedUserList, func(e *sdkws.GroupMemberFullInfo) string { return e.UserID })
allKickedUserIDs = append(allKickedUserIDs, kickedUserIDs...)
}
}
}
allKickedUserIDs = datautil.Distinct(allKickedUserIDs)
}
// 检查用户是否是被踢出的用户(即使已经被踢出,也应该能看到自己被踢出的通知)
isKickedUserInMsgs := datautil.Contain(userID, allKickedUserIDs...)
// 获取当前用户在群中的角色(如果用户已经被踢出,这里会返回错误)
// 添加超时保护防止大群查询阻塞3秒超时
memberCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
member, err := m.GroupLocalCache.GetGroupMember(memberCtx, groupID, userID)
isOwnerOrAdmin := false
if err != nil {
if memberCtx.Err() == context.DeadlineExceeded {
log.ZWarn(ctx, "filterMuteNotificationMsgs: GetGroupMember timeout", err, "groupID", groupID, "userID", userID)
} else {
log.ZDebug(ctx, "filterMuteNotificationMsgs: GetGroupMember failed (user may be kicked)", err, "groupID", groupID, "userID", userID, "isKickedUserInMsgs", isKickedUserInMsgs)
}
// 如果获取失败(可能已经被踢出或超时),仍然需要检查是否是相关成员本人
// 继续处理,但 isOwnerOrAdmin 保持为 false
// 如果是被踢出的用户,仍然可以查看自己被踢出的通知
} else {
isOwnerOrAdmin = member.RoleLevel == constant.GroupOwner || member.RoleLevel == constant.GroupAdmin
}
// 过滤消息
for _, msg := range msgs {
if msg.ContentType == constant.GroupMemberMutedNotification {
var tips sdkws.GroupMemberMutedTips
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal GroupMemberMutedTips failed", err)
filteredMsgs = append(filteredMsgs, msg)
continue
}
mutedUserID := tips.MutedUser.UserID
if isOwnerOrAdmin || userID == mutedUserID {
filteredMsgs = append(filteredMsgs, msg)
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberMutedNotification allowed", "userID", userID, "mutedUserID", mutedUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
} else {
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberMutedNotification filtered", "userID", userID, "mutedUserID", mutedUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
}
} else if msg.ContentType == constant.GroupMemberCancelMutedNotification {
var tips sdkws.GroupMemberCancelMutedTips
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal GroupMemberCancelMutedTips failed", err)
filteredMsgs = append(filteredMsgs, msg)
continue
}
cancelMutedUserID := tips.MutedUser.UserID
if isOwnerOrAdmin || userID == cancelMutedUserID {
filteredMsgs = append(filteredMsgs, msg)
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberCancelMutedNotification allowed", "userID", userID, "cancelMutedUserID", cancelMutedUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
} else {
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberCancelMutedNotification filtered", "userID", userID, "cancelMutedUserID", cancelMutedUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
}
} else if msg.ContentType == constant.MemberQuitNotification {
var tips sdkws.MemberQuitTips
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal MemberQuitTips failed", err)
filteredMsgs = append(filteredMsgs, msg)
continue
}
quitUserID := tips.QuitUser.UserID
// 退出群聊通知只允许群主和管理员看到,退出者本人不通知
if isOwnerOrAdmin {
filteredMsgs = append(filteredMsgs, msg)
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberQuitNotification allowed", "userID", userID, "quitUserID", quitUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
} else {
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberQuitNotification filtered", "userID", userID, "quitUserID", quitUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
}
} else if msg.ContentType == constant.MemberInvitedNotification {
var tips sdkws.MemberInvitedTips
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal MemberInvitedTips failed", err)
filteredMsgs = append(filteredMsgs, msg)
continue
}
// 获取被邀请的用户ID列表
invitedUserIDs := datautil.Slice(tips.InvitedUserList, func(e *sdkws.GroupMemberFullInfo) string { return e.UserID })
isInvitedUser := datautil.Contain(userID, invitedUserIDs...)
// 邀请入群通知:允许群主、管理员和被邀请的用户本人看到
if isOwnerOrAdmin || isInvitedUser {
filteredMsgs = append(filteredMsgs, msg)
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberInvitedNotification allowed", "userID", userID, "invitedUserIDs", invitedUserIDs, "isOwnerOrAdmin", isOwnerOrAdmin, "isInvitedUser", isInvitedUser)
} else {
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberInvitedNotification filtered", "userID", userID, "invitedUserIDs", invitedUserIDs, "isOwnerOrAdmin", isOwnerOrAdmin, "isInvitedUser", isInvitedUser)
}
} else if msg.ContentType == constant.MemberEnterNotification {
var tips sdkws.MemberEnterTips
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal MemberEnterTips failed", err)
filteredMsgs = append(filteredMsgs, msg)
continue
}
entrantUserID := tips.EntrantUser.UserID
// 进入群聊通知只允许群主和管理员看到
if isOwnerOrAdmin {
filteredMsgs = append(filteredMsgs, msg)
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberEnterNotification allowed", "userID", userID, "entrantUserID", entrantUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
} else {
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberEnterNotification filtered", "userID", userID, "entrantUserID", entrantUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
}
} else if msg.ContentType == constant.MemberKickedNotification {
var tips sdkws.MemberKickedTips
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal MemberKickedTips failed", err)
filteredMsgs = append(filteredMsgs, msg)
continue
}
// 获取被踢出的用户ID列表
kickedUserIDs := datautil.Slice(tips.KickedUserList, func(e *sdkws.GroupMemberFullInfo) string { return e.UserID })
isKickedUser := datautil.Contain(userID, kickedUserIDs...)
// 被踢出群聊通知:允许群主、管理员和被踢出的用户本人看到
if isOwnerOrAdmin || isKickedUser {
filteredMsgs = append(filteredMsgs, msg)
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberKickedNotification allowed", "userID", userID, "kickedUserIDs", kickedUserIDs, "isOwnerOrAdmin", isOwnerOrAdmin, "isKickedUser", isKickedUser)
} else {
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberKickedNotification filtered", "userID", userID, "kickedUserIDs", kickedUserIDs, "isOwnerOrAdmin", isOwnerOrAdmin, "isKickedUser", isKickedUser)
}
} else if msg.ContentType == constant.GroupMemberInfoSetNotification {
var tips sdkws.GroupMemberInfoSetTips
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal GroupMemberInfoSetTips failed", err)
filteredMsgs = append(filteredMsgs, msg)
continue
}
changedUserID := tips.ChangedUser.UserID
// 群成员信息设置通知(如背景音)只允许群主和管理员看到
if isOwnerOrAdmin {
filteredMsgs = append(filteredMsgs, msg)
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberInfoSetNotification allowed", "userID", userID, "changedUserID", changedUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
} else {
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberInfoSetNotification filtered", "userID", userID, "changedUserID", changedUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
}
} else if msg.ContentType == constant.GroupMemberSetToAdminNotification || msg.ContentType == constant.GroupMemberSetToOrdinaryUserNotification {
var tips sdkws.GroupMemberInfoSetTips
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal GroupMemberInfoSetTips failed", err)
filteredMsgs = append(filteredMsgs, msg)
continue
}
changedUserID := tips.ChangedUser.UserID
// 设置为管理员/普通用户通知:允许群主、管理员和本人看到
if isOwnerOrAdmin || userID == changedUserID {
filteredMsgs = append(filteredMsgs, msg)
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberSetToAdmin/OrdinaryUserNotification allowed", "userID", userID, "changedUserID", changedUserID, "isOwnerOrAdmin", isOwnerOrAdmin, "contentType", msg.ContentType)
} else {
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberSetToAdmin/OrdinaryUserNotification filtered", "userID", userID, "changedUserID", changedUserID, "isOwnerOrAdmin", isOwnerOrAdmin, "contentType", msg.ContentType)
}
} else {
// 其他消息直接通过
filteredMsgs = append(filteredMsgs, msg)
}
}
return filteredMsgs
}
// unmarshalNotificationContent 解析通知消息内容
func unmarshalNotificationContent(content string, v interface{}) error {
var notificationElem sdkws.NotificationElem
if err := json.Unmarshal([]byte(content), &notificationElem); err != nil {
return err
}
return jsonutil.JsonUnmarshal([]byte(notificationElem.Detail), v)
}
// enrichRedPacketMessages 填充红包消息的领取信息和状态
func (m *msgServer) enrichRedPacketMessages(ctx context.Context, userID string, msgs []*sdkws.MsgData) []*sdkws.MsgData {
if m.redPacketReceiveDB == nil || m.redPacketDB == nil {
return msgs
}
for _, msg := range msgs {
// 只处理自定义消息类型
if msg.ContentType != constant.Custom {
continue
}
// 解析自定义消息内容
var customElem apistruct.CustomElem
if err := json.Unmarshal(msg.Content, &customElem); err != nil {
continue
}
// 检查是否为红包消息通过description字段判断二级类型
if customElem.Description != "redpacket" {
continue
}
// 解析红包消息内容从data字段中解析
var redPacketElem apistruct.RedPacketElem
if err := json.Unmarshal([]byte(customElem.Data), &redPacketElem); err != nil {
log.ZWarn(ctx, "enrichRedPacketMessages: failed to unmarshal red packet data", err, "redPacketID", redPacketElem.RedPacketID)
continue
}
// 查询红包记录
redPacket, err := m.redPacketDB.Take(ctx, redPacketElem.RedPacketID)
if err != nil {
log.ZWarn(ctx, "enrichRedPacketMessages: failed to get red packet", err, "redPacketID", redPacketElem.RedPacketID)
// 如果查询失败,保持原有状态,不填充信息
continue
}
// 填充红包状态信息
redPacketElem.Status = redPacket.Status
// 判断是否已过期(检查过期时间和状态)
now := time.Now()
isExpired := redPacket.Status == model.RedPacketStatusExpired || (redPacket.ExpireTime.Before(now) && redPacket.Status == model.RedPacketStatusActive)
redPacketElem.IsExpired = isExpired
// 判断是否已领完
isFinished := redPacket.Status == model.RedPacketStatusFinished || redPacket.RemainCount <= 0
redPacketElem.IsFinished = isFinished
// 如果已过期,更新状态
if isExpired && redPacket.Status == model.RedPacketStatusActive {
redPacket.Status = model.RedPacketStatusExpired
redPacketElem.Status = model.RedPacketStatusExpired
}
// 查询用户是否已领取
receive, err := m.redPacketReceiveDB.FindByUserAndRedPacketID(ctx, userID, redPacketElem.RedPacketID)
if err != nil {
// 如果查询失败或未找到记录,说明未领取
redPacketElem.IsReceived = false
redPacketElem.ReceiveInfo = nil
} else {
// 已领取,填充领取信息(包括金额)
redPacketElem.IsReceived = true
redPacketElem.ReceiveInfo = &apistruct.RedPacketReceiveInfo{
Amount: receive.Amount,
ReceiveTime: receive.ReceiveTime.UnixMilli(),
IsLucky: false, // 已去掉手气最佳功能,始终返回 false
}
}
// 更新自定义消息的data字段包含领取信息和状态
redPacketData := jsonutil.StructToJsonString(redPacketElem)
customElem.Data = redPacketData
// 重新序列化并更新消息内容
newContent, err := json.Marshal(customElem)
if err != nil {
log.ZWarn(ctx, "enrichRedPacketMessages: failed to marshal custom elem", err, "redPacketID", redPacketElem.RedPacketID)
continue
}
msg.Content = newContent
}
return msgs
}
func (m *msgServer) GetMaxSeq(ctx context.Context, req *sdkws.GetMaxSeqReq) (*sdkws.GetMaxSeqResp, error) {
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
return nil, err
}
conversationIDs, err := m.ConversationLocalCache.GetConversationIDs(ctx, req.UserID)
if err != nil {
return nil, err
}
for _, conversationID := range conversationIDs {
conversationIDs = append(conversationIDs, conversationutil.GetNotificationConversationIDByConversationID(conversationID))
}
conversationIDs = append(conversationIDs, conversationutil.GetSelfNotificationConversationID(req.UserID))
log.ZDebug(ctx, "GetMaxSeq", "conversationIDs", conversationIDs)
maxSeqs, err := m.MsgDatabase.GetMaxSeqs(ctx, conversationIDs)
if err != nil {
log.ZWarn(ctx, "GetMaxSeqs error", err, "conversationIDs", conversationIDs, "maxSeqs", maxSeqs)
return nil, err
}
// avoid pulling messages from sessions with a large number of max seq values of 0
for conversationID, seq := range maxSeqs {
if seq == 0 {
delete(maxSeqs, conversationID)
}
}
resp := new(sdkws.GetMaxSeqResp)
resp.MaxSeqs = maxSeqs
return resp, nil
}
func (m *msgServer) SearchMessage(ctx context.Context, req *msg.SearchMessageReq) (resp *msg.SearchMessageResp, err error) {
// var chatLogs []*sdkws.MsgData
var chatLogs []*msg.SearchedMsgData
var total int64
resp = &msg.SearchMessageResp{}
if total, chatLogs, err = m.MsgDatabase.SearchMessage(ctx, req); err != nil {
return nil, err
}
var (
sendIDs []string
recvIDs []string
groupIDs []string
sendNameMap = make(map[string]string)
sendFaceMap = make(map[string]string)
recvMap = make(map[string]string)
groupMap = make(map[string]*sdkws.GroupInfo)
seenSendIDs = make(map[string]struct{})
seenRecvIDs = make(map[string]struct{})
seenGroupIDs = make(map[string]struct{})
)
for _, chatLog := range chatLogs {
if chatLog.MsgData.SenderNickname == "" || chatLog.MsgData.SenderFaceURL == "" {
if _, ok := seenSendIDs[chatLog.MsgData.SendID]; !ok {
seenSendIDs[chatLog.MsgData.SendID] = struct{}{}
sendIDs = append(sendIDs, chatLog.MsgData.SendID)
}
}
switch chatLog.MsgData.SessionType {
case constant.SingleChatType, constant.NotificationChatType:
if _, ok := seenRecvIDs[chatLog.MsgData.RecvID]; !ok {
seenRecvIDs[chatLog.MsgData.RecvID] = struct{}{}
recvIDs = append(recvIDs, chatLog.MsgData.RecvID)
}
case constant.WriteGroupChatType, constant.ReadGroupChatType:
if _, ok := seenGroupIDs[chatLog.MsgData.GroupID]; !ok {
seenGroupIDs[chatLog.MsgData.GroupID] = struct{}{}
groupIDs = append(groupIDs, chatLog.MsgData.GroupID)
}
}
}
// Retrieve sender and receiver information
if len(sendIDs) != 0 {
sendInfos, err := m.UserLocalCache.GetUsersInfo(ctx, sendIDs)
if err != nil {
return nil, err
}
for _, sendInfo := range sendInfos {
sendNameMap[sendInfo.UserID] = sendInfo.Nickname
sendFaceMap[sendInfo.UserID] = sendInfo.FaceURL
}
}
if len(recvIDs) != 0 {
recvInfos, err := m.UserLocalCache.GetUsersInfo(ctx, recvIDs)
if err != nil {
return nil, err
}
for _, recvInfo := range recvInfos {
recvMap[recvInfo.UserID] = recvInfo.Nickname
}
}
// Retrieve group information including member counts
if len(groupIDs) != 0 {
groupInfos, err := m.GroupLocalCache.GetGroupInfos(ctx, groupIDs)
if err != nil {
return nil, err
}
for _, groupInfo := range groupInfos {
groupMap[groupInfo.GroupID] = groupInfo
// Get actual member count
memberIDs, err := m.GroupLocalCache.GetGroupMemberIDs(ctx, groupInfo.GroupID)
if err == nil {
groupInfo.MemberCount = uint32(len(memberIDs)) // Update the member count with actual number
}
}
}
// Construct response with updated information
for _, chatLog := range chatLogs {
pbchatLog := &msg.ChatLog{}
datautil.CopyStructFields(pbchatLog, chatLog.MsgData)
pbchatLog.SendTime = chatLog.MsgData.SendTime
pbchatLog.CreateTime = chatLog.MsgData.CreateTime
if chatLog.MsgData.SenderNickname == "" {
pbchatLog.SenderNickname = sendNameMap[chatLog.MsgData.SendID]
}
if chatLog.MsgData.SenderFaceURL == "" {
pbchatLog.SenderFaceURL = sendFaceMap[chatLog.MsgData.SendID]
}
switch chatLog.MsgData.SessionType {
case constant.SingleChatType, constant.NotificationChatType:
pbchatLog.RecvNickname = recvMap[chatLog.MsgData.RecvID]
case constant.ReadGroupChatType:
groupInfo := groupMap[chatLog.MsgData.GroupID]
pbchatLog.GroupMemberCount = groupInfo.MemberCount // Reflects actual member count
pbchatLog.RecvID = groupInfo.GroupID
pbchatLog.GroupName = groupInfo.GroupName
pbchatLog.GroupOwner = groupInfo.OwnerUserID
pbchatLog.GroupType = groupInfo.GroupType
}
searchChatLog := &msg.SearchChatLog{ChatLog: pbchatLog, IsRevoked: chatLog.IsRevoked}
resp.ChatLogs = append(resp.ChatLogs, searchChatLog)
}
resp.ChatLogsNum = int32(total)
return resp, nil
}
func (m *msgServer) GetServerTime(ctx context.Context, _ *msg.GetServerTimeReq) (*msg.GetServerTimeResp, error) {
return &msg.GetServerTimeResp{ServerTime: timeutil.GetCurrentTimestampByMill()}, nil
}
func (m *msgServer) GetLastMessage(ctx context.Context, req *msg.GetLastMessageReq) (*msg.GetLastMessageResp, error) {
msgs, err := m.MsgDatabase.GetLastMessage(ctx, req.ConversationIDs, req.UserID)
if err != nil {
return nil, err
}
return &msg.GetLastMessageResp{Msgs: msgs}, nil
}

91
internal/rpc/msg/utils.go Normal file
View File

@@ -0,0 +1,91 @@
// 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 (
"git.imall.cloud/openim/protocol/msg"
"github.com/openimsdk/tools/errs"
"github.com/redis/go-redis/v9"
"go.mongodb.org/mongo-driver/mongo"
)
func IsNotFound(err error) bool {
switch errs.Unwrap(err) {
case redis.Nil, mongo.ErrNoDocuments:
return true
default:
return false
}
}
type activeConversations []*msg.ActiveConversation
func (s activeConversations) Len() int {
return len(s)
}
func (s activeConversations) Less(i, j int) bool {
return s[i].LastTime > s[j].LastTime
}
func (s activeConversations) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
//type seqTime struct {
// ConversationID string
// Seq int64
// Time int64
// Unread int64
// Pinned bool
//}
//
//func (s seqTime) String() string {
// return fmt.Sprintf("<Time_%d,Unread_%d,Pinned_%t>", s.Time, s.Unread, s.Pinned)
//}
//
//type seqTimes []seqTime
//
//func (s seqTimes) Len() int {
// return len(s)
//}
//
//// Less sticky priority, unread priority, time descending
//func (s seqTimes) Less(i, j int) bool {
// iv, jv := s[i], s[j]
// if iv.Pinned && (!jv.Pinned) {
// return true
// }
// if jv.Pinned && (!iv.Pinned) {
// return false
// }
// if iv.Unread > 0 && jv.Unread == 0 {
// return true
// }
// if jv.Unread > 0 && iv.Unread == 0 {
// return false
// }
// return iv.Time > jv.Time
//}
//
//func (s seqTimes) Swap(i, j int) {
// s[i], s[j] = s[j], s[i]
//}
//
//type conversationStatus struct {
// ConversationID string
// Pinned bool
// Recv bool
//}

405
internal/rpc/msg/verify.go Normal file
View File

@@ -0,0 +1,405 @@
// 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
}