feat: incr sync version.

This commit is contained in:
Gordon
2024-06-24 17:48:33 +08:00
parent e8ccae6349
commit 88b8043224
308 changed files with 55952 additions and 59 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,714 @@
// Copyright © 2023 OpenIM SDK. 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 conversation_msg
import (
"context"
"encoding/json"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/tools/utils/datautil"
"reflect"
"runtime"
"time"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (c *Conversation) Work(c2v common.Cmd2Value) {
log.ZDebug(c2v.Ctx, "NotificationCmd start", "cmd", c2v.Cmd, "value", c2v.Value)
defer log.ZDebug(c2v.Ctx, "NotificationCmd end", "cmd", c2v.Cmd, "value", c2v.Value)
switch c2v.Cmd {
case constant.CmdNewMsgCome:
c.doMsgNew(c2v)
case constant.CmdSuperGroupMsgCome:
case constant.CmdUpdateConversation:
c.doUpdateConversation(c2v)
case constant.CmdUpdateMessage:
c.doUpdateMessage(c2v)
case constant.CmSyncReactionExtensions:
case constant.CmdNotification:
c.doNotificationNew(c2v)
}
}
func (c *Conversation) doNotificationNew(c2v common.Cmd2Value) {
ctx := c2v.Ctx
allMsg := c2v.Value.(sdk_struct.CmdNewMsgComeToConversation).Msgs
syncFlag := c2v.Value.(sdk_struct.CmdNewMsgComeToConversation).SyncFlag
switch syncFlag {
case constant.MsgSyncBegin:
c.startTime = time.Now()
c.ConversationListener().OnSyncServerStart()
if err := c.SyncAllConversationHashReadSeqs(ctx); err != nil {
log.ZError(ctx, "SyncConversationHashReadSeqs err", err)
}
//clear SubscriptionStatusMap
c.user.OnlineStatusCache.DeleteAll()
for _, syncFunc := range []func(c context.Context) error{
c.user.SyncLoginUserInfo,
c.friend.SyncAllBlackList, c.friend.SyncAllFriendApplication, c.friend.SyncAllSelfFriendApplication,
c.group.SyncAllAdminGroupApplication, c.group.SyncAllSelfGroupApplication, c.user.SyncAllCommand,
} {
go func(syncFunc func(c context.Context) error) {
_ = syncFunc(ctx)
}(syncFunc)
}
syncFunctions := []func(c context.Context) error{
c.group.SyncAllJoinedGroupsAndMembers, c.friend.IncrSyncFriends,
}
for _, syncFunc := range syncFunctions {
funcName := runtime.FuncForPC(reflect.ValueOf(syncFunc).Pointer()).Name()
startTime := time.Now()
err := syncFunc(ctx)
duration := time.Since(startTime)
if err != nil {
log.ZWarn(ctx, fmt.Sprintf("%s sync err", funcName), err, "duration", duration)
} else {
log.ZDebug(ctx, fmt.Sprintf("%s completed successfully", funcName), "duration", duration)
}
}
case constant.MsgSyncFailed:
c.ConversationListener().OnSyncServerFailed()
case constant.MsgSyncEnd:
log.ZDebug(ctx, "MsgSyncEnd", "time", time.Since(c.startTime).Milliseconds())
defer c.ConversationListener().OnSyncServerFinish()
go c.SyncAllConversations(ctx)
}
for conversationID, msgs := range allMsg {
log.ZDebug(ctx, "notification handling", "conversationID", conversationID, "msgs", msgs)
if len(msgs.Msgs) != 0 {
lastMsg := msgs.Msgs[len(msgs.Msgs)-1]
log.ZDebug(ctx, "SetNotificationSeq", "conversationID", conversationID, "seq", lastMsg.Seq)
if lastMsg.Seq != 0 {
if err := c.db.SetNotificationSeq(ctx, conversationID, lastMsg.Seq); err != nil {
log.ZError(ctx, "SetNotificationSeq err", err, "conversationID", conversationID, "lastMsg", lastMsg)
}
}
}
for _, v := range msgs.Msgs {
switch {
case v.ContentType == constant.ConversationChangeNotification:
c.DoConversationChangedNotification(ctx, v)
case v.ContentType == constant.ConversationPrivateChatNotification:
c.DoConversationIsPrivateChangedNotification(ctx, v)
case v.ContentType == constant.ConversationUnreadNotification:
var tips sdkws.ConversationHasReadTips
_ = json.Unmarshal(v.Content, &tips)
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: tips.ConversationID, Action: constant.UnreadCountSetZero}})
c.db.DeleteConversationUnreadMessageList(ctx, tips.ConversationID, tips.UnreadCountTime)
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: []string{tips.ConversationID}}})
continue
case v.ContentType == constant.BusinessNotification:
c.business.DoNotification(ctx, v)
continue
case v.ContentType == constant.RevokeNotification:
c.doRevokeMsg(ctx, v)
case v.ContentType == constant.ClearConversationNotification:
c.doClearConversations(ctx, v)
case v.ContentType == constant.DeleteMsgsNotification:
c.doDeleteMsgs(ctx, v)
case v.ContentType == constant.HasReadReceipt:
c.doReadDrawing(ctx, v)
}
switch v.SessionType {
case constant.SingleChatType:
if v.ContentType > constant.FriendNotificationBegin && v.ContentType < constant.FriendNotificationEnd {
c.friend.DoNotification(ctx, v)
} else if v.ContentType > constant.UserNotificationBegin && v.ContentType < constant.UserNotificationEnd {
c.user.DoNotification(ctx, v)
} else if datautil.Contain(v.ContentType, constant.GroupApplicationRejectedNotification, constant.GroupApplicationAcceptedNotification, constant.JoinGroupApplicationNotification) {
c.group.DoNotification(ctx, v)
} else if v.ContentType > constant.SignalingNotificationBegin && v.ContentType < constant.SignalingNotificationEnd {
continue
}
case constant.GroupChatType, constant.SuperGroupChatType:
if v.ContentType > constant.GroupNotificationBegin && v.ContentType < constant.GroupNotificationEnd {
c.group.DoNotification(ctx, v)
} else if v.ContentType > constant.SignalingNotificationBegin && v.ContentType < constant.SignalingNotificationEnd {
continue
}
}
}
}
}
func (c *Conversation) doDeleteConversation(c2v common.Cmd2Value) {
node := c2v.Value.(common.DeleteConNode)
ctx := c2v.Ctx
// Mark messages related to this conversation for deletion
err := c.db.UpdateMessageStatusBySourceID(context.Background(), node.SourceID, constant.MsgStatusHasDeleted, int32(node.SessionType))
if err != nil {
log.ZError(ctx, "setMessageStatusBySourceID", err)
return
}
// Reset the session information, empty session
err = c.db.ResetConversation(ctx, node.ConversationID)
if err != nil {
log.ZError(ctx, "ResetConversation err:", err)
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{"", constant.TotalUnreadMessageChanged, ""}})
}
func (c *Conversation) getConversationLatestMsgClientID(latestMsg string) string {
msg := &sdk_struct.MsgStruct{}
if err := json.Unmarshal([]byte(latestMsg), msg); err != nil {
log.ZError(context.Background(), "getConversationLatestMsgClientID", err, "latestMsg", latestMsg)
}
return msg.ClientMsgID
}
func (c *Conversation) doUpdateConversation(c2v common.Cmd2Value) {
ctx := c2v.Ctx
node := c2v.Value.(common.UpdateConNode)
switch node.Action {
case constant.AddConOrUpLatMsg:
var list []*model_struct.LocalConversation
lc := node.Args.(model_struct.LocalConversation)
oc, err := c.db.GetConversation(ctx, lc.ConversationID)
if err == nil {
// log.Info("this is old conversation", *oc)
if lc.LatestMsgSendTime >= oc.LatestMsgSendTime || c.getConversationLatestMsgClientID(lc.LatestMsg) == c.getConversationLatestMsgClientID(oc.LatestMsg) { // The session update of asynchronous messages is subject to the latest sending time
err := c.db.UpdateColumnsConversation(ctx, node.ConID, map[string]interface{}{"latest_msg_send_time": lc.LatestMsgSendTime, "latest_msg": lc.LatestMsg})
if err != nil {
log.ZError(ctx, "updateConversationLatestMsgModel", err, "conversationID", node.ConID)
} else {
oc.LatestMsgSendTime = lc.LatestMsgSendTime
oc.LatestMsg = lc.LatestMsg
list = append(list, oc)
c.ConversationListener().OnConversationChanged(utils.StructToJsonString(list))
}
}
} else {
// log.Info("this is new conversation", lc)
err4 := c.db.InsertConversation(ctx, &lc)
if err4 != nil {
// log.Error("internal", "insert new conversation err:", err4.Error())
} else {
list = append(list, &lc)
c.ConversationListener().OnNewConversation(utils.StructToJsonString(list))
}
}
case constant.UnreadCountSetZero:
if err := c.db.UpdateColumnsConversation(ctx, node.ConID, map[string]interface{}{"unread_count": 0}); err != nil {
log.ZError(ctx, "updateConversationUnreadCountModel err", err, "conversationID", node.ConID)
} else {
totalUnreadCount, err := c.db.GetTotalUnreadMsgCountDB(ctx)
if err == nil {
c.ConversationListener().OnTotalUnreadMessageCountChanged(totalUnreadCount)
} else {
log.ZError(ctx, "getTotalUnreadMsgCountDB err", err)
}
}
// case ConChange:
// err, list := u.getAllConversationListModel()
// if err != nil {
// sdkLog("getAllConversationListModel database err:", err.Error())
// } else {
// if list == nil {
// u.ConversationListenerx.OnConversationChanged(structToJsonString([]ConversationStruct{}))
// } else {
// u.ConversationListenerx.OnConversationChanged(structToJsonString(list))
//
// }
// }
case constant.IncrUnread:
err := c.db.IncrConversationUnreadCount(ctx, node.ConID)
if err != nil {
// log.Error("internal", "incrConversationUnreadCount database err:", err.Error())
return
}
case constant.TotalUnreadMessageChanged:
totalUnreadCount, err := c.db.GetTotalUnreadMsgCountDB(ctx)
if err != nil {
// log.Error("internal", "TotalUnreadMessageChanged database err:", err.Error())
} else {
c.ConversationListener().OnTotalUnreadMessageCountChanged(totalUnreadCount)
}
case constant.UpdateConFaceUrlAndNickName:
var lc model_struct.LocalConversation
st := node.Args.(common.SourceIDAndSessionType)
log.ZInfo(ctx, "UpdateConFaceUrlAndNickName", "st", st)
switch st.SessionType {
case constant.SingleChatType:
lc.UserID = st.SourceID
lc.ConversationID = c.getConversationIDBySessionType(st.SourceID, constant.SingleChatType)
lc.ConversationType = constant.SingleChatType
case constant.SuperGroupChatType:
conversationID, conversationType, err := c.getConversationTypeByGroupID(ctx, st.SourceID)
if err != nil {
return
}
lc.GroupID = st.SourceID
lc.ConversationID = conversationID
lc.ConversationType = conversationType
case constant.NotificationChatType:
lc.UserID = st.SourceID
lc.ConversationID = c.getConversationIDBySessionType(st.SourceID, constant.NotificationChatType)
lc.ConversationType = constant.NotificationChatType
default:
log.ZError(ctx, "not support sessionType", nil, "sessionType", st.SessionType)
return
}
lc.ShowName = st.Nickname
lc.FaceURL = st.FaceURL
err := c.db.UpdateConversation(ctx, &lc)
if err != nil {
// log.Error("internal", "setConversationFaceUrlAndNickName database err:", err.Error())
return
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: lc.ConversationID, Action: constant.ConChange, Args: []string{lc.ConversationID}}})
case constant.UpdateLatestMessageChange:
conversationID := node.ConID
var latestMsg sdk_struct.MsgStruct
l, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
log.ZError(ctx, "getConversationLatestMsgModel err", err, "conversationID", conversationID)
} else {
err := json.Unmarshal([]byte(l.LatestMsg), &latestMsg)
if err != nil {
log.ZError(ctx, "latestMsg,Unmarshal err", err)
} else {
latestMsg.IsRead = true
newLatestMessage := utils.StructToJsonString(latestMsg)
err = c.db.UpdateColumnsConversation(ctx, node.ConID, map[string]interface{}{"latest_msg_send_time": latestMsg.SendTime, "latest_msg": newLatestMessage})
if err != nil {
log.ZError(ctx, "updateConversationLatestMsgModel err", err)
}
}
}
case constant.ConChange:
conversationIDs := node.Args.([]string)
conversations, err := c.db.GetMultipleConversationDB(ctx, conversationIDs)
if err != nil {
log.ZError(ctx, "getMultipleConversationModel err", err)
} else {
var newCList []*model_struct.LocalConversation
for _, v := range conversations {
if v.LatestMsgSendTime != 0 {
newCList = append(newCList, v)
}
}
c.ConversationListener().OnConversationChanged(utils.StructToJsonStringDefault(newCList))
}
case constant.NewCon:
cidList := node.Args.([]string)
cLists, err := c.db.GetMultipleConversationDB(ctx, cidList)
if err != nil {
// log.Error("internal", "getMultipleConversationModel err :", err.Error())
} else {
if cLists != nil {
// log.Info("internal", "getMultipleConversationModel success :", cLists)
c.ConversationListener().OnNewConversation(utils.StructToJsonString(cLists))
}
}
case constant.ConChangeDirect:
cidList := node.Args.(string)
c.ConversationListener().OnConversationChanged(cidList)
case constant.NewConDirect:
cidList := node.Args.(string)
// log.Debug("internal", "NewConversation", cidList)
c.ConversationListener().OnNewConversation(cidList)
case constant.ConversationLatestMsgHasRead:
hasReadMsgList := node.Args.(map[string][]string)
var result []*model_struct.LocalConversation
var latestMsg sdk_struct.MsgStruct
var lc model_struct.LocalConversation
for conversationID, msgIDList := range hasReadMsgList {
LocalConversation, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
// log.Error("internal", "get conversation err", err.Error(), conversationID)
continue
}
err = utils.JsonStringToStruct(LocalConversation.LatestMsg, &latestMsg)
if err != nil {
// log.Error("internal", "JsonStringToStruct err", err.Error(), conversationID)
continue
}
if utils.IsContain(latestMsg.ClientMsgID, msgIDList) {
latestMsg.IsRead = true
lc.ConversationID = conversationID
lc.LatestMsg = utils.StructToJsonString(latestMsg)
LocalConversation.LatestMsg = utils.StructToJsonString(latestMsg)
err := c.db.UpdateConversation(ctx, &lc)
if err != nil {
// log.Error("internal", "UpdateConversation database err:", err.Error())
continue
} else {
result = append(result, LocalConversation)
}
}
}
if result != nil {
// log.Info("internal", "getMultipleConversationModel success :", result)
c.ConversationListener().OnNewConversation(utils.StructToJsonString(result))
}
case constant.SyncConversation:
}
}
func (c *Conversation) doUpdateMessage(c2v common.Cmd2Value) {
node := c2v.Value.(common.UpdateMessageNode)
ctx := c2v.Ctx
switch node.Action {
case constant.UpdateMsgFaceUrlAndNickName:
args := node.Args.(common.UpdateMessageInfo)
switch args.SessionType {
case constant.SingleChatType:
if args.UserID == c.loginUserID {
conversationIDList, err := c.db.GetAllSingleConversationIDList(ctx)
if err != nil {
log.ZError(ctx, "GetAllSingleConversationIDList err", err)
return
} else {
log.ZDebug(ctx, "get single conversationID list", "conversationIDList", conversationIDList)
for _, conversationID := range conversationIDList {
err := c.db.UpdateMsgSenderFaceURLAndSenderNickname(ctx, conversationID, args.UserID, args.FaceURL, args.Nickname)
if err != nil {
log.ZError(ctx, "UpdateMsgSenderFaceURLAndSenderNickname err", err)
continue
}
}
}
} else {
conversationID := c.getConversationIDBySessionType(args.UserID, constant.SingleChatType)
err := c.db.UpdateMsgSenderFaceURLAndSenderNickname(ctx, conversationID, args.UserID, args.FaceURL, args.Nickname)
if err != nil {
log.ZError(ctx, "UpdateMsgSenderFaceURLAndSenderNickname err", err)
}
}
case constant.SuperGroupChatType:
conversationID := c.getConversationIDBySessionType(args.GroupID, constant.SuperGroupChatType)
err := c.db.UpdateMsgSenderFaceURLAndSenderNickname(ctx, conversationID, args.UserID, args.FaceURL, args.Nickname)
if err != nil {
log.ZError(ctx, "UpdateMsgSenderFaceURLAndSenderNickname err", err)
}
case constant.NotificationChatType:
conversationID := c.getConversationIDBySessionType(args.UserID, constant.NotificationChatType)
err := c.db.UpdateMsgSenderFaceURLAndSenderNickname(ctx, conversationID, args.UserID, args.FaceURL, args.Nickname)
if err != nil {
log.ZError(ctx, "UpdateMsgSenderFaceURLAndSenderNickname err", err)
}
default:
log.ZError(ctx, "not support sessionType", nil, "args", args)
return
}
}
}
// funcation (c *Conversation) doSyncReactionExtensions(c2v common.Cmd2Value) {
// if c.ConversationListener == nil {
// // log.Error("internal", "not set conversationListener")
// return
// }
// node := c2v.Value.(common.SyncReactionExtensionsNode)
// ctx := mcontext.NewCtx(node.OperationID)
// switch node.Action {
// case constant.SyncMessageListReactionExtensions:
// args := node.Args.(syncReactionExtensionParams)
// // log.Error(node.OperationID, "come SyncMessageListReactionExtensions", args)
// var reqList []server_api_params.OperateMessageListReactionExtensionsReq
// for _, v := range args.MessageList {
// var temp server_api_params.OperateMessageListReactionExtensionsReq
// temp.ClientMsgID = v.ClientMsgID
// temp.MsgFirstModifyTime = v.MsgFirstModifyTime
// reqList = append(reqList, temp)
// }
// var apiReq server_api_params.GetMessageListReactionExtensionsReq
// apiReq.SourceID = args.SourceID
// apiReq.TypeKeyList = args.TypeKeyList
// apiReq.SessionType = args.SessionType
// apiReq.MessageReactionKeyList = reqList
// apiReq.IsExternalExtensions = args.IsExternalExtension
// apiReq.OperationID = node.OperationID
// apiResp, err := util.CallApi[server_api_params.GetMessageListReactionExtensionsResp](ctx, constant.GetMessageListReactionExtensionsRouter, &apiReq)
// if err != nil {
// // log.NewError(node.OperationID, utils.GetSelfFuncName(), "getMessageListReactionExtensions err:", err.Error())
// return
// }
// // for _, result := range apiResp {
// // log.Warn(node.OperationID, "api return reslut is:", result.ClientMsgID, result.ReactionExtensionList)
// // }
// onLocal := funcation(data []*model_struct.LocalChatLogReactionExtensions) []*server_api_params.SingleMessageExtensionResult {
// var result []*server_api_params.SingleMessageExtensionResult
// for _, v := range data {
// temp := new(server_api_params.SingleMessageExtensionResult)
// tempMap := make(map[string]*sdkws.KeyValue)
// _ = json.Unmarshal(v.LocalReactionExtensions, &tempMap)
// if len(args.TypeKeyList) != 0 {
// for s, _ := range tempMap {
// if !utils.IsContain(s, args.TypeKeyList) {
// delete(tempMap, s)
// }
// }
// }
//
// temp.ReactionExtensionList = tempMap
// temp.ClientMsgID = v.ClientMsgID
// result = append(result, temp)
// }
// return result
// }(args.ExtendMessageList)
// var onServer []*server_api_params.SingleMessageExtensionResult
// for _, v := range *apiResp {
// if v.ErrCode == 0 {
// onServer = append(onServer, v)
// }
// }
// aInBNot, _, sameA, _ := common.CheckReactionExtensionsDiff(onServer, onLocal)
// for _, v := range aInBNot {
// // log.Error(node.OperationID, "come InsertMessageReactionExtension", args, v.ClientMsgID)
// if len(v.ReactionExtensionList) > 0 {
// temp := model_struct.LocalChatLogReactionExtensions{ClientMsgID: v.ClientMsgID, LocalReactionExtensions: []byte(utils.StructToJsonString(v.ReactionExtensionList))}
// err := c.db.InsertMessageReactionExtension(ctx, &temp)
// if err != nil {
// // log.Error(node.OperationID, "InsertMessageReactionExtension err:", err.Error())
// continue
// }
// }
// var changedKv []*sdkws.KeyValue
// for _, value := range v.ReactionExtensionList {
// changedKv = append(changedKv, value)
// }
// if len(changedKv) > 0 {
// c.msgListener.OnRecvMessageExtensionsChanged(v.ClientMsgID, utils.StructToJsonString(changedKv))
// }
// }
// // for _, result := range sameA {
// // log.ZWarn(ctx, "result", result.ReactionExtensionList, result.ClientMsgID)
// // }
// for _, v := range sameA {
// // log.Error(node.OperationID, "come sameA", v.ClientMsgID, v.ReactionExtensionList)
// tempMap := make(map[string]*sdkws.KeyValue)
// for _, extensions := range args.ExtendMessageList {
// if v.ClientMsgID == extensions.ClientMsgID {
// _ = json.Unmarshal(extensions.LocalReactionExtensions, &tempMap)
// break
// }
// }
// if len(v.ReactionExtensionList) == 0 {
// err := c.db.DeleteMessageReactionExtension(ctx, v.ClientMsgID)
// if err != nil {
// // log.Error(node.OperationID, "DeleteMessageReactionExtension err:", err.Error())
// continue
// }
// var deleteKeyList []string
// for key, _ := range tempMap {
// deleteKeyList = append(deleteKeyList, key)
// }
// if len(deleteKeyList) > 0 {
// c.msgListener.OnRecvMessageExtensionsDeleted(v.ClientMsgID, utils.StructToJsonString(deleteKeyList))
// }
// } else {
// deleteKeyList, changedKv := funcation(local, server map[string]*sdkws.KeyValue) ([]string, []*sdkws.KeyValue) {
// var deleteKeyList []string
// var changedKv []*sdkws.KeyValue
// for k, v := range local {
// ia, ok := server[k]
// if ok {
// //服务器不同的kv
// if ia.Value != v.Value {
// changedKv = append(changedKv, ia)
// }
// } else {
// //服务器已经没有kv
// deleteKeyList = append(deleteKeyList, k)
// }
// }
// //从服务器新增的kv
// for k, v := range server {
// _, ok := local[k]
// if !ok {
// changedKv = append(changedKv, v)
//
// }
// }
// return deleteKeyList, changedKv
// }(tempMap, v.ReactionExtensionList)
// extendMsg := model_struct.LocalChatLogReactionExtensions{ClientMsgID: v.ClientMsgID, LocalReactionExtensions: []byte(utils.StructToJsonString(v.ReactionExtensionList))}
// err = c.db.UpdateMessageReactionExtension(ctx, &extendMsg)
// if err != nil {
// // log.Error(node.OperationID, "UpdateMessageReactionExtension err:", err.Error())
// continue
// }
// if len(deleteKeyList) > 0 {
// c.msgListener.OnRecvMessageExtensionsDeleted(v.ClientMsgID, utils.StructToJsonString(deleteKeyList))
// }
// if len(changedKv) > 0 {
// c.msgListener.OnRecvMessageExtensionsChanged(v.ClientMsgID, utils.StructToJsonString(changedKv))
// }
// }
// //err := c.db.GetAndUpdateMessageReactionExtension(v.ClientMsgID, v.ReactionExtensionList)
// //if err != nil {
// // log.Error(node.OperationID, "GetAndUpdateMessageReactionExtension err:", err.Error())
// // continue
// //}
// //var changedKv []*server_api_params.KeyValue
// //for _, value := range v.ReactionExtensionList {
// // changedKv = append(changedKv, value)
// //}
// //if len(changedKv) > 0 {
// // c.msgListener.OnRecvMessageExtensionsChanged(v.ClientMsgID, utils.StructToJsonString(changedKv))
// //}
// }
// case constant.SyncMessageListTypeKeyInfo:
// messageList := node.Args.([]*sdk_struct.MsgStruct)
// var sourceID string
// var sessionType int32
// var reqList []server_api_params.OperateMessageListReactionExtensionsReq
// var temp server_api_params.OperateMessageListReactionExtensionsReq
// for _, v := range messageList {
// //todo syncMessage must sync
// message, err := c.db.GetMessage(ctx, "", v.ClientMsgID)
// if err != nil {
// // log.Error(node.OperationID, "GetMessageController err:", err.Error(), *v)
// continue
// }
// temp.ClientMsgID = message.ClientMsgID
// temp.MsgFirstModifyTime = message.MsgFirstModifyTime
// reqList = append(reqList, temp)
// switch message.SessionType {
// case constant.SingleChatType:
// sourceID = message.SendID + message.RecvID
// case constant.NotificationChatType:
// sourceID = message.RecvID
// case constant.GroupChatType, constant.SuperGroupChatType:
// sourceID = message.RecvID
// }
// sessionType = message.SessionType
// }
// var apiReq server_api_params.GetMessageListReactionExtensionsReq
// apiReq.SourceID = sourceID
// apiReq.SessionType = sessionType
// apiReq.MessageReactionKeyList = reqList
// apiReq.OperationID = node.OperationID
// //var apiResp server_api_params.GetMessageListReactionExtensionsResp
//
// apiResp, err := util.CallApi[server_api_params.GetMessageListReactionExtensionsResp](ctx, constant.GetMessageListReactionExtensionsRouter, &apiReq)
// if err != nil {
// // log.Error(node.OperationID, "GetMessageListReactionExtensions from server err:", err.Error(), apiReq)
// return
// }
// var messageChangedList []*messageKvList
// for _, v := range *apiResp {
// if v.ErrCode == 0 {
// var changedKv []*sdkws.KeyValue
// var prefixTypeKey []string
// extendMsg, _ := c.db.GetMessageReactionExtension(ctx, v.ClientMsgID)
// localKV := make(map[string]*sdkws.KeyValue)
// _ = json.Unmarshal(extendMsg.LocalReactionExtensions, &localKV)
// for typeKey, value := range v.ReactionExtensionList {
// oldValue, ok := localKV[typeKey]
// if ok {
// if !cmp.Equal(value, oldValue) {
// localKV[typeKey] = value
// prefixTypeKey = append(prefixTypeKey, getPrefixTypeKey(typeKey))
// changedKv = append(changedKv, value)
// }
// } else {
// localKV[typeKey] = value
// prefixTypeKey = append(prefixTypeKey, getPrefixTypeKey(typeKey))
// changedKv = append(changedKv, value)
//
// }
//
// }
// extendMsg.LocalReactionExtensions = []byte(utils.StructToJsonString(localKV))
// _ = c.db.UpdateMessageReactionExtension(ctx, extendMsg)
// if len(changedKv) > 0 {
// c.msgListener.OnRecvMessageExtensionsChanged(extendMsg.ClientMsgID, utils.StructToJsonString(changedKv))
// }
// prefixTypeKey = utils.RemoveRepeatedStringInList(prefixTypeKey)
// if len(prefixTypeKey) > 0 && c.msgKvListener != nil {
// var result []*sdk.SingleTypeKeyInfoSum
// oneMessageChanged := new(messageKvList)
// oneMessageChanged.ClientMsgID = extendMsg.ClientMsgID
// for _, v := range prefixTypeKey {
// singleResult := new(sdk.SingleTypeKeyInfoSum)
// singleResult.TypeKey = v
// for typeKey, value := range localKV {
// if strings.HasPrefix(typeKey, v) {
// singleTypeKeyInfo := new(sdk.SingleTypeKeyInfo)
// err := json.Unmarshal([]byte(value.Value), singleTypeKeyInfo)
// if err != nil {
// continue
// }
// if _, ok := singleTypeKeyInfo.InfoList[c.loginUserID]; ok {
// singleResult.IsContainSelf = true
// }
// for _, info := range singleTypeKeyInfo.InfoList {
// v := *info
// singleResult.InfoList = append(singleResult.InfoList, &v)
// }
// singleResult.Counter += singleTypeKeyInfo.Counter
// }
// }
// result = append(result, singleResult)
// }
// oneMessageChanged.ChangedKvList = result
// messageChangedList = append(messageChangedList, oneMessageChanged)
// }
// }
// }
// if len(messageChangedList) > 0 && c.msgKvListener != nil {
// c.msgKvListener.OnMessageKvInfoChanged(utils.StructToJsonString(messageChangedList))
// }
//
// }
//
// }
func (c *Conversation) DoConversationChangedNotification(ctx context.Context, msg *sdkws.MsgData) {
//var notification sdkws.ConversationChangedNotification
tips := &sdkws.ConversationUpdateTips{}
if err := utils.UnmarshalNotificationElem(msg.Content, tips); err != nil {
log.ZError(ctx, "UnmarshalNotificationElem err", err, "msg", msg)
return
}
c.SyncConversations(ctx, tips.ConversationIDList)
}
func (c *Conversation) DoConversationIsPrivateChangedNotification(ctx context.Context, msg *sdkws.MsgData) {
tips := &sdkws.ConversationSetPrivateTips{}
if err := utils.UnmarshalNotificationElem(msg.Content, tips); err != nil {
log.ZError(ctx, "UnmarshalNotificationElem err", err, "msg", msg)
return
}
c.SyncConversations(ctx, []string{tips.ConversationID})
}

View File

@@ -0,0 +1,57 @@
// Copyright © 2023 OpenIM SDK. 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 conversation_msg
import (
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
pbConversation "github.com/openimsdk/protocol/conversation"
)
func ServerConversationToLocal(conversation *pbConversation.Conversation) *model_struct.LocalConversation {
return &model_struct.LocalConversation{
ConversationID: conversation.ConversationID,
ConversationType: conversation.ConversationType,
UserID: conversation.UserID,
GroupID: conversation.GroupID,
RecvMsgOpt: conversation.RecvMsgOpt,
GroupAtType: conversation.GroupAtType,
IsPinned: conversation.IsPinned,
BurnDuration: conversation.BurnDuration,
IsPrivateChat: conversation.IsPrivateChat,
AttachedInfo: conversation.AttachedInfo,
Ex: conversation.Ex,
MsgDestructTime: conversation.MsgDestructTime,
IsMsgDestruct: conversation.IsMsgDestruct,
}
}
func LocalConversationToServer(conversation *model_struct.LocalConversation) *pbConversation.Conversation {
return &pbConversation.Conversation{
ConversationID: conversation.ConversationID,
ConversationType: conversation.ConversationType,
UserID: conversation.UserID,
GroupID: conversation.GroupID,
RecvMsgOpt: conversation.RecvMsgOpt,
GroupAtType: conversation.GroupAtType,
IsPinned: conversation.IsPinned,
BurnDuration: conversation.BurnDuration,
IsPrivateChat: conversation.IsPrivateChat,
AttachedInfo: conversation.AttachedInfo,
MsgDestructTime: conversation.MsgDestructTime,
Ex: conversation.Ex,
IsMsgDestruct: conversation.IsMsgDestruct,
}
}

View File

@@ -0,0 +1,492 @@
// Copyright © 2023 OpenIM SDK. 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 conversation_msg
import (
"context"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"os"
"path/filepath"
"strings"
)
func (c *Conversation) CreateTextMessage(ctx context.Context, text string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Text)
if err != nil {
return nil, err
}
s.TextElem = &sdk_struct.TextElem{Content: text}
return &s, nil
}
func (c *Conversation) CreateAdvancedTextMessage(ctx context.Context, text string, messageEntities []*sdk_struct.MessageEntity) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.AdvancedText)
if err != nil {
return nil, err
}
s.AdvancedTextElem = &sdk_struct.AdvancedTextElem{
Text: text,
MessageEntityList: messageEntities,
}
return &s, nil
}
func (c *Conversation) CreateTextAtMessage(ctx context.Context, text string, userIDList []string, usersInfo []*sdk_struct.AtInfo, qs *sdk_struct.MsgStruct) (*sdk_struct.MsgStruct, error) {
if text == "" {
return nil, errors.New("text can not be empty")
}
if len(userIDList) > 10 {
return nil, sdkerrs.ErrArgs
}
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.AtText)
if err != nil {
return nil, err
}
//Avoid nested references
if qs != nil {
if qs.ContentType == constant.Quote {
qs.ContentType = constant.Text
qs.TextElem = &sdk_struct.TextElem{Content: qs.QuoteElem.Text}
}
}
s.AtTextElem = &sdk_struct.AtTextElem{
Text: text,
AtUserList: userIDList,
AtUsersInfo: usersInfo,
QuoteMessage: qs,
}
return &s, nil
}
func (c *Conversation) CreateLocationMessage(ctx context.Context, description string, longitude, latitude float64) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Location)
if err != nil {
return nil, err
}
s.LocationElem = &sdk_struct.LocationElem{
Description: description,
Longitude: longitude,
Latitude: latitude,
}
return &s, nil
}
func (c *Conversation) CreateCustomMessage(ctx context.Context, data, extension string, description string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Custom)
if err != nil {
return nil, err
}
s.CustomElem = &sdk_struct.CustomElem{
Data: data,
Extension: extension,
Description: description,
}
return &s, nil
}
func (c *Conversation) CreateQuoteMessage(ctx context.Context, text string, qs *sdk_struct.MsgStruct) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Quote)
if err != nil {
return nil, err
}
//Avoid nested references
if qs.ContentType == constant.Quote {
qs.ContentType = constant.Text
qs.TextElem = &sdk_struct.TextElem{Content: qs.QuoteElem.Text}
}
s.QuoteElem = &sdk_struct.QuoteElem{
Text: text,
QuoteMessage: qs,
}
return &s, nil
}
func (c *Conversation) CreateAdvancedQuoteMessage(ctx context.Context, text string, qs *sdk_struct.MsgStruct, messageEntities []*sdk_struct.MessageEntity) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Quote)
if err != nil {
return nil, err
}
//Avoid nested references
if qs.ContentType == constant.Quote {
//qs.Content = qs.QuoteElem.Text
qs.ContentType = constant.Text
qs.TextElem = &sdk_struct.TextElem{Content: qs.QuoteElem.Text}
}
s.QuoteElem = &sdk_struct.QuoteElem{
Text: text,
QuoteMessage: qs,
MessageEntityList: messageEntities,
}
return &s, nil
}
func (c *Conversation) CreateCardMessage(ctx context.Context, card *sdk_struct.CardElem) (*sdk_struct.MsgStruct,
error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Card)
if err != nil {
return nil, err
}
s.CardElem = card
return &s, nil
}
func (c *Conversation) CreateVideoMessageFromFullPath(ctx context.Context, videoFullPath string, videoType string,
duration int64, snapshotFullPath string) (*sdk_struct.MsgStruct, error) {
dstFile := utils.FileTmpPath(videoFullPath, c.DataDir) //a->b
written, err := utils.CopyFile(videoFullPath, dstFile)
if err != nil {
//log.Error("internal", "open file failed: ", err, videoFullPath)
return nil, err
}
log.ZDebug(ctx, "videoFullPath dstFile", "videoFullPath", videoFullPath,
"dstFile", dstFile, "written", written)
dstFile = utils.FileTmpPath(snapshotFullPath, c.DataDir) //a->b
sWritten, err := utils.CopyFile(snapshotFullPath, dstFile)
if err != nil {
//log.Error("internal", "open file failed: ", err, snapshotFullPath)
return nil, err
}
log.ZDebug(ctx, "snapshotFullPath dstFile", "snapshotFullPath", snapshotFullPath,
"dstFile", dstFile, "sWritten", sWritten)
s := sdk_struct.MsgStruct{}
err = c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Video)
if err != nil {
return nil, err
}
s.VideoElem = &sdk_struct.VideoElem{
VideoPath: videoFullPath,
VideoType: videoType,
Duration: duration,
}
if snapshotFullPath == "" {
s.VideoElem.SnapshotPath = ""
} else {
s.VideoElem.SnapshotPath = snapshotFullPath
}
fi, err := os.Stat(s.VideoElem.VideoPath)
if err != nil {
//log.Error("internal", "get file Attributes error", err.Error())
return nil, err
}
s.VideoElem.VideoSize = fi.Size()
if snapshotFullPath != "" {
imageInfo, err := getImageInfo(s.VideoElem.SnapshotPath)
if err != nil {
log.ZError(ctx, "getImageInfo err:", err, "snapshotFullPath", snapshotFullPath)
return nil, err
}
s.VideoElem.SnapshotHeight = imageInfo.Height
s.VideoElem.SnapshotWidth = imageInfo.Width
s.VideoElem.SnapshotSize = imageInfo.Size
}
return &s, nil
}
func (c *Conversation) CreateFileMessageFromFullPath(ctx context.Context, fileFullPath string, fileName string) (*sdk_struct.MsgStruct, error) {
dstFile := utils.FileTmpPath(fileFullPath, c.DataDir)
_, err := utils.CopyFile(fileFullPath, dstFile)
if err != nil {
//log.Error("internal", "open file failed: ", err.Error(), fileFullPath)
return nil, err
}
s := sdk_struct.MsgStruct{}
err = c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.File)
if err != nil {
return nil, err
}
fi, err := os.Stat(fileFullPath)
if err != nil {
//log.Error("internal", "get file Attributes error", err.Error())
return nil, err
}
s.FileElem = &sdk_struct.FileElem{
FilePath: fileFullPath,
FileName: fileName,
FileSize: fi.Size(),
}
return &s, nil
}
func (c *Conversation) CreateImageMessageFromFullPath(ctx context.Context, imageFullPath string) (*sdk_struct.MsgStruct, error) {
dstFile := utils.FileTmpPath(imageFullPath, c.DataDir) //a->b
_, err := utils.CopyFile(imageFullPath, dstFile)
if err != nil {
//log.Error(operationID, "open file failed: ", err, imageFullPath)
return nil, err
}
s := sdk_struct.MsgStruct{}
err = c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Picture)
if err != nil {
return nil, err
}
imageInfo, err := getImageInfo(imageFullPath)
if err != nil {
//log.Error(operationID, "getImageInfo err:", err.Error())
return nil, err
}
s.PictureElem = &sdk_struct.PictureElem{
SourcePath: imageFullPath,
SourcePicture: &sdk_struct.PictureBaseInfo{
Width: imageInfo.Width,
Height: imageInfo.Height,
Type: imageInfo.Type,
},
}
return &s, nil
}
func (c *Conversation) CreateSoundMessageFromFullPath(ctx context.Context, soundPath string, duration int64) (*sdk_struct.MsgStruct, error) {
dstFile := utils.FileTmpPath(soundPath, c.DataDir) //a->b
_, err := utils.CopyFile(soundPath, dstFile)
if err != nil {
//log.Error("internal", "open file failed: ", err, soundPath)
return nil, err
}
s := sdk_struct.MsgStruct{}
err = c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Sound)
if err != nil {
return nil, err
}
fi, err := os.Stat(soundPath)
if err != nil {
//log.Error("internal", "getSoundInfo err:", err.Error(), s.SoundElem.SoundPath)
return nil, err
}
s.SoundElem = &sdk_struct.SoundElem{
SoundPath: soundPath,
Duration: duration,
DataSize: fi.Size(),
SoundType: strings.Replace(filepath.Ext(fi.Name()), ".", "", 1),
}
return &s, nil
}
func (c *Conversation) CreateImageMessage(ctx context.Context, imagePath string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Picture)
if err != nil {
return nil, err
}
path := c.DataDir + imagePath
//path := imagePath
imageInfo, err := getImageInfo(path)
if err != nil {
//log.Error("internal", "get imageInfo err", err.Error())
return nil, err
}
s.PictureElem = &sdk_struct.PictureElem{
SourcePath: path,
SourcePicture: &sdk_struct.PictureBaseInfo{
Width: imageInfo.Width,
Height: imageInfo.Height,
Type: imageInfo.Type,
},
}
return &s, nil
}
func (c *Conversation) CreateImageMessageByURL(ctx context.Context, sourcePath string, sourcePicture, bigPicture, snapshotPicture sdk_struct.PictureBaseInfo) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Picture)
if err != nil {
return nil, err
}
s.PictureElem = &sdk_struct.PictureElem{
SourcePath: sourcePath,
SourcePicture: &sourcePicture,
BigPicture: &bigPicture,
SnapshotPicture: &snapshotPicture,
}
return &s, nil
}
func (c *Conversation) CreateSoundMessageByURL(ctx context.Context, soundElem *sdk_struct.SoundBaseInfo) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Sound)
if err != nil {
return nil, err
}
s.SoundElem = &sdk_struct.SoundElem{
UUID: soundElem.UUID,
SoundPath: soundElem.SoundPath,
SourceURL: soundElem.SourceURL,
DataSize: soundElem.DataSize,
Duration: soundElem.Duration,
SoundType: soundElem.SoundType,
}
return &s, nil
}
func (c *Conversation) CreateSoundMessage(ctx context.Context, soundPath string, duration int64) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Sound)
if err != nil {
return nil, err
}
path := c.DataDir + soundPath
fi, err := os.Stat(path)
if err != nil {
//log.Error("internal", "get sound info err", err.Error())
return nil, err
}
s.SoundElem = &sdk_struct.SoundElem{
SoundPath: path,
Duration: duration,
DataSize: fi.Size(),
}
if typ := strings.Replace(filepath.Ext(fi.Name()), ".", "", 1); typ != "" {
s.SoundElem.SoundType = "audio/" + strings.ToLower(typ)
}
return &s, nil
}
func (c *Conversation) CreateVideoMessageByURL(ctx context.Context, videoElem sdk_struct.VideoBaseInfo) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Video)
if err != nil {
return nil, err
}
s.VideoElem = &sdk_struct.VideoElem{
VideoPath: videoElem.VideoPath,
VideoUUID: videoElem.VideoUUID,
VideoURL: videoElem.VideoURL,
VideoType: videoElem.VideoType,
VideoSize: videoElem.VideoSize,
Duration: videoElem.Duration,
SnapshotPath: videoElem.SnapshotPath,
SnapshotUUID: videoElem.SnapshotUUID,
SnapshotSize: videoElem.SnapshotSize,
SnapshotURL: videoElem.SnapshotURL,
SnapshotWidth: videoElem.SnapshotWidth,
SnapshotHeight: videoElem.SnapshotHeight,
SnapshotType: videoElem.SnapshotType,
}
return &s, nil
}
func (c *Conversation) CreateVideoMessage(ctx context.Context, videoPath string, videoType string, duration int64, snapshotPath string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Video)
if err != nil {
return nil, err
}
s.VideoElem = &sdk_struct.VideoElem{}
s.VideoElem.VideoPath = c.DataDir + videoPath
s.VideoElem.VideoType = videoType
s.VideoElem.Duration = duration
if snapshotPath == "" {
s.VideoElem.SnapshotPath = ""
} else {
s.VideoElem.SnapshotPath = c.DataDir + snapshotPath
}
fi, err := os.Stat(s.VideoElem.VideoPath)
if err != nil {
log.ZDebug(ctx, "get video file error", "videoPath", videoPath, "snapshotPath", snapshotPath)
return nil, err
}
s.VideoElem.VideoSize = fi.Size()
if snapshotPath != "" {
imageInfo, err := getImageInfo(s.VideoElem.SnapshotPath)
if err != nil {
//log.Error("internal", "get snapshot info ", err.Error())
return nil, err
}
s.VideoElem.SnapshotHeight = imageInfo.Height
s.VideoElem.SnapshotWidth = imageInfo.Width
s.VideoElem.SnapshotSize = imageInfo.Size
}
return &s, nil
}
func (c *Conversation) CreateFileMessageByURL(ctx context.Context, fileElem sdk_struct.FileBaseInfo) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.File)
if err != nil {
return nil, err
}
s.FileElem = &sdk_struct.FileElem{
FilePath: fileElem.FilePath,
UUID: fileElem.UUID,
SourceURL: fileElem.SourceURL,
FileName: fileElem.FileName,
FileSize: fileElem.FileSize,
FileType: fileElem.FileType,
}
return &s, nil
}
func (c *Conversation) CreateFileMessage(ctx context.Context, filePath string, fileName string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{FileElem: &sdk_struct.FileElem{}}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.File)
if err != nil {
return nil, err
}
s.FileElem.FilePath = c.DataDir + filePath
s.FileElem.FileName = fileName
fi, err := os.Stat(s.FileElem.FilePath)
if err != nil {
//log.Error("internal", "get file message err", err.Error())
return nil, err
}
s.FileElem.FileSize = fi.Size()
s.Content = utils.StructToJsonString(s.FileElem)
return &s, nil
}
func (c *Conversation) CreateMergerMessage(ctx context.Context, messages []*sdk_struct.MsgStruct, title string, summaries []string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{MergeElem: &sdk_struct.MergeElem{}}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Merger)
if err != nil {
return nil, err
}
s.MergeElem.AbstractList = summaries
s.MergeElem.Title = title
s.MergeElem.MultiMessage = messages
s.Content = utils.StructToJsonString(s.MergeElem)
return &s, nil
}
func (c *Conversation) CreateFaceMessage(ctx context.Context, index int, data string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{FaceElem: &sdk_struct.FaceElem{}}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Face)
if err != nil {
return nil, err
}
s.FaceElem.Data = data
s.FaceElem.Index = index
s.Content = utils.StructToJsonString(s.FaceElem)
return &s, nil
}
func (c *Conversation) CreateForwardMessage(ctx context.Context, s *sdk_struct.MsgStruct) (*sdk_struct.MsgStruct, error) {
if s.Status != constant.MsgStatusSendSuccess {
log.ZError(ctx, "only send success message can be Forward",
errors.New("only send success message can be Forward"))
return nil, errors.New("only send success message can be Forward")
}
err := c.initBasicInfo(ctx, s, constant.UserMsgType, s.ContentType)
if err != nil {
return nil, err
}
//Forward message seq is set to 0
s.Seq = 0
s.Status = constant.MsgStatusSendSuccess
return s, nil
}

View File

@@ -0,0 +1,245 @@
// Copyright © 2023 OpenIM SDK. 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 conversation_msg
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/jinzhu/copier"
pbMsg "github.com/openimsdk/protocol/msg"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
// Delete the local and server
// Delete the local, do not change the server data
// To delete the server, you need to change the local message status to delete
func (c *Conversation) clearConversationFromLocalAndSvr(ctx context.Context, conversationID string, f func(ctx context.Context, conversationID string) error) error {
_, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
return err
}
// Use conversationID to remove conversations and messages from the server first
err = c.clearConversationMsgFromSvr(ctx, conversationID)
if err != nil {
return err
}
if err := c.clearConversationAndDeleteAllMsg(ctx, conversationID, false, f); err != nil {
return err
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: []string{conversationID}}})
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}})
return nil
}
func (c *Conversation) clearConversationAndDeleteAllMsg(ctx context.Context, conversationID string, markDelete bool, f func(ctx context.Context, conversationID string) error) error {
err := c.getConversationMaxSeqAndSetHasRead(ctx, conversationID)
if err != nil {
return err
}
if markDelete {
err = c.db.MarkDeleteConversationAllMessages(ctx, conversationID)
} else {
err = c.db.DeleteConversationAllMessages(ctx, conversationID)
}
if err != nil {
return err
}
log.ZDebug(ctx, "reset conversation", "conversationID", conversationID)
err = f(ctx, conversationID)
if err != nil {
return err
}
return nil
}
// To delete session information, delete the server first, and then invoke the interface.
// The client receives a callback to delete all local information.
func (c *Conversation) clearConversationMsgFromSvr(ctx context.Context, conversationID string) error {
var apiReq pbMsg.ClearConversationsMsgReq
apiReq.UserID = c.loginUserID
apiReq.ConversationIDs = []string{conversationID}
return util.ApiPost(ctx, constant.ClearConversationMsgRouter, &apiReq, nil)
}
// Delete all messages
func (c *Conversation) deleteAllMsgFromLocalAndSvr(ctx context.Context) error {
// Delete the server first (high error rate), then delete it.
err := c.deleteAllMessageFromSvr(ctx)
if err != nil {
return err
}
err = c.deleteAllMsgFromLocal(ctx, false)
if err != nil {
return err
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}})
return nil
}
// Delete all server messages
func (c *Conversation) deleteAllMessageFromSvr(ctx context.Context) error {
var apiReq pbMsg.UserClearAllMsgReq
apiReq.UserID = c.loginUserID
err := util.ApiPost(ctx, constant.ClearAllMsgRouter, &apiReq, nil)
if err != nil {
return err
}
return nil
}
// Delete all messages from the local
func (c *Conversation) deleteAllMsgFromLocal(ctx context.Context, markDelete bool) error {
conversations, err := c.db.GetAllConversationListDB(ctx)
if err != nil {
return err
}
var successCids []string
log.ZDebug(ctx, "deleteAllMsgFromLocal", "conversations", conversations, "markDelete", markDelete)
for _, v := range conversations {
if err := c.clearConversationAndDeleteAllMsg(ctx, v.ConversationID, markDelete, c.db.ClearConversation); err != nil {
log.ZError(ctx, "clearConversation err", err, "conversationID", v.ConversationID)
continue
}
successCids = append(successCids, v.ConversationID)
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: successCids}})
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}})
return nil
}
// Delete a message from the local
func (c *Conversation) deleteMessage(ctx context.Context, conversationID string, clientMsgID string) error {
if err := c.deleteMessageFromSvr(ctx, conversationID, clientMsgID); err != nil {
return err
}
return c.deleteMessageFromLocal(ctx, conversationID, clientMsgID)
}
// The user deletes part of the message from the server
func (c *Conversation) deleteMessageFromSvr(ctx context.Context, conversationID string, clientMsgID string) error {
_, err := c.db.GetMessage(ctx, conversationID, clientMsgID)
if err != nil {
return err
}
localMessage, err := c.db.GetMessage(ctx, conversationID, clientMsgID)
if err != nil {
return err
}
if localMessage.Status == constant.MsgStatusSendFailed {
log.ZInfo(ctx, "delete msg status is send failed, do not need delete", "msg", localMessage)
return nil
}
if localMessage.Seq == 0 {
log.ZInfo(ctx, "delete msg seq is 0, try again", "msg", localMessage)
return sdkerrs.ErrMsgHasNoSeq
}
var apiReq pbMsg.DeleteMsgsReq
apiReq.UserID = c.loginUserID
apiReq.Seqs = []int64{localMessage.Seq}
apiReq.ConversationID = conversationID
return util.ApiPost(ctx, constant.DeleteMsgsRouter, &apiReq, nil)
}
// Delete messages from local
func (c *Conversation) deleteMessageFromLocal(ctx context.Context, conversationID string, clientMsgID string) error {
s, err := c.db.GetMessage(ctx, conversationID, clientMsgID)
if err != nil {
return err
}
if err := c.db.DeleteConversationMsgs(ctx, conversationID, []string{clientMsgID}); err != nil {
return err
}
if !s.IsRead && s.SendID != c.loginUserID {
if err := c.db.DecrConversationUnreadCount(ctx, conversationID, 1); err != nil {
return err
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: conversationID, Action: constant.ConChange, Args: []string{conversationID}}})
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}})
}
conversation, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
return err
}
var latestMsg sdk_struct.MsgStruct
utils.JsonStringToStruct(conversation.LatestMsg, &latestMsg)
if latestMsg.ClientMsgID == clientMsgID {
log.ZDebug(ctx, "latesetMsg deleted", "seq", latestMsg.Seq, "clientMsgID", latestMsg.ClientMsgID)
msgs, err := c.db.GetMessageListNoTime(ctx, conversationID, 1, false)
if err != nil {
return err
}
latestMsgSendTime := latestMsg.SendTime
latestMsgStr := ""
if len(msgs) > 0 {
copier.Copy(&latestMsg, msgs[0])
err := c.msgConvert(&latestMsg)
if err != nil {
log.ZError(ctx, "parsing data error", err, latestMsg)
}
latestMsgStr = utils.StructToJsonString(latestMsg)
latestMsgSendTime = latestMsg.SendTime
}
if err := c.db.UpdateColumnsConversation(ctx, conversationID, map[string]interface{}{"latest_msg": latestMsgStr, "latest_msg_send_time": latestMsgSendTime}); err != nil {
return err
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: []string{conversationID}}})
}
c.msgListener().OnMsgDeleted(utils.StructToJsonString(s))
return nil
}
func (c *Conversation) doDeleteMsgs(ctx context.Context, msg *sdkws.MsgData) {
tips := sdkws.DeleteMsgsTips{}
utils.UnmarshalNotificationElem(msg.Content, &tips)
log.ZDebug(ctx, "doDeleteMsgs", "seqs", tips.Seqs)
for _, v := range tips.Seqs {
msg, err := c.db.GetMessageBySeq(ctx, tips.ConversationID, v)
if err != nil {
log.ZError(ctx, "GetMessageBySeq err", err, "conversationID", tips.ConversationID, "seq", v)
continue
}
var s sdk_struct.MsgStruct
copier.Copy(&s, msg)
err = c.msgConvert(&s)
if err != nil {
log.ZError(ctx, "parsing data error", err, "msg", msg)
}
if err := c.deleteMessageFromLocal(ctx, tips.ConversationID, msg.ClientMsgID); err != nil {
log.ZError(ctx, "deleteMessageFromLocal err", err, "conversationID", tips.ConversationID, "seq", v)
}
}
}
func (c *Conversation) doClearConversations(ctx context.Context, msg *sdkws.MsgData) {
tips := sdkws.ClearConversationTips{}
utils.UnmarshalNotificationElem(msg.Content, &tips)
log.ZDebug(ctx, "doClearConversations", "tips", tips)
for _, v := range tips.ConversationIDs {
if err := c.clearConversationAndDeleteAllMsg(ctx, v, false, c.db.ClearConversation); err != nil {
log.ZError(ctx, "clearConversation err", err, "conversationID", v)
}
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: tips.ConversationIDs}})
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}})
}

View File

@@ -0,0 +1,217 @@
package conversation_msg
import (
"context"
"encoding/json"
"github.com/jinzhu/copier"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
"github.com/patrickmn/go-cache"
"time"
)
const (
_ int = iota
stateCodeSuccess
stateCodeEnd
)
const (
inputStatesSendTime = time.Second * 10 // input status sending interval time
inputStatesTimeout = inputStatesSendTime + inputStatesSendTime/2 // input status timeout
inputStatesMsgTimeout = inputStatesSendTime / 2 // message sending timeout
)
func newTyping(c *Conversation) *typing {
e := &typing{
conv: c,
send: cache.New(inputStatesSendTime, inputStatesTimeout),
state: cache.New(inputStatesTimeout, inputStatesTimeout),
}
e.platformIDs = make([]int32, 0, len(constant.PlatformID2Name))
e.platformIDSet = make(map[int32]struct{})
for id := range constant.PlatformID2Name {
e.platformIDSet[int32(id)] = struct{}{}
e.platformIDs = append(e.platformIDs, int32(id))
}
datautil.Sort(e.platformIDs, true)
e.state.OnEvicted(func(key string, val interface{}) {
var data inputStatesKey
if err := json.Unmarshal([]byte(key), &data); err != nil {
return
}
e.changes(data.ConversationID, data.UserID)
})
return e
}
type typing struct {
send *cache.Cache
state *cache.Cache
conv *Conversation
platformIDs []int32
platformIDSet map[int32]struct{}
}
func (e *typing) ChangeInputStates(ctx context.Context, conversationID string, focus bool) error {
if conversationID == "" {
return errs.ErrArgs.WrapMsg("conversationID can't be empty")
}
conversation, err := e.conv.db.GetConversation(ctx, conversationID)
if err != nil {
return err
}
key := conversation.ConversationID
if focus {
if val, ok := e.send.Get(key); ok {
if val.(int) == stateCodeSuccess {
log.ZDebug(ctx, "typing stateCodeSuccess", "conversationID", conversationID, "focus", focus)
return nil
}
}
e.send.SetDefault(key, stateCodeSuccess)
} else {
if val, ok := e.send.Get(key); ok {
if val.(int) == stateCodeEnd {
log.ZDebug(ctx, "typing stateCodeEnd", "conversationID", conversationID, "focus", focus)
return nil
}
e.send.SetDefault(key, stateCodeEnd)
} else {
log.ZDebug(ctx, "typing send not found", "conversationID", conversationID, "focus", focus)
return nil
}
}
ctx, cancel := context.WithTimeout(ctx, inputStatesMsgTimeout)
defer cancel()
if err := e.sendMsg(ctx, conversation, focus); err != nil {
e.send.Delete(key)
return err
}
return nil
}
func (e *typing) sendMsg(ctx context.Context, conversation *model_struct.LocalConversation, focus bool) error {
s := sdk_struct.MsgStruct{}
err := e.conv.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Typing)
if err != nil {
return err
}
s.RecvID = conversation.UserID
s.GroupID = conversation.GroupID
s.SessionType = conversation.ConversationType
var typingElem sdk_struct.TypingElem
if focus {
typingElem.MsgTips = "yes"
} else {
typingElem.MsgTips = "no"
}
s.Content = utils.StructToJsonString(typingElem)
options := make(map[string]bool, 6)
utils.SetSwitchFromOptions(options, constant.IsHistory, false)
utils.SetSwitchFromOptions(options, constant.IsPersistent, false)
utils.SetSwitchFromOptions(options, constant.IsSenderSync, false)
utils.SetSwitchFromOptions(options, constant.IsConversationUpdate, false)
utils.SetSwitchFromOptions(options, constant.IsSenderConversationUpdate, false)
utils.SetSwitchFromOptions(options, constant.IsUnreadCount, false)
utils.SetSwitchFromOptions(options, constant.IsOfflinePush, false)
var wsMsgData sdkws.MsgData
copier.Copy(&wsMsgData, s)
wsMsgData.Content = []byte(s.Content)
wsMsgData.CreateTime = s.CreateTime
wsMsgData.Options = options
var sendMsgResp sdkws.UserSendMsgResp
err = e.conv.LongConnMgr.SendReqWaitResp(ctx, &wsMsgData, constant.SendMsg, &sendMsgResp)
if err != nil {
log.ZError(ctx, "typing msg to server failed", err, "message", s)
return err
}
return nil
}
type inputStatesKey struct {
ConversationID string `json:"cid,omitempty"`
UserID string `json:"uid,omitempty"`
PlatformID int32 `json:"pid,omitempty"`
}
func (e *typing) getStateKey(conversationID string, userID string, platformID int32) string {
data, err := json.Marshal(inputStatesKey{ConversationID: conversationID, UserID: userID, PlatformID: platformID})
if err != nil {
panic(err)
}
return string(data)
}
func (e *typing) onNewMsg(ctx context.Context, msg *sdkws.MsgData) {
var enteringElem sdk_struct.TypingElem
if err := json.Unmarshal(msg.Content, &enteringElem); err != nil {
log.ZError(ctx, "typing onNewMsg Unmarshal failed", err, "message", msg)
return
}
if msg.SendID == e.conv.loginUserID {
return
}
if _, ok := e.platformIDSet[msg.SenderPlatformID]; !ok {
return
}
now := time.Now().UnixMilli()
expirationTimestamp := msg.SendTime + int64(inputStatesSendTime/time.Millisecond)
if msg.SendTime > now || expirationTimestamp <= now {
return
}
var sourceID string
if msg.GroupID == "" {
sourceID = msg.SendID
} else {
sourceID = msg.GroupID
}
conversationID := e.conv.getConversationIDBySessionType(sourceID, int(msg.SessionType))
key := e.getStateKey(conversationID, msg.SendID, msg.SenderPlatformID)
if enteringElem.MsgTips == "yes" {
d := time.Duration(expirationTimestamp-now) * time.Millisecond
if v, t, ok := e.state.GetWithExpiration(key); ok {
if t.UnixMilli() >= expirationTimestamp {
return
}
e.state.Set(key, v, d)
} else {
e.state.Set(key, struct{}{}, d)
e.changes(conversationID, msg.SendID)
}
} else {
if _, ok := e.state.Get(key); ok {
e.state.Delete(key)
}
}
}
type InputStatesChangedData struct {
ConversationID string `json:"conversationID"`
UserID string `json:"userID"`
PlatformIDs []int32 `json:"platformIDs"`
}
func (e *typing) changes(conversationID string, userID string) {
data := InputStatesChangedData{ConversationID: conversationID, UserID: userID, PlatformIDs: e.GetInputStates(conversationID, userID)}
e.conv.ConversationListener().OnConversationUserInputStatusChanged(utils.StructToJsonString(data))
}
func (e *typing) GetInputStates(conversationID string, userID string) []int32 {
platformIDs := make([]int32, 0, 1)
for _, platformID := range e.platformIDs {
key := e.getStateKey(conversationID, userID, platformID)
if _, ok := e.state.Get(key); ok {
platformIDs = append(platformIDs, platformID)
}
}
return platformIDs
}

View File

@@ -0,0 +1,32 @@
package conversation_msg
import (
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/tools/errs"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"os"
)
func getImageInfo(filePath string) (*sdk_struct.ImageInfo, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, errs.WrapMsg(err, "image file open err")
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return nil, err
}
img, format, err := image.Decode(file)
if err != nil {
return nil, errs.WrapMsg(err, "image file decode err")
}
size := img.Bounds().Max
return &sdk_struct.ImageInfo{Width: int32(size.X), Height: int32(size.Y), Type: "image/" + format, Size: info.Size()}, nil
}

View File

@@ -0,0 +1,52 @@
// Copyright © 2023 OpenIM SDK. 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 conversation_msg
import "sync"
type MaxSeqRecorder struct {
seqs map[string]int64
lock sync.RWMutex
}
func NewMaxSeqRecorder() MaxSeqRecorder {
m := make(map[string]int64)
return MaxSeqRecorder{seqs: m}
}
func (m *MaxSeqRecorder) Get(conversationID string) int64 {
m.lock.RLock()
defer m.lock.RUnlock()
return m.seqs[conversationID]
}
func (m *MaxSeqRecorder) Set(conversationID string, seq int64) {
m.lock.Lock()
defer m.lock.Unlock()
m.seqs[conversationID] = seq
}
func (m *MaxSeqRecorder) Incr(conversationID string, num int64) {
m.lock.Lock()
defer m.lock.Unlock()
m.seqs[conversationID] += num
}
func (m *MaxSeqRecorder) IsNewMsg(conversationID string, seq int64) bool {
m.lock.RLock()
defer m.lock.RUnlock()
currentSeq := m.seqs[conversationID]
return seq > currentSeq
}

View File

@@ -0,0 +1,347 @@
// Copyright © 2023 OpenIM SDK. 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 conversation_msg
import (
"context"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
sdk "github.com/openimsdk/openim-sdk-core/v3/pkg/sdk_params_callback"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/protocol/sdkws"
)
// 检测其内部连续性,如果不连续,则向前补齐,获取这一组消息的最大最小seq以及需要补齐的seq列表长度
func (c *Conversation) messageBlocksInternalContinuityCheck(ctx context.Context, conversationID string, notStartTime, isReverse bool, count int,
startTime int64, list *[]*model_struct.LocalChatLog, messageListCallback *sdk.GetAdvancedHistoryMessageListCallback) (max, min int64, length int) {
var lostSeqListLength int
maxSeq, minSeq, haveSeqList := c.getMaxAndMinHaveSeqList(*list)
log.ZDebug(ctx, "getMaxAndMinHaveSeqList is:", "maxSeq", maxSeq, "minSeq", minSeq, "haveSeqList", haveSeqList)
if maxSeq != 0 && minSeq != 0 {
successiveSeqList := func(max, min int64) (seqList []int64) {
for i := min; i <= max; i++ {
seqList = append(seqList, i)
}
return seqList
}(maxSeq, minSeq)
lostSeqList := utils.DifferenceSubset(successiveSeqList, haveSeqList)
lostSeqListLength = len(lostSeqList)
log.ZDebug(ctx, "get lost seqList is :", "maxSeq", maxSeq, "minSeq", minSeq, "lostSeqList", lostSeqList, "length:", lostSeqListLength)
if lostSeqListLength > 0 {
var pullSeqList []int64
if lostSeqListLength <= constant.PullMsgNumForReadDiffusion {
pullSeqList = lostSeqList
} else {
pullSeqList = lostSeqList[lostSeqListLength-constant.PullMsgNumForReadDiffusion : lostSeqListLength]
}
c.pullMessageAndReGetHistoryMessages(ctx, conversationID, pullSeqList, notStartTime, isReverse, count, startTime, list, messageListCallback)
}
}
return maxSeq, minSeq, lostSeqListLength
}
// 检测消息块之间的连续性,如果不连续,则向前补齐,返回块之间是否连续bool
func (c *Conversation) messageBlocksBetweenContinuityCheck(ctx context.Context, lastMinSeq, maxSeq int64, conversationID string,
notStartTime, isReverse bool, count int, startTime int64, list *[]*model_struct.LocalChatLog, messageListCallback *sdk.GetAdvancedHistoryMessageListCallback) bool {
if lastMinSeq != 0 {
log.ZDebug(ctx, "get lost LastMinSeq is :", "lastMinSeq", lastMinSeq, "thisMaxSeq", maxSeq)
if maxSeq != 0 {
if maxSeq+1 != lastMinSeq {
startSeq := int64(lastMinSeq) - constant.PullMsgNumForReadDiffusion
if startSeq <= maxSeq {
startSeq = int64(maxSeq) + 1
}
successiveSeqList := func(max, min int64) (seqList []int64) {
for i := min; i <= max; i++ {
seqList = append(seqList, i)
}
return seqList
}(lastMinSeq-1, startSeq)
log.ZDebug(ctx, "get lost successiveSeqList is :", "successiveSeqList", successiveSeqList, "length:", len(successiveSeqList))
if len(successiveSeqList) > 0 {
c.pullMessageAndReGetHistoryMessages(ctx, conversationID, successiveSeqList, notStartTime, isReverse, count, startTime, list, messageListCallback)
}
} else {
return true
}
} else {
return true
}
} else {
return true
}
return false
}
// 根据最小seq向前补齐消息由服务器告诉拉取消息结果是否到底如果网络则向前补齐,获取这一组消息的最大最小seq以及需要补齐的seq列表长度
func (c *Conversation) messageBlocksEndContinuityCheck(ctx context.Context, minSeq int64, conversationID string, notStartTime,
isReverse bool, count int, startTime int64, list *[]*model_struct.LocalChatLog, messageListCallback *sdk.GetAdvancedHistoryMessageListCallback) {
if minSeq != 0 {
seqList := func(seq int64) (seqList []int64) {
startSeq := seq - constant.PullMsgNumForReadDiffusion
if startSeq <= 0 {
startSeq = 1
}
log.ZDebug(ctx, "pull start is ", "start seq", startSeq)
for i := startSeq; i < seq; i++ {
seqList = append(seqList, i)
}
return seqList
}(minSeq)
log.ZDebug(ctx, "pull seqList is ", "seqList", seqList, "len", len(seqList))
if len(seqList) > 0 {
c.pullMessageAndReGetHistoryMessages(ctx, conversationID, seqList, notStartTime, isReverse, count, startTime, list, messageListCallback)
}
} else {
//local don't have messages,本地无消息但是服务器最大消息不为0
seqList := []int64{0, 0}
c.pullMessageAndReGetHistoryMessages(ctx, conversationID, seqList, notStartTime, isReverse, count, startTime, list, messageListCallback)
}
}
func (c *Conversation) getMaxAndMinHaveSeqList(messages []*model_struct.LocalChatLog) (max, min int64, seqList []int64) {
for i := 0; i < len(messages); i++ {
if messages[i].Seq != 0 {
seqList = append(seqList, messages[i].Seq)
}
if messages[i].Seq != 0 && min == 0 && max == 0 {
min = messages[i].Seq
max = messages[i].Seq
}
if messages[i].Seq < min && messages[i].Seq != 0 {
min = messages[i].Seq
}
if messages[i].Seq > max {
max = messages[i].Seq
}
}
return max, min, seqList
}
// 1、保证单次拉取消息量低于sdk单次从服务器拉取量
// 2、块中连续性检测
// 3、块之间连续性检测
func (c *Conversation) pullMessageAndReGetHistoryMessages(ctx context.Context, conversationID string, seqList []int64,
notStartTime, isReverse bool, count int, startTime int64, list *[]*model_struct.LocalChatLog,
messageListCallback *sdk.GetAdvancedHistoryMessageListCallback) {
existedSeqList, err := c.db.GetAlreadyExistSeqList(ctx, conversationID, seqList)
if err != nil {
log.ZError(ctx, "GetAlreadyExistSeqList err", err, "conversationID", conversationID,
"seqList", seqList)
return
}
if len(existedSeqList) == len(seqList) {
log.ZDebug(ctx, "do not pull message", "seqList", seqList, "existedSeqList", existedSeqList)
return
}
newSeqList := utils.DifferenceSubset(seqList, existedSeqList)
if len(newSeqList) == 0 {
log.ZDebug(ctx, "do not pull message", "seqList", seqList, "existedSeqList", existedSeqList,
"newSeqList", newSeqList)
return
}
var pullMsgResp sdkws.PullMessageBySeqsResp
var pullMsgReq sdkws.PullMessageBySeqsReq
pullMsgReq.UserID = c.loginUserID
var seqRange sdkws.SeqRange
seqRange.ConversationID = conversationID
seqRange.Begin = newSeqList[0]
seqRange.End = newSeqList[len(newSeqList)-1]
seqRange.Num = int64(len(newSeqList))
pullMsgReq.SeqRanges = append(pullMsgReq.SeqRanges, &seqRange)
log.ZDebug(ctx, "conversation pull message, ", "req", pullMsgReq)
if notStartTime && !c.LongConnMgr.IsConnected() {
return
}
err = c.SendReqWaitResp(ctx, &pullMsgReq, constant.PullMsgBySeqList, &pullMsgResp)
if err != nil {
errHandle(newSeqList, list, err, messageListCallback)
log.ZDebug(ctx, "pullmsg SendReqWaitResp failed", err, "req")
} else {
log.ZDebug(ctx, "syncMsgFromServerSplit pull msg", "resp", pullMsgResp)
if pullMsgResp.Msgs == nil {
log.ZWarn(ctx, "syncMsgFromServerSplit pull msg is null", errors.New("pull message is null"),
"req", pullMsgReq)
return
}
if v, ok := pullMsgResp.Msgs[conversationID]; ok {
c.pullMessageIntoTable(ctx, pullMsgResp.Msgs, conversationID)
messageListCallback.IsEnd = v.IsEnd
if notStartTime {
*list, err = c.db.GetMessageListNoTime(ctx, conversationID, count, isReverse)
} else {
*list, err = c.db.GetMessageList(ctx, conversationID, count, startTime, isReverse)
}
}
}
}
func errHandle(seqList []int64, list *[]*model_struct.LocalChatLog, err error, messageListCallback *sdk.GetAdvancedHistoryMessageListCallback) {
messageListCallback.ErrCode = 100
messageListCallback.ErrMsg = err.Error()
var result []*model_struct.LocalChatLog
needPullMaxSeq := seqList[len(seqList)-1]
for _, chatLog := range *list {
if chatLog.Seq == 0 || chatLog.Seq > needPullMaxSeq {
temp := chatLog
result = append(result, temp)
} else {
if chatLog.Seq <= needPullMaxSeq {
break
}
}
}
*list = result
}
func (c *Conversation) pullMessageIntoTable(ctx context.Context, pullMsgData map[string]*sdkws.PullMsgs, conversationID string) {
insertMsg := make(map[string][]*model_struct.LocalChatLog, 20)
updateMsg := make(map[string][]*model_struct.LocalChatLog, 30)
var insertMessage, selfInsertMessage, othersInsertMessage []*model_struct.LocalChatLog
var updateMessage []*model_struct.LocalChatLog
var exceptionMsg []*model_struct.LocalErrChatLog
log.ZDebug(ctx, "do Msg come here, len: ", "msg length", len(pullMsgData))
for conversationID, msgs := range pullMsgData {
for _, v := range msgs.Msgs {
log.ZDebug(ctx, "msg detail", "msg", v, "conversationID", conversationID)
msg := c.msgDataToLocalChatLog(v)
//When the message has been marked and deleted by the cloud, it is directly inserted locally without any conversation and message update.
if msg.Status == constant.MsgStatusHasDeleted {
insertMessage = append(insertMessage, msg)
continue
}
msg.Status = constant.MsgStatusSendSuccess
// log.Info(operationID, "new msg, seq, ServerMsgID, ClientMsgID", msg.Seq, msg.ServerMsgID, msg.ClientMsgID)
//De-analyze data
if msg.ClientMsgID == "" {
exceptionMsg = append(exceptionMsg, c.msgDataToLocalErrChatLog(msg))
continue
}
if v.SendID == c.loginUserID { //seq
// Messages sent by myself //if sent through this terminal
m, err := c.db.GetMessage(ctx, conversationID, msg.ClientMsgID)
if err == nil {
log.ZInfo(ctx, "have message", "msg", msg)
if m.Seq == 0 {
updateMessage = append(updateMessage, msg)
} else {
exceptionMsg = append(exceptionMsg, c.msgDataToLocalErrChatLog(msg))
}
} else { // send through other terminal
log.ZInfo(ctx, "sync message", "msg", msg)
selfInsertMessage = append(selfInsertMessage, msg)
}
} else { //Sent by others
if oldMessage, err := c.db.GetMessage(ctx, conversationID, msg.ClientMsgID); err != nil { //Deduplication operation
othersInsertMessage = append(othersInsertMessage, msg)
} else {
if oldMessage.Seq == 0 {
updateMessage = append(updateMessage, msg)
}
}
}
insertMsg[conversationID] = append(insertMessage, c.faceURLAndNicknameHandle(ctx, selfInsertMessage, othersInsertMessage, conversationID)...)
updateMsg[conversationID] = updateMessage
}
//update message
if err6 := c.messageController.BatchUpdateMessageList(ctx, updateMsg); err6 != nil {
log.ZError(ctx, "sync seq normal message err :", err6)
}
b3 := utils.GetCurrentTimestampByMill()
//Normal message storage
_ = c.messageController.BatchInsertMessageList(ctx, insertMsg)
b4 := utils.GetCurrentTimestampByMill()
log.ZDebug(ctx, "BatchInsertMessageListController, ", "cost time", b4-b3)
//Exception message storage
for _, v := range exceptionMsg {
log.ZWarn(ctx, "exceptionMsg show: ", nil, "msg", *v)
}
}
}
// 拉取的消息都需要经过块内部连续性检测以及块和上一块之间的连续性检测不连续则补补齐的过程中如果出现任何异常只给seq从大到小到断层
// 拉取消息不满量获取服务器中该群最大seq以及用户对于此群最小seq本地该群的最小seq如果本地的不为0并且小于等于服务器最小的说明已经到底部
// 如果本地的为0可以理解为初始化的时候数据还未同步或者异常情况如果服务器最大seq-服务器最小seq>=0说明还未到底部否则到底部
func (c *Conversation) faceURLAndNicknameHandle(ctx context.Context, self, others []*model_struct.LocalChatLog, conversationID string) []*model_struct.LocalChatLog {
lc, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
return append(self, others...)
}
switch lc.ConversationType {
case constant.SingleChatType:
c.singleHandle(ctx, self, others, lc)
case constant.SuperGroupChatType:
c.groupHandle(ctx, self, others, lc)
}
return append(self, others...)
}
func (c *Conversation) singleHandle(ctx context.Context, self, others []*model_struct.LocalChatLog, lc *model_struct.LocalConversation) {
userInfo, err := c.db.GetLoginUser(ctx, c.loginUserID)
if err == nil {
for _, chatLog := range self {
chatLog.SenderFaceURL = userInfo.FaceURL
chatLog.SenderNickname = userInfo.Nickname
}
}
if lc.FaceURL != "" && lc.ShowName != "" {
for _, chatLog := range others {
chatLog.SenderFaceURL = lc.FaceURL
chatLog.SenderNickname = lc.ShowName
}
}
}
func (c *Conversation) groupHandle(ctx context.Context, self, others []*model_struct.LocalChatLog, lc *model_struct.LocalConversation) {
allMessage := append(self, others...)
localGroupMemberInfo, err := c.group.GetSpecifiedGroupMembersInfo(ctx, lc.GroupID, datautil.Slice(allMessage, func(e *model_struct.LocalChatLog) string {
return e.SendID
}))
if err != nil {
log.ZError(ctx, "get group member info err", err)
return
}
groupMap := datautil.SliceToMap(localGroupMemberInfo, func(e *model_struct.LocalGroupMember) string {
return e.UserID
})
for _, chatLog := range allMessage {
if g, ok := groupMap[chatLog.SendID]; ok {
if g.FaceURL != "" && g.Nickname != "" {
chatLog.SenderFaceURL = g.FaceURL
chatLog.SenderNickname = g.Nickname
}
}
}
}

View File

@@ -0,0 +1,124 @@
// Copyright © 2023 OpenIM SDK. 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 conversation_msg
import (
"context"
"encoding/json"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
type MessageController struct {
db db_interface.DataBase
ch chan common.Cmd2Value
}
func NewMessageController(db db_interface.DataBase, ch chan common.Cmd2Value) *MessageController {
return &MessageController{db: db, ch: ch}
}
func (m *MessageController) BatchUpdateMessageList(ctx context.Context, updateMsg map[string][]*model_struct.LocalChatLog) error {
if updateMsg == nil {
return nil
}
for conversationID, messages := range updateMsg {
conversation, err := m.db.GetConversation(ctx, conversationID)
if err != nil {
log.ZError(ctx, "GetConversation err", err, "conversationID", conversationID)
continue
}
latestMsg := &sdk_struct.MsgStruct{}
if err := json.Unmarshal([]byte(conversation.LatestMsg), latestMsg); err != nil {
log.ZError(ctx, "Unmarshal err", err, "conversationID",
conversationID, "latestMsg", conversation.LatestMsg)
continue
}
for _, v := range messages {
v1 := new(model_struct.LocalChatLog)
v1.ClientMsgID = v.ClientMsgID
v1.Seq = v.Seq
v1.Status = v.Status
v1.RecvID = v.RecvID
v1.SessionType = v.SessionType
v1.ServerMsgID = v.ServerMsgID
v1.SendTime = v.SendTime
err := m.db.UpdateMessage(ctx, conversationID, v1)
if err != nil {
return utils.Wrap(err, "BatchUpdateMessageList failed")
}
if latestMsg.ClientMsgID == v.ClientMsgID {
latestMsg.ServerMsgID = v.ServerMsgID
latestMsg.Seq = v.Seq
latestMsg.SendTime = v.SendTime
conversation.LatestMsg = utils.StructToJsonString(latestMsg)
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: conversation.ConversationID,
Action: constant.AddConOrUpLatMsg, Args: *conversation}, m.ch)
}
}
}
return nil
}
func (m *MessageController) BatchInsertMessageList(ctx context.Context, insertMsg map[string][]*model_struct.LocalChatLog) error {
if insertMsg == nil {
return nil
}
for conversationID, messages := range insertMsg {
if len(messages) == 0 {
continue
}
err := m.db.BatchInsertMessageList(ctx, conversationID, messages)
if err != nil {
log.ZError(ctx, "insert GetMessage detail err:", err, "conversationID", conversationID, "messages", messages)
for _, v := range messages {
e := m.db.InsertMessage(ctx, conversationID, v)
if e != nil {
log.ZError(ctx, "InsertMessage err", err, "conversationID", conversationID, "message", v)
}
}
}
}
return nil
}
func (c *Conversation) PullMessageBySeqs(ctx context.Context, seqs []*sdkws.SeqRange) (*sdkws.PullMessageBySeqsResp, error) {
return util.CallApi[sdkws.PullMessageBySeqsResp](ctx, constant.PullUserMsgBySeqRouter, sdkws.PullMessageBySeqsReq{UserID: c.loginUserID, SeqRanges: seqs})
}
func (m *MessageController) SearchMessageByContentTypeAndKeyword(ctx context.Context, contentType []int, keywordList []string,
keywordListMatchType int, startTime, endTime int64) (result []*model_struct.LocalChatLog, err error) {
var list []*model_struct.LocalChatLog
conversationIDList, err := m.db.GetAllConversationIDList(ctx)
for _, v := range conversationIDList {
sList, err := m.db.SearchMessageByContentTypeAndKeyword(ctx, contentType, v, keywordList, keywordListMatchType, startTime, endTime)
if err != nil {
// TODO: log.Error(operationID, "search message in group err", err.Error(), v)
continue
}
list = append(list, sList...)
}
return list, nil
}

View File

@@ -0,0 +1,102 @@
// Copyright © 2023 OpenIM SDK. 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 conversation_msg
import (
"context"
"encoding/json"
"github.com/openimsdk/openim-sdk-core/v3/internal/file"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/tools/log"
)
func NewUploadFileCallback(ctx context.Context, progress func(progress int), msg *sdk_struct.MsgStruct, conversationID string, db db_interface.DataBase) file.UploadFileCallback {
if msg.AttachedInfoElem == nil {
msg.AttachedInfoElem = &sdk_struct.AttachedInfoElem{}
}
if msg.AttachedInfoElem.Progress == nil {
msg.AttachedInfoElem.Progress = &sdk_struct.UploadProgress{}
}
return &msgUploadFileCallback{ctx: ctx, progress: progress, msg: msg, db: db, conversationID: conversationID}
}
type msgUploadFileCallback struct {
ctx context.Context
db db_interface.DataBase
msg *sdk_struct.MsgStruct
conversationID string
value int
progress func(progress int)
}
func (c *msgUploadFileCallback) Open(size int64) {
}
func (c *msgUploadFileCallback) PartSize(partSize int64, num int) {
}
func (c *msgUploadFileCallback) HashPartProgress(index int, size int64, partHash string) {
}
func (c *msgUploadFileCallback) HashPartComplete(partsHash string, fileHash string) {
}
func (c *msgUploadFileCallback) UploadID(uploadID string) {
c.msg.AttachedInfoElem.Progress.UploadID = uploadID
data, err := json.Marshal(c.msg.AttachedInfoElem)
if err != nil {
panic(err)
}
if err := c.db.UpdateColumnsMessage(c.ctx, c.conversationID, c.msg.ClientMsgID, map[string]any{"attached_info": string(data)}); err != nil {
log.ZError(c.ctx, "update PutProgress message attached info failed", err)
}
}
func (c *msgUploadFileCallback) UploadPartComplete(index int, partSize int64, partHash string) {
}
func (c *msgUploadFileCallback) UploadComplete(fileSize int64, streamSize int64, storageSize int64) {
c.msg.AttachedInfoElem.Progress.Save = storageSize
c.msg.AttachedInfoElem.Progress.Current = streamSize
c.msg.AttachedInfoElem.Progress.Total = fileSize
data, err := json.Marshal(c.msg.AttachedInfoElem)
if err != nil {
panic(err)
}
if err := c.db.UpdateColumnsMessage(c.ctx, c.conversationID, c.msg.ClientMsgID, map[string]any{"attached_info": string(data)}); err != nil {
log.ZError(c.ctx, "update PutProgress message attached info failed", err)
}
value := int(float64(streamSize) / float64(fileSize) * 100)
if c.value < value {
c.value = value
c.progress(value)
}
}
func (c *msgUploadFileCallback) Complete(size int64, url string, typ int) {
if c.value != 100 {
c.progress(100)
}
c.msg.AttachedInfoElem.Progress = nil
data, err := json.Marshal(c.msg.AttachedInfoElem)
if err != nil {
panic(err)
}
if err := c.db.UpdateColumnsMessage(c.ctx, c.conversationID, c.msg.ClientMsgID, map[string]any{"attached_info": string(data)}); err != nil {
log.ZError(c.ctx, "update PutComplete message attached info failed", err)
}
}

View File

@@ -0,0 +1,281 @@
// Copyright © 2023 OpenIM SDK. 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 conversation_msg
import (
"context"
"encoding/json"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/tools/utils/datautil"
pbMsg "github.com/openimsdk/protocol/msg"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (c *Conversation) markMsgAsRead2Svr(ctx context.Context, conversationID string, seqs []int64) error {
req := &pbMsg.MarkMsgsAsReadReq{UserID: c.loginUserID, ConversationID: conversationID, Seqs: seqs}
return util.ApiPost(ctx, constant.MarkMsgsAsReadRouter, req, nil)
}
func (c *Conversation) markConversationAsReadSvr(ctx context.Context, conversationID string, hasReadSeq int64, seqs []int64) error {
req := &pbMsg.MarkConversationAsReadReq{UserID: c.loginUserID, ConversationID: conversationID, HasReadSeq: hasReadSeq, Seqs: seqs}
return util.ApiPost(ctx, constant.MarkConversationAsRead, req, nil)
}
func (c *Conversation) setConversationHasReadSeq(ctx context.Context, conversationID string, hasReadSeq int64) error {
req := &pbMsg.SetConversationHasReadSeqReq{UserID: c.loginUserID, ConversationID: conversationID, HasReadSeq: hasReadSeq}
return util.ApiPost(ctx, constant.SetConversationHasReadSeq, req, nil)
}
func (c *Conversation) getConversationMaxSeqAndSetHasRead(ctx context.Context, conversationID string) error {
maxSeq, err := c.db.GetConversationNormalMsgSeq(ctx, conversationID)
if err != nil {
return err
}
if maxSeq == 0 {
return nil
}
if err := c.setConversationHasReadSeq(ctx, conversationID, maxSeq); err != nil {
return err
}
if err := c.db.UpdateColumnsConversation(ctx, conversationID, map[string]interface{}{"has_read_seq": maxSeq}); err != nil {
return err
}
return nil
}
// mark a conversation's all message as read
func (c *Conversation) markConversationMessageAsRead(ctx context.Context, conversationID string) error {
conversation, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
return err
}
if conversation.UnreadCount == 0 {
return sdkerrs.ErrUnreadCount
}
// get the maximum sequence number of messages in the table that are not sent by oneself
peerUserMaxSeq, err := c.db.GetConversationPeerNormalMsgSeq(ctx, conversationID)
if err != nil {
return err
}
// get the maximum sequence number of messages in the table
maxSeq, err := c.db.GetConversationNormalMsgSeq(ctx, conversationID)
if err != nil {
return err
}
switch conversation.ConversationType {
case constant.SingleChatType:
msgs, err := c.db.GetUnreadMessage(ctx, conversationID)
if err != nil {
return err
}
log.ZDebug(ctx, "get unread message", "msgs", len(msgs))
msgIDs, seqs := c.getAsReadMsgMapAndList(ctx, msgs)
if len(seqs) == 0 {
log.ZWarn(ctx, "seqs is empty", nil, "conversationID", conversationID)
return nil
}
log.ZDebug(ctx, "markConversationMessageAsRead", "conversationID", conversationID, "seqs",
seqs, "peerUserMaxSeq", peerUserMaxSeq, "maxSeq", maxSeq)
if err := c.markConversationAsReadSvr(ctx, conversationID, maxSeq, seqs); err != nil {
return err
}
_, err = c.db.MarkConversationMessageAsReadDB(ctx, conversationID, msgIDs)
if err != nil {
log.ZWarn(ctx, "MarkConversationMessageAsRead err", err, "conversationID", conversationID, "msgIDs", msgIDs)
}
case constant.SuperGroupChatType, constant.NotificationChatType:
log.ZDebug(ctx, "markConversationMessageAsRead", "conversationID", conversationID, "peerUserMaxSeq", peerUserMaxSeq, "maxSeq", maxSeq)
if err := c.markConversationAsReadSvr(ctx, conversationID, maxSeq, nil); err != nil {
return err
}
}
if err := c.db.UpdateColumnsConversation(ctx, conversationID, map[string]interface{}{"unread_count": 0}); err != nil {
log.ZError(ctx, "UpdateColumnsConversation err", err, "conversationID", conversationID)
}
log.ZDebug(ctx, "update columns sucess")
c.unreadChangeTrigger(ctx, conversationID, peerUserMaxSeq == maxSeq)
return nil
}
// mark a conversation's message as read by seqs
func (c *Conversation) markMessagesAsReadByMsgID(ctx context.Context, conversationID string, msgIDs []string) error {
_, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
return err
}
msgs, err := c.db.GetMessagesByClientMsgIDs(ctx, conversationID, msgIDs)
if err != nil {
return err
}
if len(msgs) == 0 {
return nil
}
var hasReadSeq = msgs[0].Seq
maxSeq, err := c.db.GetConversationNormalMsgSeq(ctx, conversationID)
if err != nil {
return err
}
markAsReadMsgIDs, seqs := c.getAsReadMsgMapAndList(ctx, msgs)
log.ZDebug(ctx, "msgs len", "markAsReadMsgIDs", len(markAsReadMsgIDs), "seqs", seqs)
if len(seqs) == 0 {
log.ZWarn(ctx, "seqs is empty", nil, "conversationID", conversationID)
return nil
}
if err := c.markMsgAsRead2Svr(ctx, conversationID, seqs); err != nil {
return err
}
decrCount, err := c.db.MarkConversationMessageAsReadDB(ctx, conversationID, markAsReadMsgIDs)
if err != nil {
return err
}
if err := c.db.DecrConversationUnreadCount(ctx, conversationID, decrCount); err != nil {
log.ZError(ctx, "decrConversationUnreadCount err", err, "conversationID", conversationID,
"decrCount", decrCount)
}
c.unreadChangeTrigger(ctx, conversationID, hasReadSeq == maxSeq && msgs[0].SendID != c.loginUserID)
return nil
}
func (c *Conversation) getAsReadMsgMapAndList(ctx context.Context,
msgs []*model_struct.LocalChatLog) (asReadMsgIDs []string, seqs []int64) {
for _, msg := range msgs {
if !msg.IsRead && msg.SendID != c.loginUserID {
if msg.Seq == 0 {
log.ZWarn(ctx, "exception seq", errors.New("exception message "), "msg", msg)
} else {
asReadMsgIDs = append(asReadMsgIDs, msg.ClientMsgID)
seqs = append(seqs, msg.Seq)
}
} else {
log.ZWarn(ctx, "msg can't marked as read", nil, "msg", msg)
}
}
return
}
func (c *Conversation) unreadChangeTrigger(ctx context.Context, conversationID string, latestMsgIsRead bool) {
if latestMsgIsRead {
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: conversationID,
Action: constant.UpdateLatestMessageChange, Args: []string{conversationID}}, Ctx: ctx})
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: conversationID,
Action: constant.ConChange, Args: []string{conversationID}}, Ctx: ctx})
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged},
Ctx: ctx})
}
func (c *Conversation) doUnreadCount(ctx context.Context, conversation *model_struct.LocalConversation, hasReadSeq int64, seqs []int64) {
if conversation.ConversationType == constant.SingleChatType {
if len(seqs) != 0 {
_, err := c.db.MarkConversationMessageAsReadBySeqs(ctx, conversation.ConversationID, seqs)
if err != nil {
log.ZWarn(ctx, "MarkConversationMessageAsReadBySeqs err", err, "conversationID", conversation.ConversationID, "seqs", seqs)
}
} else {
log.ZWarn(ctx, "seqs is empty", nil, "conversationID", conversation.ConversationID, "hasReadSeq", hasReadSeq)
}
if hasReadSeq > conversation.HasReadSeq {
decrUnreadCount := hasReadSeq - conversation.HasReadSeq
if err := c.db.DecrConversationUnreadCount(ctx, conversation.ConversationID, decrUnreadCount); err != nil {
log.ZError(ctx, "DecrConversationUnreadCount err", err, "conversationID", conversation.ConversationID, "decrUnreadCount", decrUnreadCount)
}
if err := c.db.UpdateColumnsConversation(ctx, conversation.ConversationID, map[string]interface{}{"has_read_seq": hasReadSeq}); err != nil {
log.ZError(ctx, "UpdateColumnsConversation err", err, "conversationID", conversation.ConversationID)
}
}
latestMsg := &sdk_struct.MsgStruct{}
if err := json.Unmarshal([]byte(conversation.LatestMsg), latestMsg); err != nil {
log.ZError(ctx, "Unmarshal err", err, "conversationID", conversation.ConversationID, "latestMsg", conversation.LatestMsg)
}
if (!latestMsg.IsRead) && datautil.Contain(latestMsg.Seq, seqs...) {
latestMsg.IsRead = true
conversation.LatestMsg = utils.StructToJsonString(&latestMsg)
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: conversation.ConversationID, Action: constant.AddConOrUpLatMsg, Args: *conversation}, c.GetCh())
}
} else {
if err := c.db.UpdateColumnsConversation(ctx, conversation.ConversationID, map[string]interface{}{"unread_count": 0}); err != nil {
log.ZError(ctx, "UpdateColumnsConversation err", err, "conversationID", conversation.ConversationID)
}
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: conversation.ConversationID, Action: constant.ConChange, Args: []string{conversation.ConversationID}}})
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}})
}
func (c *Conversation) doReadDrawing(ctx context.Context, msg *sdkws.MsgData) {
tips := &sdkws.MarkAsReadTips{}
err := utils.UnmarshalNotificationElem(msg.Content, tips)
if err != nil {
log.ZWarn(ctx, "UnmarshalNotificationElem err", err, "msg", msg)
return
}
log.ZDebug(ctx, "do readDrawing", "tips", tips)
conversation, err := c.db.GetConversation(ctx, tips.ConversationID)
if err != nil {
log.ZError(ctx, "GetConversation err", err, "conversationID", tips.ConversationID)
return
}
if tips.MarkAsReadUserID != c.loginUserID {
if len(tips.Seqs) == 0 {
return
}
messages, err := c.db.GetMessagesBySeqs(ctx, tips.ConversationID, tips.Seqs)
if err != nil {
log.ZError(ctx, "GetMessagesBySeqs err", err, "conversationID", tips.ConversationID, "seqs", tips.Seqs)
return
}
if conversation.ConversationType == constant.SingleChatType {
latestMsg := &sdk_struct.MsgStruct{}
if err := json.Unmarshal([]byte(conversation.LatestMsg), latestMsg); err != nil {
log.ZError(ctx, "Unmarshal err", err, "conversationID", tips.ConversationID, "latestMsg", conversation.LatestMsg)
}
var successMsgIDs []string
for _, message := range messages {
attachInfo := sdk_struct.AttachedInfoElem{}
_ = utils.JsonStringToStruct(message.AttachedInfo, &attachInfo)
attachInfo.HasReadTime = msg.SendTime
message.AttachedInfo = utils.StructToJsonString(attachInfo)
message.IsRead = true
if err = c.db.UpdateMessage(ctx, tips.ConversationID, message); err != nil {
log.ZError(ctx, "UpdateMessage err", err, "conversationID", tips.ConversationID, "message", message)
} else {
if latestMsg.ClientMsgID == message.ClientMsgID {
latestMsg.IsRead = message.IsRead
conversation.LatestMsg = utils.StructToJsonString(latestMsg)
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: conversation.ConversationID, Action: constant.AddConOrUpLatMsg, Args: *conversation}, c.GetCh())
}
successMsgIDs = append(successMsgIDs, message.ClientMsgID)
}
}
var messageReceiptResp = []*sdk_struct.MessageReceipt{{UserID: tips.MarkAsReadUserID, MsgIDList: successMsgIDs,
SessionType: conversation.ConversationType, ReadTime: msg.SendTime}}
c.msgListener().OnRecvC2CReadReceipt(utils.StructToJsonString(messageReceiptResp))
}
} else {
c.doUnreadCount(ctx, conversation, tips.HasReadSeq, tips.Seqs)
}
}

View File

@@ -0,0 +1,203 @@
// Copyright © 2023 OpenIM SDK. 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 conversation_msg
import (
"context"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/tools/utils/timeutil"
"github.com/jinzhu/copier"
pbMsg "github.com/openimsdk/protocol/msg"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (c *Conversation) doRevokeMsg(ctx context.Context, msg *sdkws.MsgData) {
var tips sdkws.RevokeMsgTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
log.ZError(ctx, "unmarshal failed", err, "msg", msg)
return
}
log.ZDebug(ctx, "do revokeMessage", "tips", &tips)
c.revokeMessage(ctx, &tips)
}
func (c *Conversation) revokeMessage(ctx context.Context, tips *sdkws.RevokeMsgTips) {
revokedMsg, err := c.db.GetMessageBySeq(ctx, tips.ConversationID, tips.Seq)
if err != nil {
log.ZError(ctx, "GetMessageBySeq failed", err, "tips", &tips)
return
}
var revokerRole int32
var revokerNickname string
if tips.IsAdminRevoke || tips.SesstionType == constant.SingleChatType {
_, userName, err := c.getUserNameAndFaceURL(ctx, tips.RevokerUserID)
if err != nil {
log.ZError(ctx, "GetUserNameAndFaceURL failed", err, "tips", &tips)
} else {
log.ZDebug(ctx, "revoker user name", "userName", userName)
}
revokerNickname = userName
} else if tips.SesstionType == constant.SuperGroupChatType {
conversation, err := c.db.GetConversation(ctx, tips.ConversationID)
if err != nil {
log.ZError(ctx, "GetConversation failed", err, "conversationID", tips.ConversationID)
return
}
groupMember, err := c.db.GetGroupMemberInfoByGroupIDUserID(ctx, conversation.GroupID, tips.RevokerUserID)
if err != nil {
log.ZError(ctx, "GetGroupMemberInfoByGroupIDUserID failed", err, "tips", &tips)
} else {
log.ZDebug(ctx, "revoker member name", "groupMember", groupMember)
revokerRole = groupMember.RoleLevel
revokerNickname = groupMember.Nickname
}
}
m := sdk_struct.MessageRevoked{
RevokerID: tips.RevokerUserID,
RevokerRole: revokerRole,
ClientMsgID: revokedMsg.ClientMsgID,
RevokerNickname: revokerNickname,
RevokeTime: tips.RevokeTime,
SourceMessageSendTime: revokedMsg.SendTime,
SourceMessageSendID: revokedMsg.SendID,
SourceMessageSenderNickname: revokedMsg.SenderNickname,
SessionType: tips.SesstionType,
Seq: tips.Seq,
Ex: revokedMsg.Ex,
IsAdminRevoke: tips.IsAdminRevoke,
}
// log.ZDebug(ctx, "callback revokeMessage", "m", m)
var n sdk_struct.NotificationElem
n.Detail = utils.StructToJsonString(m)
if err := c.db.UpdateMessageBySeq(ctx, tips.ConversationID, &model_struct.LocalChatLog{Seq: tips.Seq,
Content: utils.StructToJsonString(n), ContentType: constant.RevokeNotification}); err != nil {
log.ZError(ctx, "UpdateMessageBySeq failed", err, "tips", &tips)
return
}
conversation, err := c.db.GetConversation(ctx, tips.ConversationID)
if err != nil {
log.ZError(ctx, "GetConversation failed", err, "tips", &tips)
return
}
var latestMsg sdk_struct.MsgStruct
utils.JsonStringToStruct(conversation.LatestMsg, &latestMsg)
log.ZDebug(ctx, "latestMsg", "latestMsg", &latestMsg, "seq", tips.Seq)
if latestMsg.Seq <= tips.Seq {
var newLatesetMsg sdk_struct.MsgStruct
msgs, err := c.db.GetMessageListNoTime(ctx, tips.ConversationID, 1, false)
if err != nil || len(msgs) == 0 {
log.ZError(ctx, "GetMessageListNoTime failed", err, "tips", &tips)
return
}
log.ZDebug(ctx, "latestMsg is revoked", "seq", tips.Seq, "msg", msgs[0])
copier.Copy(&newLatesetMsg, msgs[0])
err = c.msgConvert(&newLatesetMsg)
if err != nil {
log.ZError(ctx, "parsing data error", err, latestMsg)
} else {
log.ZDebug(ctx, "revoke update conversatoin", "msg", utils.StructToJsonString(newLatesetMsg))
if err := c.db.UpdateColumnsConversation(ctx, tips.ConversationID, map[string]interface{}{"latest_msg": utils.StructToJsonString(newLatesetMsg),
"latest_msg_send_time": newLatesetMsg.SendTime}); err != nil {
log.ZError(ctx, "UpdateColumnsConversation failed", err, "newLatesetMsg", newLatesetMsg)
} else {
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: []string{tips.ConversationID}}})
}
}
}
c.msgListener().OnNewRecvMessageRevoked(utils.StructToJsonString(m))
msgList, err := c.db.SearchAllMessageByContentType(ctx, conversation.ConversationID, constant.Quote)
if err != nil {
log.ZError(ctx, "SearchAllMessageByContentType failed", err, "tips", &tips)
return
}
for _, v := range msgList {
c.quoteMsgRevokeHandle(ctx, tips.ConversationID, v, m)
}
}
func (c *Conversation) quoteMsgRevokeHandle(ctx context.Context, conversationID string, v *model_struct.LocalChatLog, revokedMsg sdk_struct.MessageRevoked) {
s := sdk_struct.MsgStruct{}
_ = utils.JsonStringToStruct(v.Content, &s.QuoteElem)
if s.QuoteElem.QuoteMessage == nil {
return
}
if s.QuoteElem.QuoteMessage.ClientMsgID != revokedMsg.ClientMsgID {
return
}
s.QuoteElem.QuoteMessage.Content = utils.StructToJsonString(revokedMsg)
s.QuoteElem.QuoteMessage.ContentType = constant.RevokeNotification
v.Content = utils.StructToJsonString(s.QuoteElem)
if err := c.db.UpdateMessageBySeq(ctx, conversationID, v); err != nil {
log.ZError(ctx, "UpdateMessage failed", err, "v", v)
}
}
func (c *Conversation) revokeOneMessage(ctx context.Context, conversationID, clientMsgID string) error {
conversation, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
return err
}
message, err := c.db.GetMessage(ctx, conversationID, clientMsgID)
if err != nil {
return err
}
if message.Status != constant.MsgStatusSendSuccess {
return errors.New("only send success message can be revoked")
}
switch conversation.ConversationType {
case constant.SingleChatType:
if message.SendID != c.loginUserID {
return errors.New("only send by yourself message can be revoked")
}
case constant.SuperGroupChatType:
if message.SendID != c.loginUserID {
groupAdmins, err := c.db.GetGroupMemberOwnerAndAdminDB(ctx, conversation.GroupID)
if err != nil {
return err
}
var isAdmin bool
for _, member := range groupAdmins {
if member.UserID == c.loginUserID {
isAdmin = true
break
}
}
if !isAdmin {
return errors.New("only group admin can revoke message")
}
}
}
if err := util.ApiPost(ctx, constant.RevokeMsgRouter, pbMsg.RevokeMsgReq{ConversationID: conversationID, Seq: message.Seq, UserID: c.loginUserID}, nil); err != nil {
return err
}
c.revokeMessage(ctx, &sdkws.RevokeMsgTips{
ConversationID: conversationID,
Seq: message.Seq,
RevokerUserID: c.loginUserID,
RevokeTime: timeutil.GetCurrentTimestampBySecond(),
SesstionType: conversation.ConversationType,
ClientMsgID: clientMsgID,
})
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,148 @@
// Copyright © 2023 OpenIM SDK. 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 conversation_msg
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/syncer"
"github.com/openimsdk/tools/utils/datautil"
"time"
"github.com/openimsdk/tools/log"
)
func (c *Conversation) SyncConversationsAndTriggerCallback(ctx context.Context, conversationsOnServer []*model_struct.LocalConversation) error {
conversationsOnLocal, err := c.db.GetAllConversations(ctx)
if err != nil {
return err
}
if err := c.batchAddFaceURLAndName(ctx, conversationsOnServer...); err != nil {
return err
}
if err = c.conversationSyncer.Sync(ctx, conversationsOnServer, conversationsOnLocal, func(ctx context.Context, state int, server, local *model_struct.LocalConversation) error {
if state == syncer.Update || state == syncer.Insert {
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: server.ConversationID, Action: constant.ConChange, Args: []string{server.ConversationID}}})
}
return nil
}, true); err != nil {
return err
}
return nil
}
func (c *Conversation) SyncConversations(ctx context.Context, conversationIDs []string) error {
conversationsOnServer, err := c.getServerConversationsByIDs(ctx, conversationIDs)
if err != nil {
return err
}
return c.SyncConversationsAndTriggerCallback(ctx, conversationsOnServer)
}
func (c *Conversation) SyncAllConversations(ctx context.Context) error {
ccTime := time.Now()
conversationsOnServer, err := c.getServerConversationList(ctx)
if err != nil {
return err
}
log.ZDebug(ctx, "get server cost time", "cost time", time.Since(ccTime), "conversation on server", conversationsOnServer)
return c.SyncConversationsAndTriggerCallback(ctx, conversationsOnServer)
}
func (c *Conversation) SyncAllConversationHashReadSeqs(ctx context.Context) error {
log.ZDebug(ctx, "start SyncConversationHashReadSeqs")
seqs, err := c.getServerHasReadAndMaxSeqs(ctx)
if err != nil {
return err
}
if len(seqs) == 0 {
return nil
}
var conversationChangedIDs []string
var conversationIDsNeedSync []string
conversationsOnLocal, err := c.db.GetAllConversations(ctx)
if err != nil {
log.ZWarn(ctx, "get all conversations err", err)
return err
}
conversationsOnLocalMap := datautil.SliceToMap(conversationsOnLocal, func(e *model_struct.LocalConversation) string {
return e.ConversationID
})
for conversationID, v := range seqs {
var unreadCount int32
c.maxSeqRecorder.Set(conversationID, v.MaxSeq)
if v.MaxSeq-v.HasReadSeq < 0 {
unreadCount = 0
log.ZWarn(ctx, "unread count is less than 0", nil, "conversationID",
conversationID, "maxSeq", v.MaxSeq, "hasReadSeq", v.HasReadSeq)
} else {
unreadCount = int32(v.MaxSeq - v.HasReadSeq)
}
if conversation, ok := conversationsOnLocalMap[conversationID]; ok {
if conversation.UnreadCount != unreadCount || conversation.HasReadSeq != v.HasReadSeq {
if err := c.db.UpdateColumnsConversation(ctx, conversationID, map[string]interface{}{"unread_count": unreadCount, "has_read_seq": v.HasReadSeq}); err != nil {
log.ZWarn(ctx, "UpdateColumnsConversation err", err, "conversationID", conversationID)
continue
}
conversationChangedIDs = append(conversationChangedIDs, conversationID)
}
} else {
conversationIDsNeedSync = append(conversationIDsNeedSync, conversationID)
}
}
if len(conversationIDsNeedSync) > 0 {
conversationsOnServer, err := c.getServerConversationsByIDs(ctx, conversationIDsNeedSync)
if err != nil {
log.ZWarn(ctx, "getServerConversationsByIDs err", err, "conversationIDs", conversationIDsNeedSync)
return err
}
if err := c.batchAddFaceURLAndName(ctx, conversationsOnServer...); err != nil {
log.ZWarn(ctx, "batchAddFaceURLAndName err", err, "conversationsOnServer", conversationsOnServer)
return err
}
for _, conversation := range conversationsOnServer {
var unreadCount int32
v, ok := seqs[conversation.ConversationID]
if !ok {
continue
}
if v.MaxSeq-v.HasReadSeq < 0 {
unreadCount = 0
log.ZWarn(ctx, "unread count is less than 0", nil, "server seq", v, "conversation", conversation)
} else {
unreadCount = int32(v.MaxSeq - v.HasReadSeq)
}
conversation.UnreadCount = unreadCount
conversation.HasReadSeq = v.HasReadSeq
}
err = c.db.BatchInsertConversationList(ctx, conversationsOnServer)
if err != nil {
log.ZWarn(ctx, "BatchInsertConversationList err", err, "conversationsOnServer", conversationsOnServer)
}
}
log.ZDebug(ctx, "update conversations", "conversations", conversationChangedIDs)
if len(conversationChangedIDs) > 0 {
common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{Action: constant.ConChange, Args: conversationChangedIDs}, c.GetCh())
common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}, c.GetCh())
}
return nil
}