// 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" "fmt" "github.com/openimsdk/openim-sdk-core/v3/internal/file" "github.com/openimsdk/openim-sdk-core/v3/open_im_sdk_callback" "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/content_type" "github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct" "github.com/openimsdk/openim-sdk-core/v3/pkg/sdk_params_callback" "github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs" "github.com/openimsdk/openim-sdk-core/v3/pkg/server_api_params" "github.com/openimsdk/openim-sdk-core/v3/pkg/utils" "github.com/openimsdk/openim-sdk-core/v3/sdk_struct" "net/url" "os" "path/filepath" "sort" "strings" "sync" "time" "github.com/openimsdk/tools/log" pbConversation "github.com/openimsdk/protocol/conversation" "github.com/openimsdk/protocol/sdkws" "github.com/openimsdk/protocol/wrapperspb" "github.com/jinzhu/copier" ) func (c *Conversation) GetAllConversationList(ctx context.Context) ([]*model_struct.LocalConversation, error) { return c.db.GetAllConversationListDB(ctx) } func (c *Conversation) GetConversationListSplit(ctx context.Context, offset, count int) ([]*model_struct.LocalConversation, error) { return c.db.GetConversationListSplitDB(ctx, offset, count) } func (c *Conversation) HideConversation(ctx context.Context, conversationID string) error { err := c.db.ResetConversation(ctx, conversationID) if err != nil { return err } return nil } func (c *Conversation) GetAtAllTag(_ context.Context) string { return constant.AtAllString } // deprecated func (c *Conversation) GetConversationRecvMessageOpt(ctx context.Context, conversationIDs []string) (resp []*server_api_params.GetConversationRecvMessageOptResp, err error) { conversations, err := c.db.GetMultipleConversationDB(ctx, conversationIDs) if err != nil { return nil, err } for _, conversation := range conversations { resp = append(resp, &server_api_params.GetConversationRecvMessageOptResp{ ConversationID: conversation.ConversationID, Result: &conversation.RecvMsgOpt, }) } return resp, nil } // Method to set global message receiving options func (c *Conversation) GetOneConversation(ctx context.Context, sessionType int32, sourceID string) (*model_struct.LocalConversation, error) { conversationID := c.getConversationIDBySessionType(sourceID, int(sessionType)) lc, err := c.db.GetConversation(ctx, conversationID) if err == nil { return lc, nil } else { var newConversation model_struct.LocalConversation newConversation.ConversationID = conversationID newConversation.ConversationType = sessionType switch sessionType { case constant.SingleChatType: newConversation.UserID = sourceID faceUrl, name, err := c.getUserNameAndFaceURL(ctx, sourceID) if err != nil { return nil, err } newConversation.ShowName = name newConversation.FaceURL = faceUrl case constant.GroupChatType, constant.SuperGroupChatType: newConversation.GroupID = sourceID g, err := c.full.GetGroupInfoFromLocal2Svr(ctx, sourceID, sessionType) if err != nil { return nil, err } newConversation.ShowName = g.GroupName newConversation.FaceURL = g.FaceURL } time.Sleep(time.Millisecond * 500) lc, errTemp := c.db.GetConversation(ctx, conversationID) if errTemp == nil { return lc, nil } err := c.db.InsertConversation(ctx, &newConversation) if err != nil { return nil, err } return &newConversation, nil } } func (c *Conversation) GetMultipleConversation(ctx context.Context, conversationIDList []string) ([]*model_struct.LocalConversation, error) { conversations, err := c.db.GetMultipleConversationDB(ctx, conversationIDList) if err != nil { return nil, err } return conversations, nil } func (c *Conversation) HideAllConversations(ctx context.Context) error { err := c.db.ResetAllConversation(ctx) if err != nil { return err } return nil } func (c *Conversation) SetConversationDraft(ctx context.Context, conversationID, draftText string) error { if draftText != "" { err := c.db.SetConversationDraftDB(ctx, conversationID, draftText) if err != nil { return err } } else { err := c.db.RemoveConversationDraft(ctx, conversationID, draftText) if err != nil { return err } } _ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{Action: constant.ConChange, Args: []string{conversationID}}, c.GetCh()) return nil } func (c *Conversation) setConversationAndSync(ctx context.Context, conversationID string, req *pbConversation.ConversationReq) error { lc, err := c.db.GetConversation(ctx, conversationID) if err != nil { return err } apiReq := &pbConversation.SetConversationsReq{Conversation: req} err = c.setConversation(ctx, apiReq, lc) if err != nil { return err } c.SyncConversations(ctx, []string{conversationID}) return nil } func (c *Conversation) ResetConversationGroupAtType(ctx context.Context, conversationID string) error { return c.setConversationAndSync(ctx, conversationID, &pbConversation.ConversationReq{GroupAtType: &wrapperspb.Int32Value{Value: 0}}) } func (c *Conversation) PinConversation(ctx context.Context, conversationID string, isPinned bool) error { return c.setConversationAndSync(ctx, conversationID, &pbConversation.ConversationReq{IsPinned: &wrapperspb.BoolValue{Value: isPinned}}) } func (c *Conversation) SetOneConversationPrivateChat(ctx context.Context, conversationID string, isPrivate bool) error { return c.setConversationAndSync(ctx, conversationID, &pbConversation.ConversationReq{IsPrivateChat: &wrapperspb.BoolValue{Value: isPrivate}}) } func (c *Conversation) SetConversationMsgDestructTime(ctx context.Context, conversationID string, msgDestructTime int64) error { return c.setConversationAndSync(ctx, conversationID, &pbConversation.ConversationReq{MsgDestructTime: &wrapperspb.Int64Value{Value: msgDestructTime}}) } func (c *Conversation) SetConversationIsMsgDestruct(ctx context.Context, conversationID string, isMsgDestruct bool) error { return c.setConversationAndSync(ctx, conversationID, &pbConversation.ConversationReq{IsMsgDestruct: &wrapperspb.BoolValue{Value: isMsgDestruct}}) } func (c *Conversation) SetOneConversationBurnDuration(ctx context.Context, conversationID string, burnDuration int32) error { return c.setConversationAndSync(ctx, conversationID, &pbConversation.ConversationReq{BurnDuration: &wrapperspb.Int32Value{Value: burnDuration}}) } func (c *Conversation) SetOneConversationRecvMessageOpt(ctx context.Context, conversationID string, opt int) error { return c.setConversationAndSync(ctx, conversationID, &pbConversation.ConversationReq{RecvMsgOpt: &wrapperspb.Int32Value{Value: int32(opt)}}) } func (c *Conversation) SetOneConversationEx(ctx context.Context, conversationID string, ex string) error { return c.setConversationAndSync(ctx, conversationID, &pbConversation.ConversationReq{Ex: &wrapperspb.StringValue{ Value: ex, }}) } func (c *Conversation) GetTotalUnreadMsgCount(ctx context.Context) (totalUnreadCount int32, err error) { return c.db.GetTotalUnreadMsgCountDB(ctx) } func (c *Conversation) SetConversationListener(listener func() open_im_sdk_callback.OnConversationListener) { c.ConversationListener = listener } func (c *Conversation) msgStructToLocalChatLog(src *sdk_struct.MsgStruct) *model_struct.LocalChatLog { var lc model_struct.LocalChatLog copier.Copy(&lc, src) switch src.ContentType { case constant.Text: lc.Content = utils.StructToJsonString(src.TextElem) case constant.Picture: lc.Content = utils.StructToJsonString(src.PictureElem) case constant.Sound: lc.Content = utils.StructToJsonString(src.SoundElem) case constant.Video: lc.Content = utils.StructToJsonString(src.VideoElem) case constant.File: lc.Content = utils.StructToJsonString(src.FileElem) case constant.AtText: lc.Content = utils.StructToJsonString(src.AtTextElem) case constant.Merger: lc.Content = utils.StructToJsonString(src.MergeElem) case constant.Card: lc.Content = utils.StructToJsonString(src.CardElem) case constant.Location: lc.Content = utils.StructToJsonString(src.LocationElem) case constant.Custom: lc.Content = utils.StructToJsonString(src.CustomElem) case constant.Quote: lc.Content = utils.StructToJsonString(src.QuoteElem) case constant.Face: lc.Content = utils.StructToJsonString(src.FaceElem) case constant.AdvancedText: lc.Content = utils.StructToJsonString(src.AdvancedTextElem) default: lc.Content = utils.StructToJsonString(src.NotificationElem) } if src.SessionType == constant.GroupChatType || src.SessionType == constant.SuperGroupChatType { lc.RecvID = src.GroupID } lc.AttachedInfo = utils.StructToJsonString(src.AttachedInfoElem) return &lc } func (c *Conversation) msgDataToLocalChatLog(src *sdkws.MsgData) *model_struct.LocalChatLog { var lc model_struct.LocalChatLog copier.Copy(&lc, src) lc.Content = string(src.Content) if src.SessionType == constant.GroupChatType || src.SessionType == constant.SuperGroupChatType { lc.RecvID = src.GroupID } return &lc } func (c *Conversation) msgDataToLocalErrChatLog(src *model_struct.LocalChatLog) *model_struct.LocalErrChatLog { var lc model_struct.LocalErrChatLog copier.Copy(&lc, src) return &lc } func localChatLogToMsgStruct(dst *sdk_struct.NewMsgList, src []*model_struct.LocalChatLog) { copier.Copy(dst, &src) } func (c *Conversation) updateMsgStatusAndTriggerConversation(ctx context.Context, clientMsgID, serverMsgID string, sendTime int64, status int32, s *sdk_struct.MsgStruct, lc *model_struct.LocalConversation, isOnlineOnly bool) { log.ZDebug(ctx, "this is test send message ", "sendTime", sendTime, "status", status, "clientMsgID", clientMsgID, "serverMsgID", serverMsgID) if isOnlineOnly { return } s.SendTime = sendTime s.Status = status s.ServerMsgID = serverMsgID err := c.db.UpdateMessageTimeAndStatus(ctx, lc.ConversationID, clientMsgID, serverMsgID, sendTime, status) if err != nil { log.ZWarn(ctx, "send message update message status error", err, "sendTime", sendTime, "status", status, "clientMsgID", clientMsgID, "serverMsgID", serverMsgID) } err = c.db.DeleteSendingMessage(ctx, lc.ConversationID, clientMsgID) if err != nil { log.ZWarn(ctx, "send message delete sending message error", err) } lc.LatestMsg = utils.StructToJsonString(s) lc.LatestMsgSendTime = sendTime // log.Info("", "2 send message come here", *lc) _ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: lc.ConversationID, Action: constant.AddConOrUpLatMsg, Args: *lc}, c.GetCh()) } func (c *Conversation) fileName(ftype string, id string) string { return fmt.Sprintf("msg_%s_%s", ftype, id) } func (c *Conversation) checkID(ctx context.Context, s *sdk_struct.MsgStruct, recvID, groupID string, options map[string]bool) (*model_struct.LocalConversation, error) { if recvID == "" && groupID == "" { return nil, sdkerrs.ErrArgs } s.SendID = c.loginUserID s.SenderPlatformID = c.platformID lc := &model_struct.LocalConversation{LatestMsgSendTime: s.CreateTime} //根据单聊群聊类型组装消息和会话 if recvID == "" { g, err := c.full.GetGroupInfoByGroupID(ctx, groupID) if err != nil { return nil, err } lc.ShowName = g.GroupName lc.FaceURL = g.FaceURL switch g.GroupType { case constant.NormalGroup: s.SessionType = constant.GroupChatType lc.ConversationType = constant.GroupChatType lc.ConversationID = c.getConversationIDBySessionType(groupID, constant.GroupChatType) case constant.SuperGroup, constant.WorkingGroup: s.SessionType = constant.SuperGroupChatType lc.ConversationID = c.getConversationIDBySessionType(groupID, constant.SuperGroupChatType) lc.ConversationType = constant.SuperGroupChatType } s.GroupID = groupID lc.GroupID = groupID gm, err := c.db.GetGroupMemberInfoByGroupIDUserID(ctx, groupID, c.loginUserID) if err == nil && gm != nil { if gm.Nickname != "" { s.SenderNickname = gm.Nickname } } var attachedInfo sdk_struct.AttachedInfoElem attachedInfo.GroupHasReadInfo.GroupMemberCount = g.MemberCount s.AttachedInfoElem = &attachedInfo } else { s.SessionType = constant.SingleChatType s.RecvID = recvID lc.ConversationID = utils.GetConversationIDByMsg(s) lc.UserID = recvID lc.ConversationType = constant.SingleChatType oldLc, err := c.db.GetConversation(ctx, lc.ConversationID) if err == nil && oldLc.IsPrivateChat { options[constant.IsNotPrivate] = false var attachedInfo sdk_struct.AttachedInfoElem attachedInfo.IsPrivateChat = true attachedInfo.BurnDuration = oldLc.BurnDuration s.AttachedInfoElem = &attachedInfo } if err != nil { t := time.Now() faceUrl, name, err := c.getUserNameAndFaceURL(ctx, recvID) log.ZDebug(ctx, "GetUserNameAndFaceURL", "cost time", time.Since(t)) if err != nil { return nil, err } lc.FaceURL = faceUrl lc.ShowName = name } } return lc, nil } func (c *Conversation) getConversationIDBySessionType(sourceID string, sessionType int) string { switch sessionType { case constant.SingleChatType: l := []string{c.loginUserID, sourceID} sort.Strings(l) return "si_" + strings.Join(l, "_") // single chat case constant.GroupChatType: return "g_" + sourceID // group chat case constant.SuperGroupChatType: return "sg_" + sourceID // super group chat case constant.NotificationChatType: return "sn_" + sourceID + "_" + c.loginUserID // server notification chat } return "" } func (c *Conversation) GetConversationIDBySessionType(_ context.Context, sourceID string, sessionType int) string { return c.getConversationIDBySessionType(sourceID, sessionType) } func (c *Conversation) SendMessage(ctx context.Context, s *sdk_struct.MsgStruct, recvID, groupID string, p *sdkws.OfflinePushInfo, isOnlineOnly bool) (*sdk_struct.MsgStruct, error) { filepathExt := func(name ...string) string { for _, path := range name { if ext := filepath.Ext(path); ext != "" { return ext } } return "" } options := make(map[string]bool, 2) lc, err := c.checkID(ctx, s, recvID, groupID, options) if err != nil { return nil, err } callback, _ := ctx.Value("callback").(open_im_sdk_callback.SendMsgCallBack) log.ZDebug(ctx, "before insert message is", "message", *s) if !isOnlineOnly { oldMessage, err := c.db.GetMessage(ctx, lc.ConversationID, s.ClientMsgID) if err != nil { localMessage := c.msgStructToLocalChatLog(s) err := c.db.InsertMessage(ctx, lc.ConversationID, localMessage) if err != nil { return nil, err } err = c.db.InsertSendingMessage(ctx, &model_struct.LocalSendingMessages{ ConversationID: lc.ConversationID, ClientMsgID: localMessage.ClientMsgID, }) if err != nil { return nil, err } } else { if oldMessage.Status != constant.MsgStatusSendFailed { return nil, sdkerrs.ErrMsgRepeated } else { s.Status = constant.MsgStatusSending err = c.db.InsertSendingMessage(ctx, &model_struct.LocalSendingMessages{ ConversationID: lc.ConversationID, ClientMsgID: s.ClientMsgID, }) if err != nil { return nil, err } } } lc.LatestMsg = utils.StructToJsonString(s) log.ZDebug(ctx, "send message come here", "conversion", *lc) _ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: lc.ConversationID, Action: constant.AddConOrUpLatMsg, Args: *lc}, c.GetCh()) } var delFile []string //media file handle switch s.ContentType { case constant.Picture: if s.Status == constant.MsgStatusSendSuccess { s.Content = utils.StructToJsonString(s.PictureElem) break } var sourcePath string if utils.FileExist(s.PictureElem.SourcePath) { sourcePath = s.PictureElem.SourcePath delFile = append(delFile, utils.FileTmpPath(s.PictureElem.SourcePath, c.DataDir)) } else { sourcePath = utils.FileTmpPath(s.PictureElem.SourcePath, c.DataDir) delFile = append(delFile, sourcePath) } // log.Info("", "file", sourcePath, delFile) log.ZDebug(ctx, "send picture", "path", sourcePath) res, err := c.file.UploadFile(ctx, &file.UploadFileReq{ ContentType: s.PictureElem.SourcePicture.Type, Filepath: sourcePath, Uuid: s.PictureElem.SourcePicture.UUID, Name: c.fileName("picture", s.ClientMsgID) + filepathExt(s.PictureElem.SourcePicture.UUID, sourcePath), Cause: "msg-picture", }, NewUploadFileCallback(ctx, callback.OnProgress, s, lc.ConversationID, c.db)) if err != nil { c.updateMsgStatusAndTriggerConversation(ctx, s.ClientMsgID, "", s.CreateTime, constant.MsgStatusSendFailed, s, lc, isOnlineOnly) return nil, err } s.PictureElem.SourcePicture.Url = res.URL s.PictureElem.BigPicture = s.PictureElem.SourcePicture u, err := url.Parse(res.URL) if err == nil { snapshot := u.Query() snapshot.Set("type", "image") snapshot.Set("width", "640") snapshot.Set("height", "640") u.RawQuery = snapshot.Encode() s.PictureElem.SnapshotPicture = &sdk_struct.PictureBaseInfo{ Width: 640, Height: 640, Url: u.String(), } } else { log.ZError(ctx, "parse url failed", err, "url", res.URL, "err", err) s.PictureElem.SnapshotPicture = s.PictureElem.SourcePicture } s.Content = utils.StructToJsonString(s.PictureElem) case constant.Sound: if s.Status == constant.MsgStatusSendSuccess { s.Content = utils.StructToJsonString(s.SoundElem) break } var sourcePath string if utils.FileExist(s.SoundElem.SoundPath) { sourcePath = s.SoundElem.SoundPath delFile = append(delFile, utils.FileTmpPath(s.SoundElem.SoundPath, c.DataDir)) } else { sourcePath = utils.FileTmpPath(s.SoundElem.SoundPath, c.DataDir) delFile = append(delFile, sourcePath) } // log.Info("", "file", sourcePath, delFile) res, err := c.file.UploadFile(ctx, &file.UploadFileReq{ ContentType: s.SoundElem.SoundType, Filepath: sourcePath, Uuid: s.SoundElem.UUID, Name: c.fileName("voice", s.ClientMsgID) + filepathExt(s.SoundElem.UUID, sourcePath), Cause: "msg-voice", }, NewUploadFileCallback(ctx, callback.OnProgress, s, lc.ConversationID, c.db)) if err != nil { c.updateMsgStatusAndTriggerConversation(ctx, s.ClientMsgID, "", s.CreateTime, constant.MsgStatusSendFailed, s, lc, isOnlineOnly) return nil, err } s.SoundElem.SourceURL = res.URL s.Content = utils.StructToJsonString(s.SoundElem) case constant.Video: if s.Status == constant.MsgStatusSendSuccess { s.Content = utils.StructToJsonString(s.VideoElem) break } var videoPath string var snapPath string if utils.FileExist(s.VideoElem.VideoPath) { videoPath = s.VideoElem.VideoPath snapPath = s.VideoElem.SnapshotPath delFile = append(delFile, utils.FileTmpPath(s.VideoElem.VideoPath, c.DataDir)) delFile = append(delFile, utils.FileTmpPath(s.VideoElem.SnapshotPath, c.DataDir)) } else { videoPath = utils.FileTmpPath(s.VideoElem.VideoPath, c.DataDir) snapPath = utils.FileTmpPath(s.VideoElem.SnapshotPath, c.DataDir) delFile = append(delFile, videoPath) delFile = append(delFile, snapPath) } log.ZDebug(ctx, "file", "videoPath", videoPath, "snapPath", snapPath, "delFile", delFile) var wg sync.WaitGroup wg.Add(2) var putErrs error go func() { defer wg.Done() snapRes, err := c.file.UploadFile(ctx, &file.UploadFileReq{ ContentType: s.VideoElem.SnapshotType, Filepath: snapPath, Uuid: s.VideoElem.SnapshotUUID, Name: c.fileName("videoSnapshot", s.ClientMsgID) + filepathExt(s.VideoElem.SnapshotUUID, snapPath), Cause: "msg-video-snapshot", }, nil) if err != nil { log.ZWarn(ctx, "upload video snapshot failed", err) return } s.VideoElem.SnapshotURL = snapRes.URL }() go func() { defer wg.Done() res, err := c.file.UploadFile(ctx, &file.UploadFileReq{ ContentType: content_type.GetType(s.VideoElem.VideoType, filepath.Ext(s.VideoElem.VideoPath)), Filepath: videoPath, Uuid: s.VideoElem.VideoUUID, Name: c.fileName("video", s.ClientMsgID) + filepathExt(s.VideoElem.VideoUUID, videoPath), Cause: "msg-video", }, NewUploadFileCallback(ctx, callback.OnProgress, s, lc.ConversationID, c.db)) if err != nil { c.updateMsgStatusAndTriggerConversation(ctx, s.ClientMsgID, "", s.CreateTime, constant.MsgStatusSendFailed, s, lc, isOnlineOnly) putErrs = err } if res != nil { s.VideoElem.VideoURL = res.URL } }() wg.Wait() if err := putErrs; err != nil { return nil, err } s.Content = utils.StructToJsonString(s.VideoElem) case constant.File: if s.Status == constant.MsgStatusSendSuccess { s.Content = utils.StructToJsonString(s.FileElem) break } name := s.FileElem.FileName if name == "" { name = s.FileElem.FilePath } if name == "" { name = fmt.Sprintf("msg_file_%s.unknown", s.ClientMsgID) } res, err := c.file.UploadFile(ctx, &file.UploadFileReq{ ContentType: content_type.GetType(s.FileElem.FileType, filepath.Ext(s.FileElem.FilePath), filepath.Ext(s.FileElem.FileName)), Filepath: s.FileElem.FilePath, Uuid: s.FileElem.UUID, Name: c.fileName("file", s.ClientMsgID) + "/" + filepath.Base(name), Cause: "msg-file", }, NewUploadFileCallback(ctx, callback.OnProgress, s, lc.ConversationID, c.db)) if err != nil { c.updateMsgStatusAndTriggerConversation(ctx, s.ClientMsgID, "", s.CreateTime, constant.MsgStatusSendFailed, s, lc, isOnlineOnly) return nil, err } s.FileElem.SourceURL = res.URL s.Content = utils.StructToJsonString(s.FileElem) case constant.Text: s.Content = utils.StructToJsonString(s.TextElem) case constant.AtText: s.Content = utils.StructToJsonString(s.AtTextElem) case constant.Location: s.Content = utils.StructToJsonString(s.LocationElem) case constant.Custom: s.Content = utils.StructToJsonString(s.CustomElem) case constant.Merger: s.Content = utils.StructToJsonString(s.MergeElem) case constant.Quote: s.Content = utils.StructToJsonString(s.QuoteElem) case constant.Card: s.Content = utils.StructToJsonString(s.CardElem) case constant.Face: s.Content = utils.StructToJsonString(s.FaceElem) case constant.AdvancedText: s.Content = utils.StructToJsonString(s.AdvancedTextElem) default: return nil, sdkerrs.ErrMsgContentTypeNotSupport } if utils.IsContainInt(int(s.ContentType), []int{constant.Picture, constant.Sound, constant.Video, constant.File}) { if !isOnlineOnly { localMessage := c.msgStructToLocalChatLog(s) log.ZDebug(ctx, "update message is ", "localMessage", localMessage) err = c.db.UpdateMessage(ctx, lc.ConversationID, localMessage) if err != nil { return nil, err } } } return c.sendMessageToServer(ctx, s, lc, callback, delFile, p, options, isOnlineOnly) } func (c *Conversation) SendMessageNotOss(ctx context.Context, s *sdk_struct.MsgStruct, recvID, groupID string, p *sdkws.OfflinePushInfo, isOnlineOnly bool) (*sdk_struct.MsgStruct, error) { options := make(map[string]bool, 2) lc, err := c.checkID(ctx, s, recvID, groupID, options) if err != nil { return nil, err } callback, _ := ctx.Value("callback").(open_im_sdk_callback.SendMsgCallBack) if !isOnlineOnly { oldMessage, err := c.db.GetMessage(ctx, lc.ConversationID, s.ClientMsgID) if err != nil { localMessage := c.msgStructToLocalChatLog(s) err := c.db.InsertMessage(ctx, lc.ConversationID, localMessage) if err != nil { return nil, err } err = c.db.InsertSendingMessage(ctx, &model_struct.LocalSendingMessages{ ConversationID: lc.ConversationID, ClientMsgID: localMessage.ClientMsgID, }) if err != nil { return nil, err } } else { if oldMessage.Status != constant.MsgStatusSendFailed { return nil, sdkerrs.ErrMsgRepeated } else { s.Status = constant.MsgStatusSending err = c.db.InsertSendingMessage(ctx, &model_struct.LocalSendingMessages{ ConversationID: lc.ConversationID, ClientMsgID: s.ClientMsgID, }) if err != nil { return nil, err } } } } lc.LatestMsg = utils.StructToJsonString(s) var delFile []string switch s.ContentType { case constant.Picture: s.Content = utils.StructToJsonString(s.PictureElem) case constant.Sound: s.Content = utils.StructToJsonString(s.SoundElem) case constant.Video: s.Content = utils.StructToJsonString(s.VideoElem) case constant.File: s.Content = utils.StructToJsonString(s.FileElem) case constant.Text: s.Content = utils.StructToJsonString(s.TextElem) case constant.AtText: s.Content = utils.StructToJsonString(s.AtTextElem) case constant.Location: s.Content = utils.StructToJsonString(s.LocationElem) case constant.Custom: s.Content = utils.StructToJsonString(s.CustomElem) case constant.Merger: s.Content = utils.StructToJsonString(s.MergeElem) case constant.Quote: s.Content = utils.StructToJsonString(s.QuoteElem) case constant.Card: s.Content = utils.StructToJsonString(s.CardElem) case constant.Face: s.Content = utils.StructToJsonString(s.FaceElem) case constant.AdvancedText: s.Content = utils.StructToJsonString(s.AdvancedTextElem) default: return nil, sdkerrs.ErrMsgContentTypeNotSupport } if utils.IsContainInt(int(s.ContentType), []int{constant.Picture, constant.Sound, constant.Video, constant.File}) { if isOnlineOnly { localMessage := c.msgStructToLocalChatLog(s) err = c.db.UpdateMessage(ctx, lc.ConversationID, localMessage) if err != nil { return nil, err } } } return c.sendMessageToServer(ctx, s, lc, callback, delFile, p, options, isOnlineOnly) } func (c *Conversation) sendMessageToServer(ctx context.Context, s *sdk_struct.MsgStruct, lc *model_struct.LocalConversation, callback open_im_sdk_callback.SendMsgCallBack, delFile []string, offlinePushInfo *sdkws.OfflinePushInfo, options map[string]bool, isOnlineOnly bool) (*sdk_struct.MsgStruct, error) { if isOnlineOnly { 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) } //Protocol conversion var wsMsgData sdkws.MsgData copier.Copy(&wsMsgData, s) wsMsgData.AttachedInfo = utils.StructToJsonString(s.AttachedInfoElem) wsMsgData.Content = []byte(s.Content) wsMsgData.CreateTime = s.CreateTime wsMsgData.SendTime = 0 wsMsgData.Options = options if wsMsgData.ContentType == constant.AtText { wsMsgData.AtUserIDList = s.AtTextElem.AtUserList } wsMsgData.OfflinePushInfo = offlinePushInfo s.Content = "" var sendMsgResp sdkws.UserSendMsgResp err := c.LongConnMgr.SendReqWaitResp(ctx, &wsMsgData, constant.SendMsg, &sendMsgResp) if err != nil { //if send message network timeout need to double-check message has received by db. if sdkerrs.ErrNetworkTimeOut.Is(err) && !isOnlineOnly { oldMessage, _ := c.db.GetMessage(ctx, lc.ConversationID, s.ClientMsgID) if oldMessage.Status == constant.MsgStatusSendSuccess { sendMsgResp.SendTime = oldMessage.SendTime sendMsgResp.ClientMsgID = oldMessage.ClientMsgID sendMsgResp.ServerMsgID = oldMessage.ServerMsgID } else { log.ZError(ctx, "send msg to server failed", err, "message", s) c.updateMsgStatusAndTriggerConversation(ctx, s.ClientMsgID, "", s.CreateTime, constant.MsgStatusSendFailed, s, lc, isOnlineOnly) return s, err } } else { log.ZError(ctx, "send msg to server failed", err, "message", s) c.updateMsgStatusAndTriggerConversation(ctx, s.ClientMsgID, "", s.CreateTime, constant.MsgStatusSendFailed, s, lc, isOnlineOnly) return s, err } } s.SendTime = sendMsgResp.SendTime s.Status = constant.MsgStatusSendSuccess s.ServerMsgID = sendMsgResp.ServerMsgID go func() { //remove media cache file for _, v := range delFile { err := os.Remove(v) if err != nil { // log.Error("", "remove failed,", err.Error(), v) } // log.Debug("", "remove file: ", v) } c.updateMsgStatusAndTriggerConversation(ctx, sendMsgResp.ClientMsgID, sendMsgResp.ServerMsgID, sendMsgResp.SendTime, constant.MsgStatusSendSuccess, s, lc, isOnlineOnly) }() return s, nil } func (c *Conversation) FindMessageList(ctx context.Context, req []*sdk_params_callback.ConversationArgs) (*sdk_params_callback.FindMessageListCallback, error) { var r sdk_params_callback.FindMessageListCallback type tempConversationAndMessageList struct { conversation *model_struct.LocalConversation msgIDList []string } var s []*tempConversationAndMessageList for _, conversationsArgs := range req { localConversation, err := c.db.GetConversation(ctx, conversationsArgs.ConversationID) if err != nil { log.ZError(ctx, "GetConversation err", err, "conversationsArgs", conversationsArgs) } else { t := new(tempConversationAndMessageList) t.conversation = localConversation t.msgIDList = conversationsArgs.ClientMsgIDList s = append(s, t) } } for _, v := range s { messages, err := c.db.GetMessagesByClientMsgIDs(ctx, v.conversation.ConversationID, v.msgIDList) if err == nil { var tempMessageList []*sdk_struct.MsgStruct for _, message := range messages { temp := sdk_struct.MsgStruct{} temp.ClientMsgID = message.ClientMsgID temp.ServerMsgID = message.ServerMsgID temp.CreateTime = message.CreateTime temp.SendTime = message.SendTime temp.SessionType = message.SessionType temp.SendID = message.SendID temp.RecvID = message.RecvID temp.MsgFrom = message.MsgFrom temp.ContentType = message.ContentType temp.SenderPlatformID = message.SenderPlatformID temp.SenderNickname = message.SenderNickname temp.SenderFaceURL = message.SenderFaceURL temp.Content = message.Content temp.Seq = message.Seq temp.IsRead = message.IsRead temp.Status = message.Status temp.AttachedInfo = message.AttachedInfo temp.Ex = message.Ex temp.LocalEx = message.LocalEx err := c.msgHandleByContentType(&temp) if err != nil { log.ZError(ctx, "msgHandleByContentType err", err, "message", temp) continue } switch message.SessionType { case constant.GroupChatType: fallthrough case constant.SuperGroupChatType: temp.GroupID = temp.RecvID temp.RecvID = c.loginUserID } tempMessageList = append(tempMessageList, &temp) } findResultItem := sdk_params_callback.SearchByConversationResult{} findResultItem.ConversationID = v.conversation.ConversationID findResultItem.FaceURL = v.conversation.FaceURL findResultItem.ShowName = v.conversation.ShowName findResultItem.ConversationType = v.conversation.ConversationType findResultItem.MessageList = tempMessageList findResultItem.MessageCount = len(findResultItem.MessageList) r.FindResultItems = append(r.FindResultItems, &findResultItem) r.TotalCount += findResultItem.MessageCount } else { log.ZError(ctx, "GetMessagesByClientMsgIDs err", err, "conversationID", v.conversation.ConversationID, "msgIDList", v.msgIDList) } } return &r, nil } func (c *Conversation) GetAdvancedHistoryMessageList(ctx context.Context, req sdk_params_callback.GetAdvancedHistoryMessageListParams) (*sdk_params_callback.GetAdvancedHistoryMessageListCallback, error) { result, err := c.getAdvancedHistoryMessageList(ctx, req, false) if err != nil { return nil, err } if len(result.MessageList) == 0 { s := make([]*sdk_struct.MsgStruct, 0) result.MessageList = s } return result, nil } func (c *Conversation) GetAdvancedHistoryMessageListReverse(ctx context.Context, req sdk_params_callback.GetAdvancedHistoryMessageListParams) (*sdk_params_callback.GetAdvancedHistoryMessageListCallback, error) { result, err := c.getAdvancedHistoryMessageList(ctx, req, true) if err != nil { return nil, err } if len(result.MessageList) == 0 { s := make([]*sdk_struct.MsgStruct, 0) result.MessageList = s } return result, nil } func (c *Conversation) RevokeMessage(ctx context.Context, conversationID, clientMsgID string) error { return c.revokeOneMessage(ctx, conversationID, clientMsgID) } func (c *Conversation) TypingStatusUpdate(ctx context.Context, recvID, msgTip string) error { return c.typingStatusUpdate(ctx, recvID, msgTip) } // funcation (c *Conversation) MarkMessageAsReadByConID(ctx context.Context, conversationID string, msgIDList []string) error { // if len(msgIDList) == 0 { // _ = c.setOneConversationUnread(ctx, conversationID, 0) // _ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: conversationID, Action: constant.UnreadCountSetZero}, c.GetCh()) // _ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: conversationID, Action: constant.ConChange, Args: []string{conversationID}}, c.GetCh()) // return nil // } // return nil // } // deprecated // funcation (c *Conversation) MarkGroupMessageHasRead(ctx context.Context, groupID string) { // conversationID := c.getConversationIDBySessionType(groupID, constant.GroupChatType) // _ = c.setOneConversationUnread(ctx, conversationID, 0) // _ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: conversationID, Action: constant.UnreadCountSetZero}, c.GetCh()) // _ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: conversationID, Action: constant.ConChange, Args: []string{conversationID}}, c.GetCh()) // } // read draw func (c *Conversation) MarkConversationMessageAsRead(ctx context.Context, conversationID string) error { return c.markConversationMessageAsRead(ctx, conversationID) } func (c *Conversation) MarkMessagesAsReadByMsgID(ctx context.Context, conversationID string, clientMsgIDs []string) error { return c.markMessagesAsReadByMsgID(ctx, conversationID, clientMsgIDs) } // delete func (c *Conversation) DeleteMessageFromLocalStorage(ctx context.Context, conversationID string, clientMsgID string) error { return c.deleteMessageFromLocal(ctx, conversationID, clientMsgID) } func (c *Conversation) DeleteMessage(ctx context.Context, conversationID string, clientMsgID string) error { return c.deleteMessage(ctx, conversationID, clientMsgID) } func (c *Conversation) DeleteAllMsgFromLocalAndSvr(ctx context.Context) error { return c.deleteAllMsgFromLocalAndSvr(ctx) } func (c *Conversation) DeleteAllMessageFromLocalStorage(ctx context.Context) error { return c.deleteAllMsgFromLocal(ctx, true) } func (c *Conversation) ClearConversationAndDeleteAllMsg(ctx context.Context, conversationID string) error { return c.clearConversationFromLocalAndSvr(ctx, conversationID, c.db.ClearConversation) } func (c *Conversation) DeleteConversationAndDeleteAllMsg(ctx context.Context, conversationID string) error { return c.clearConversationFromLocalAndSvr(ctx, conversationID, c.db.ResetConversation) } // insert func (c *Conversation) InsertSingleMessageToLocalStorage(ctx context.Context, s *sdk_struct.MsgStruct, recvID, sendID string) (*sdk_struct.MsgStruct, error) { if recvID == "" || sendID == "" { return nil, sdkerrs.ErrArgs } var conversation model_struct.LocalConversation if sendID != c.loginUserID { faceUrl, name, err := c.getUserNameAndFaceURL(ctx, sendID) if err != nil { //log.Error(operationID, "GetUserNameAndFaceURL err", err.Error(), sendID) } s.SenderFaceURL = faceUrl s.SenderNickname = name conversation.FaceURL = faceUrl conversation.ShowName = name conversation.UserID = sendID conversation.ConversationID = c.getConversationIDBySessionType(sendID, constant.SingleChatType) } else { conversation.UserID = recvID conversation.ConversationID = c.getConversationIDBySessionType(recvID, constant.SingleChatType) _, err := c.db.GetConversation(ctx, conversation.ConversationID) if err != nil { faceUrl, name, err := c.getUserNameAndFaceURL(ctx, recvID) if err != nil { return nil, err } conversation.FaceURL = faceUrl conversation.ShowName = name } } s.SendID = sendID s.RecvID = recvID s.ClientMsgID = utils.GetMsgID(s.SendID) s.SendTime = utils.GetCurrentTimestampByMill() s.SessionType = constant.SingleChatType s.Status = constant.MsgStatusSendSuccess localMessage := c.msgStructToLocalChatLog(s) conversation.LatestMsg = utils.StructToJsonString(s) conversation.ConversationType = constant.SingleChatType conversation.LatestMsgSendTime = s.SendTime err := c.insertMessageToLocalStorage(ctx, conversation.ConversationID, localMessage) if err != nil { return nil, err } _ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: conversation.ConversationID, Action: constant.AddConOrUpLatMsg, Args: conversation}, c.GetCh()) return s, nil } func (c *Conversation) InsertGroupMessageToLocalStorage(ctx context.Context, s *sdk_struct.MsgStruct, groupID, sendID string) (*sdk_struct.MsgStruct, error) { if groupID == "" || sendID == "" { return nil, sdkerrs.ErrArgs } var conversation model_struct.LocalConversation var err error _, conversation.ConversationType, err = c.getConversationTypeByGroupID(ctx, groupID) if err != nil { return nil, err } conversation.ConversationID = c.getConversationIDBySessionType(groupID, int(conversation.ConversationType)) if sendID != c.loginUserID { faceUrl, name, err := c.getUserNameAndFaceURL(ctx, sendID) if err != nil { // log.Error("", "getUserNameAndFaceUrlByUid err", err.Error(), sendID) } s.SenderFaceURL = faceUrl s.SenderNickname = name } s.SendID = sendID s.RecvID = groupID s.GroupID = groupID s.ClientMsgID = utils.GetMsgID(s.SendID) s.SendTime = utils.GetCurrentTimestampByMill() s.SessionType = conversation.ConversationType s.Status = constant.MsgStatusSendSuccess localMessage := c.msgStructToLocalChatLog(s) conversation.LatestMsg = utils.StructToJsonString(s) conversation.LatestMsgSendTime = s.SendTime conversation.FaceURL = s.SenderFaceURL conversation.ShowName = s.SenderNickname err = c.insertMessageToLocalStorage(ctx, conversation.ConversationID, localMessage) if err != nil { return nil, err } _ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: conversation.ConversationID, Action: constant.AddConOrUpLatMsg, Args: conversation}, c.GetCh()) return s, nil } func (c *Conversation) SearchLocalMessages(ctx context.Context, searchParam *sdk_params_callback.SearchLocalMessagesParams) (*sdk_params_callback.SearchLocalMessagesCallback, error) { searchParam.KeywordList = utils.TrimStringList(searchParam.KeywordList) return c.searchLocalMessages(ctx, searchParam) } func (c *Conversation) SetMessageLocalEx(ctx context.Context, conversationID string, clientMsgID string, localEx string) error { err := c.db.UpdateColumnsMessage(ctx, conversationID, clientMsgID, map[string]interface{}{"local_ex": localEx}) if err != nil { return err } 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, "latestMsg local ex changed", "seq", latestMsg.Seq, "clientMsgID", latestMsg.ClientMsgID) latestMsg.LocalEx = localEx latestMsgStr := utils.StructToJsonString(latestMsg) if err = c.db.UpdateColumnsConversation(ctx, conversationID, map[string]interface{}{"latest_msg": latestMsgStr, "latest_msg_send_time": latestMsg.SendTime}); err != nil { return err } c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: []string{conversationID}}}) } return nil } func (c *Conversation) initBasicInfo(ctx context.Context, message *sdk_struct.MsgStruct, msgFrom, contentType int32) error { message.CreateTime = utils.GetCurrentTimestampByMill() message.SendTime = message.CreateTime message.IsRead = false message.Status = constant.MsgStatusSending message.SendID = c.loginUserID userInfo, err := c.db.GetLoginUser(ctx, c.loginUserID) if err != nil { return err } else { message.SenderFaceURL = userInfo.FaceURL message.SenderNickname = userInfo.Nickname } ClientMsgID := utils.GetMsgID(message.SendID) message.ClientMsgID = ClientMsgID message.MsgFrom = msgFrom message.ContentType = contentType message.SenderPlatformID = c.platformID message.IsExternalExtensions = c.IsExternalExtensions return nil } //// 删除本地和服务器 //// 删除本地的话不用改服务器的数据 //// 删除服务器的话,需要把本地的消息状态改成删除 //funcation (c *Conversation) DeleteConversationFromLocalAndSvr(ctx context.Context, conversationID string) error { // // Use conversationID to remove conversations and messages from the server first // err := c.clearConversationFromSvr(ctx, conversationID) // if err != nil { // return err // } // return c.deleteConversation(ctx, conversationID) //} func (c *Conversation) getConversationTypeByGroupID(ctx context.Context, groupID string) (conversationID string, conversationType int32, err error) { g, err := c.full.GetGroupInfoByGroupID(ctx, groupID) if err != nil { return "", 0, utils.Wrap(err, "get group info error") } switch g.GroupType { case constant.NormalGroup: return c.getConversationIDBySessionType(groupID, constant.GroupChatType), constant.GroupChatType, nil case constant.SuperGroup, constant.WorkingGroup: return c.getConversationIDBySessionType(groupID, constant.SuperGroupChatType), constant.SuperGroupChatType, nil default: return "", 0, sdkerrs.ErrGroupType } } func (c *Conversation) SetMessageReactionExtensions(ctx context.Context, s *sdk_struct.MsgStruct, req []*server_api_params.KeyValue) ([]*server_api_params.ExtensionResult, error) { return c.setMessageReactionExtensions(ctx, s, req) } func (c *Conversation) AddMessageReactionExtensions(ctx context.Context, s *sdk_struct.MsgStruct, reactionExtensionList []*server_api_params.KeyValue) ([]*server_api_params.ExtensionResult, error) { return c.addMessageReactionExtensions(ctx, s, reactionExtensionList) } func (c *Conversation) DeleteMessageReactionExtensions(ctx context.Context, s *sdk_struct.MsgStruct, reactionExtensionKeyList []string) ([]*server_api_params.ExtensionResult, error) { return c.deleteMessageReactionExtensions(ctx, s, reactionExtensionKeyList) } func (c *Conversation) GetMessageListReactionExtensions(ctx context.Context, conversationID string, messageList []*sdk_struct.MsgStruct) ([]*server_api_params.SingleMessageExtensionResult, error) { return c.getMessageListReactionExtensions(ctx, conversationID, messageList) } func (c *Conversation) SearchConversation(ctx context.Context, searchParam string) ([]*server_api_params.Conversation, error) { // Check if search parameter is empty if searchParam == "" { return nil, sdkerrs.ErrArgs.WrapMsg("search parameter cannot be empty") } // Perform the search in your database or data source // This is a placeholder for the actual database call conversations, err := c.db.SearchConversations(ctx, searchParam) if err != nil { // Handle any errors that occurred during the search return nil, err } apiConversations := make([]*server_api_params.Conversation, len(conversations)) for i, localConv := range conversations { // Create new server_api_params.Conversation and map fields from localConv apiConv := &server_api_params.Conversation{ ConversationID: localConv.ConversationID, ConversationType: localConv.ConversationType, UserID: localConv.UserID, GroupID: localConv.GroupID, RecvMsgOpt: localConv.RecvMsgOpt, UnreadCount: localConv.UnreadCount, DraftTextTime: localConv.DraftTextTime, IsPinned: localConv.IsPinned, IsPrivateChat: localConv.IsPrivateChat, BurnDuration: localConv.BurnDuration, GroupAtType: localConv.GroupAtType, IsNotInGroup: localConv.IsNotInGroup, UpdateUnreadCountTime: localConv.UpdateUnreadCountTime, AttachedInfo: localConv.AttachedInfo, Ex: localConv.Ex, } apiConversations[i] = apiConv } // Return the list of conversations return apiConversations, nil } /** **Get some reaction extensions in reactionExtensionKeyList of message list */ //funcation (c *Conversation) GetMessageListSomeReactionExtensions(ctx context.Context, messageList, reactionExtensionKeyList, operationID string) { // var messagelist []*sdk_struct.MsgStruct // common.JsonUnmarshalAndArgsValidate(messageList, &messagelist, callback, operationID) // var list []string // common.JsonUnmarshalAndArgsValidate(reactionExtensionKeyList, &list, callback, operationID) // result := c.getMessageListSomeReactionExtensions(callback, messagelist, list, operationID) // callback.OnSuccess(utils.StructToJsonString(result)) // log.NewInfo(operationID, utils.GetSelfFuncName(), "callback: ", utils.StructToJsonString(result)) //} //funcation (c *Conversation) SetTypeKeyInfo(ctx context.Context, message, typeKey, ex string, isCanRepeat bool, operationID string) { // s := sdk_struct.MsgStruct{} // common.JsonUnmarshalAndArgsValidate(message, &s, callback, operationID) // result := c.setTypeKeyInfo(callback, &s, typeKey, ex, isCanRepeat, operationID) // callback.OnSuccess(utils.StructToJsonString(result)) // log.NewInfo(operationID, utils.GetSelfFuncName(), "callback: ", utils.StructToJsonString(result)) //} //funcation (c *Conversation) GetTypeKeyListInfo(ctx context.Context, message, typeKeyList, operationID string) { // s := sdk_struct.MsgStruct{} // common.JsonUnmarshalAndArgsValidate(message, &s, callback, operationID) // var list []string // common.JsonUnmarshalAndArgsValidate(typeKeyList, &list, callback, operationID) // result := c.getTypeKeyListInfo(callback, &s, list, operationID) // callback.OnSuccess(utils.StructToJsonString(result)) // log.NewInfo(operationID, utils.GetSelfFuncName(), "callback: ", utils.StructToJsonString(result)) //} //funcation (c *Conversation) GetAllTypeKeyInfo(ctx context.Context, message, operationID string) { // s := sdk_struct.MsgStruct{} // common.JsonUnmarshalAndArgsValidate(message, &s, callback, operationID) // result := c.getAllTypeKeyInfo(callback, &s, operationID) // callback.OnSuccess(utils.StructToJsonString(result)) // log.NewInfo(operationID, utils.GetSelfFuncName(), "callback: ", utils.StructToJsonString(result)) //}