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

View File

@@ -0,0 +1,49 @@
// 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 business
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/open_im_sdk_callback"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
"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 Business struct {
listener func() open_im_sdk_callback.OnCustomBusinessListener
db db_interface.DataBase
}
func NewBusiness(db db_interface.DataBase) *Business {
return &Business{
db: db,
}
}
func (b *Business) DoNotification(ctx context.Context, msg *sdkws.MsgData) {
var n sdk_struct.NotificationElem
err := utils.JsonStringToStruct(string(msg.Content), &n)
if err != nil {
log.ZError(ctx, "unmarshal failed", err, "msg", msg)
return
}
b.listener().OnRecvCustomBusinessMessage(n.Detail)
}

View File

@@ -0,0 +1,23 @@
// 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 business
import (
"github.com/openimsdk/openim-sdk-core/v3/open_im_sdk_callback"
)
func (w *Business) SetListener(listener func() open_im_sdk_callback.OnCustomBusinessListener) {
w.listener = listener
}

72
go/chao-sdk-core/internal/cache/cahe.go vendored Normal file
View File

@@ -0,0 +1,72 @@
package cache
import "sync"
// Cache is a Generic sync.Map structure.
type Cache[K comparable, V any] struct {
m sync.Map
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{}
}
// Load returns the value stored in the map for a key, or nil if no value is present.
func (c *Cache[K, V]) Load(key K) (value V, ok bool) {
rawValue, ok := c.m.Load(key)
if !ok {
return
}
return rawValue.(V), ok
}
// Store sets the value for a key.
func (c *Cache[K, V]) Store(key K, value V) {
c.m.Store(key, value)
}
// StoreAll sets all value by f's key.
func (c *Cache[K, V]) StoreAll(f func(value V) K, values []V) {
for _, v := range values {
c.m.Store(f(v), v)
}
}
// LoadOrStore returns the existing value for the key if present.
func (c *Cache[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
rawValue, loaded := c.m.LoadOrStore(key, value)
return rawValue.(V), loaded
}
// Delete deletes the value for a key.
func (c *Cache[K, V]) Delete(key K) {
c.m.Delete(key)
}
// DeleteAll deletes all values.
func (c *Cache[K, V]) DeleteAll() {
c.m.Range(func(key, value interface{}) bool {
c.m.Delete(key)
return true
})
}
// RangeAll returns all values in the map.
func (c *Cache[K, V]) RangeAll() (values []V) {
c.m.Range(func(rawKey, rawValue interface{}) bool {
values = append(values, rawValue.(V))
return true
})
return values
}
// RangeCon returns values in the map that satisfy condition f.
func (c *Cache[K, V]) RangeCon(f func(key K, value V) bool) (values []V) {
c.m.Range(func(rawKey, rawValue interface{}) bool {
if f(rawKey.(K), rawValue.(V)) {
values = append(values, rawValue.(V))
}
return true
})
return values
}

View File

@@ -0,0 +1,33 @@
// 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 common
import (
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/golang/protobuf/proto"
"github.com/openimsdk/protocol/sdkws"
)
func UnmarshalTips(msg *sdkws.MsgData, detail proto.Message) error {
var tips sdkws.TipsComm
if err := proto.Unmarshal(msg.Content, &tips); err != nil {
return utils.Wrap(err, "")
}
if err := proto.Unmarshal(tips.Detail, detail); err != nil {
return utils.Wrap(err, "")
}
return nil
}

View File

@@ -0,0 +1,33 @@
// 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 common
import (
"bytes"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
)
type ObjectStorage interface {
UploadImage(filePath string, onProgressFun func(int)) (string, string, error)
UploadSound(filePath string, onProgressFun func(int)) (string, string, error)
UploadFile(filePath string, onProgressFun func(int)) (string, string, error)
UploadVideo(videoPath, snapshotPath string, onProgressFun func(int)) (string, string, string, string, error)
UploadImageByBuffer(buffer *bytes.Buffer, size int64, imageType string, onProgressFun func(int)) (string, string, error)
UploadSoundByBuffer(buffer *bytes.Buffer, size int64, fileType string, onProgressFun func(int)) (string, string, error)
UploadFileByBuffer(buffer *bytes.Buffer, size int64, fileType string, onProgressFun func(int)) (string, string, error)
UploadVideoByBuffer(videoBuffer, snapshotBuffer *bytes.Buffer, videoSize, snapshotSize int64, videoType, snapshotType string, onProgressFun func(int)) (string, string, string, string, error)
}

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
}

View File

@@ -0,0 +1,89 @@
// Copyright © 2023 OpenIM open source community. 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 file
func NewBitmap(size int) *Bitmap {
data := make([]uint64, (size+63)/64)
return &Bitmap{data: data, size: size}
}
func ParseBitmap(p []byte, size int) *Bitmap {
data := make([]uint64, len(p)/8)
for i := range data {
data[i] = uint64(p[i*8])<<56 |
uint64(p[i*8+1])<<48 |
uint64(p[i*8+2])<<40 |
uint64(p[i*8+3])<<32 |
uint64(p[i*8+4])<<24 |
uint64(p[i*8+5])<<16 |
uint64(p[i*8+6])<<8 |
uint64(p[i*8+7])
}
return &Bitmap{
data: data,
size: size,
}
}
type Bitmap struct {
data []uint64
size int
}
func (b *Bitmap) Set(index int) {
if index < 0 || index >= b.size {
panic("out of range")
}
wordIndex := index / 64
bitIndex := uint(index % 64)
b.data[wordIndex] |= 1 << bitIndex
}
func (b *Bitmap) Clear(index int) {
if index < 0 || index >= b.size {
panic("out of range")
}
wordIndex := index / 64
bitIndex := uint(index % 64)
b.data[wordIndex] &= ^(1 << bitIndex)
}
func (b *Bitmap) Get(index int) bool {
if index < 0 || index >= b.size {
panic("out of range")
}
wordIndex := index / 64
bitIndex := uint(index % 64)
return (b.data[wordIndex] & (1 << bitIndex)) != 0
}
func (b *Bitmap) Size() int {
return b.size
}
func (b *Bitmap) Serialize() []byte {
p := make([]byte, len(b.data)*8)
for i, word := range b.data {
p[i*8] = byte(word >> 56)
p[i*8+1] = byte(word >> 48)
p[i*8+2] = byte(word >> 40)
p[i*8+3] = byte(word >> 32)
p[i*8+4] = byte(word >> 24)
p[i*8+5] = byte(word >> 16)
p[i*8+6] = byte(word >> 8)
p[i*8+7] = byte(word)
}
return p
}

View File

@@ -0,0 +1,62 @@
// 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 file
import "fmt"
type UploadFileCallback interface {
Open(size int64) // 文件打开的大小
PartSize(partSize int64, num int) // 分片大小,数量
HashPartProgress(index int, size int64, partHash string) // 每块分片的hash值
HashPartComplete(partsHash string, fileHash string) // 分块完成服务端标记hash和文件最终hash
UploadID(uploadID string) // 上传ID
UploadPartComplete(index int, partSize int64, partHash string) // 上传分片进度
UploadComplete(fileSize int64, streamSize int64, storageSize int64) // 整体进度
Complete(size int64, url string, typ int) // 上传完成
}
type emptyUploadCallback struct{}
func (e emptyUploadCallback) Open(size int64) {
fmt.Println("Callback Open:", size)
}
func (e emptyUploadCallback) PartSize(partSize int64, num int) {
fmt.Println("Callback PartSize:", partSize, num)
}
func (e emptyUploadCallback) HashPartProgress(index int, size int64, partHash string) {
//fmt.Println("Callback HashPartProgress:", index, size, partHash)
}
func (e emptyUploadCallback) HashPartComplete(partsHash string, fileHash string) {
fmt.Println("Callback HashPartComplete:", partsHash, fileHash)
}
func (e emptyUploadCallback) UploadID(uploadID string) {
fmt.Println("Callback UploadID:", uploadID)
}
func (e emptyUploadCallback) UploadPartComplete(index int, partSize int64, partHash string) {
fmt.Println("Callback UploadPartComplete:", index, partSize, partHash)
}
func (e emptyUploadCallback) UploadComplete(fileSize int64, streamSize int64, storageSize int64) {
fmt.Println("Callback UploadComplete:", fileSize, streamSize, storageSize)
}
func (e emptyUploadCallback) Complete(size int64, url string, typ int) {
fmt.Println("Callback Complete:", size, url, typ)
}

View File

@@ -0,0 +1,24 @@
// Copyright © 2023 OpenIM open source community. 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 file
import "io"
type ReadFile interface {
io.Reader
io.Closer
Size() int64
StartSeek(whence int) error
}

View File

@@ -0,0 +1,73 @@
// Copyright © 2023 OpenIM open source community. 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.
//go:build !js
package file
import (
"bufio"
"io"
"os"
)
const readBufferSize = 1024 * 1024 * 5 // 5mb
func Open(req *UploadFileReq) (ReadFile, error) {
file, err := os.Open(req.Filepath)
if err != nil {
return nil, err
}
info, err := file.Stat()
if err != nil {
_ = file.Close()
return nil, err
}
df := &defaultFile{
file: file,
info: info,
}
df.resetReaderBuffer()
return df, nil
}
type defaultFile struct {
file *os.File
info os.FileInfo
reader io.Reader
}
func (d *defaultFile) resetReaderBuffer() {
d.reader = bufio.NewReaderSize(d.file, readBufferSize)
}
func (d *defaultFile) Read(p []byte) (n int, err error) {
return d.reader.Read(p)
}
func (d *defaultFile) Close() error {
return d.file.Close()
}
func (d *defaultFile) StartSeek(whence int) error {
if _, err := d.file.Seek(io.SeekStart, whence); err != nil {
return err
}
d.resetReaderBuffer()
return nil
}
func (d *defaultFile) Size() int64 {
return d.info.Size()
}

View File

@@ -0,0 +1,156 @@
// Copyright © 2023 OpenIM open source community. 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.
//go:build js && wasm
// +build js,wasm
package file
import (
"bufio"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/wasm/exec"
"io"
"syscall/js"
)
const readBufferSize = 1024 * 1024 * 5 // 5mb
func Open(req *UploadFileReq) (ReadFile, error) {
file := newJsCallFile(req.Uuid)
size, err := file.Open()
if err != nil {
return nil, err
}
jf := &jsFile{
size: size,
file: file,
}
jf.resetReaderBuffer()
return jf, nil
}
type jsFile struct {
size int64
file *jsCallFile
whence int
reader io.Reader
}
func (j *jsFile) resetReaderBuffer() {
j.reader = bufio.NewReaderSize(&reader{fn: j.read}, readBufferSize)
}
func (j *jsFile) read(p []byte) (n int, err error) {
length := len(p)
if length == 0 {
return 0, errors.New("read buffer is empty")
}
if j.whence >= int(j.size) {
return 0, io.EOF
}
if j.whence+length > int(j.size) {
length = int(j.size) - j.whence
}
data, err := j.file.Read(int64(j.whence), int64(length))
if err != nil {
return 0, err
}
if len(data) > len(p) {
return 0, errors.New("js read data > length")
}
j.whence += len(data)
copy(p, data)
return len(data), nil
}
func (j *jsFile) Read(p []byte) (n int, err error) {
return j.reader.Read(p)
}
func (j *jsFile) Close() error {
return j.file.Close()
}
func (j *jsFile) Size() int64 {
return j.size
}
func (j *jsFile) StartSeek(whence int) error {
if whence < 0 || whence > int(j.size) {
return errors.New("seek whence is out of range")
}
j.whence = whence
j.resetReaderBuffer()
return nil
}
type reader struct {
fn func(p []byte) (n int, err error)
}
func (r *reader) Read(p []byte) (n int, err error) {
return r.fn(p)
}
type jsCallFile struct {
uuid string
}
func newJsCallFile(uuid string) *jsCallFile {
return &jsCallFile{uuid: uuid}
}
func (j *jsCallFile) Open() (int64, error) {
return WasmOpen(j.uuid)
}
func (j *jsCallFile) Read(offset int64, length int64) ([]byte, error) {
return WasmRead(j.uuid, offset, length)
}
func (j *jsCallFile) Close() error {
return WasmClose(j.uuid)
}
func WasmOpen(uuid string) (int64, error) {
result, err := exec.Exec(uuid)
if err != nil {
return 0, err
}
if v, ok := result.(float64); ok {
size := int64(v)
if size < 0 {
return 0, errors.New("file size < 0")
}
return size, nil
}
return 0, exec.ErrType
}
func WasmRead(uuid string, offset int64, length int64) ([]byte, error) {
result, err := exec.Exec(uuid, offset, length)
if err != nil {
return nil, err
} else {
if v, ok := result.(js.Value); ok {
return exec.ExtractArrayBuffer(v), nil
} else {
return nil, exec.ErrType
}
}
}
func WasmClose(uuid string) error {
_, err := exec.Exec(uuid)
return err
}

View File

@@ -0,0 +1,40 @@
package file
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/pkg/ccontext"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"path/filepath"
"testing"
)
func TestUpload(t *testing.T) {
conf := &ccontext.GlobalConfig{
UserID: `4931176757`,
Token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOiI0OTMxMTc2NzU3IiwiUGxhdGZvcm1JRCI6MSwiZXhwIjoxNzA3MTE0MjIyLCJuYmYiOjE2OTkzMzc5MjIsImlhdCI6MTY5OTMzODIyMn0.AyNvrMGEdXD5rkvn7ZLHCNs-lNbDCb2otn97yLXia5Y`,
IMConfig: sdk_struct.IMConfig{
ApiAddr: `http://203.56.175.233:10002`,
},
}
ctx := ccontext.WithInfo(context.WithValue(context.Background(), "operationID", "OP123456"), conf)
f := NewFile(nil, conf.UserID)
//fp := `C:\Users\openIM\Desktop\微信截图_20231025170714.png`
//fp := `C:\Users\openIM\Desktop\my_image (2).tar`
//fp := `C:\Users\openIM\Desktop\1234.zip`
//fp := `C:\Users\openIM\Desktop\openIM.wasm`
//fp := `C:\Users\openIM\Desktop\ubuntu.7z`
//fp := `C:\Users\openIM\Desktop\log2023-10-31.log`
fp := `C:\Users\openIM\Desktop\protoc.zip`
resp, err := f.UploadFile(ctx, &UploadFileReq{
Filepath: fp,
Name: filepath.Base(fp),
Cause: "test",
}, nil)
if err != nil {
t.Fatal("failed", err)
}
t.Log("success", resp.URL)
}

View File

@@ -0,0 +1,43 @@
// 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 file
import (
"crypto/md5"
"encoding/hex"
"hash"
"io"
)
func NewMd5Reader(r io.Reader) *Md5Reader {
return &Md5Reader{h: md5.New(), r: r}
}
type Md5Reader struct {
h hash.Hash
r io.Reader
}
func (r *Md5Reader) Read(p []byte) (n int, err error) {
n, err = r.r.Read(p)
if err == nil && n > 0 {
r.h.Write(p[:n])
}
return
}
func (r *Md5Reader) Md5() string {
return hex.EncodeToString(r.h.Sum(nil))
}

View File

@@ -0,0 +1,44 @@
// 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 file
import (
"io"
)
func NewProgressReader(r io.Reader, fn func(current int64)) io.Reader {
if r == nil || fn == nil {
return r
}
return &Reader{
r: r,
fn: fn,
}
}
type Reader struct {
r io.Reader
read int64
fn func(current int64)
}
func (r *Reader) Read(p []byte) (n int, err error) {
n, err = r.r.Read(p)
if err == nil && n > 0 {
r.read += int64(n)
r.fn(r.read)
}
return n, err
}

View File

@@ -0,0 +1,576 @@
// 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 file
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"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/tools/errs"
"io"
"net/http"
"net/url"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/openimsdk/protocol/third"
"github.com/openimsdk/tools/log"
)
type UploadFileReq struct {
Filepath string `json:"filepath"`
Name string `json:"name"`
ContentType string `json:"contentType"`
Cause string `json:"cause"`
Uuid string `json:"uuid"`
}
type UploadFileResp struct {
URL string `json:"url"`
}
type partInfo struct {
ContentType string
PartSize int64
PartNum int
FileMd5 string
PartMd5 string
PartSizes []int64
PartMd5s []string
}
func NewFile(database db_interface.DataBase, loginUserID string) *File {
return &File{database: database, loginUserID: loginUserID, confLock: &sync.Mutex{}, mapLocker: &sync.Mutex{}, uploading: make(map[string]*lockInfo)}
}
type File struct {
database db_interface.DataBase
loginUserID string
confLock sync.Locker
partLimit *third.PartLimitResp
mapLocker sync.Locker
uploading map[string]*lockInfo
}
type lockInfo struct {
count int32
locker sync.Locker
}
func (f *File) lockHash(hash string) {
f.mapLocker.Lock()
locker, ok := f.uploading[hash]
if !ok {
locker = &lockInfo{count: 0, locker: &sync.Mutex{}}
f.uploading[hash] = locker
}
atomic.AddInt32(&locker.count, 1)
f.mapLocker.Unlock()
locker.locker.Lock()
}
func (f *File) unlockHash(hash string) {
f.mapLocker.Lock()
locker, ok := f.uploading[hash]
if !ok {
f.mapLocker.Unlock()
return
}
if atomic.AddInt32(&locker.count, -1) == 0 {
delete(f.uploading, hash)
}
f.mapLocker.Unlock()
locker.locker.Unlock()
}
func (f *File) UploadFile(ctx context.Context, req *UploadFileReq, cb UploadFileCallback) (*UploadFileResp, error) {
if cb == nil {
cb = emptyUploadCallback{}
}
if req.Name == "" {
return nil, errors.New("name is empty")
}
if req.Name[0] == '/' {
req.Name = req.Name[1:]
}
if prefix := f.loginUserID + "/"; !strings.HasPrefix(req.Name, prefix) {
req.Name = prefix + req.Name
}
file, err := Open(req)
if err != nil {
return nil, err
}
defer file.Close()
fileSize := file.Size()
cb.Open(fileSize)
info, err := f.getPartInfo(ctx, file, fileSize, cb)
if err != nil {
return nil, err
}
if req.ContentType == "" {
req.ContentType = info.ContentType
}
partSize := info.PartSize
partSizes := info.PartSizes
partMd5s := info.PartMd5s
partMd5Val := info.PartMd5
if err := file.StartSeek(0); err != nil {
return nil, err
}
f.lockHash(partMd5Val)
defer f.unlockHash(partMd5Val)
maxParts := 20
if maxParts > len(partSizes) {
maxParts = len(partSizes)
}
uploadInfo, err := f.getUpload(ctx, &third.InitiateMultipartUploadReq{
Hash: partMd5Val,
Size: fileSize,
PartSize: partSize,
MaxParts: int32(maxParts), // 一次性获取签名数量
Cause: req.Cause,
Name: req.Name,
ContentType: req.ContentType,
})
if err != nil {
return nil, err
}
if uploadInfo.Resp.Upload == nil {
cb.Complete(fileSize, uploadInfo.Resp.Url, 0)
return &UploadFileResp{
URL: uploadInfo.Resp.Url,
}, nil
}
if uploadInfo.Resp.Upload.PartSize != partSize {
f.cleanPartLimit()
return nil, fmt.Errorf("part fileSize not match, expect %d, got %d", partSize, uploadInfo.Resp.Upload.PartSize)
}
cb.UploadID(uploadInfo.Resp.Upload.UploadID)
uploadedSize := fileSize
for i := 0; i < len(partSizes); i++ {
if !uploadInfo.Bitmap.Get(i) {
uploadedSize -= partSizes[i]
}
}
continueUpload := uploadedSize > 0
for i, currentPartSize := range partSizes {
partNumber := int32(i + 1)
md5Reader := NewMd5Reader(io.LimitReader(file, currentPartSize))
if uploadInfo.Bitmap.Get(i) {
if _, err := io.Copy(io.Discard, md5Reader); err != nil {
return nil, err
}
} else {
reader := NewProgressReader(md5Reader, func(current int64) {
cb.UploadComplete(fileSize, uploadedSize+current, uploadedSize)
})
urlval, header, err := uploadInfo.GetPartSign(ctx, partNumber)
if err != nil {
return nil, err
}
if err := f.doPut(ctx, http.DefaultClient, urlval, header, reader, currentPartSize); err != nil {
log.ZError(ctx, "doPut", err, "partMd5Val", partMd5Val, "name", req.Name, "partNumber", partNumber)
return nil, err
}
uploadedSize += currentPartSize
if uploadInfo.DBInfo != nil && uploadInfo.Bitmap != nil {
uploadInfo.Bitmap.Set(i)
uploadInfo.DBInfo.UploadInfo = base64.StdEncoding.EncodeToString(uploadInfo.Bitmap.Serialize())
if err := f.database.UpdateUpload(ctx, uploadInfo.DBInfo); err != nil {
log.ZError(ctx, "SetUploadPartPush", err, "partMd5Val", partMd5Val, "name", req.Name, "partNumber", partNumber)
}
}
}
md5val := md5Reader.Md5()
if md5val != partMd5s[i] {
return nil, fmt.Errorf("upload part %d failed, md5 not match, expect %s, got %s", i, partMd5s[i], md5val)
}
cb.UploadPartComplete(i, currentPartSize, partMd5s[i])
log.ZDebug(ctx, "upload part success", "partMd5Val", md5val, "name", req.Name, "partNumber", partNumber)
}
log.ZDebug(ctx, "upload all part success", "partHash", partMd5Val, "name", req.Name)
resp, err := f.completeMultipartUpload(ctx, &third.CompleteMultipartUploadReq{
UploadID: uploadInfo.Resp.Upload.UploadID,
Parts: partMd5s,
Name: req.Name,
ContentType: req.ContentType,
Cause: req.Cause,
})
if err != nil {
return nil, err
}
typ := 1
if continueUpload {
typ++
}
cb.Complete(fileSize, resp.Url, typ)
if uploadInfo.DBInfo != nil {
if err := f.database.DeleteUpload(ctx, info.PartMd5); err != nil {
log.ZError(ctx, "DeleteUpload", err, "partMd5Val", info.PartMd5, "name", req.Name)
}
}
return &UploadFileResp{
URL: resp.Url,
}, nil
}
func (f *File) cleanPartLimit() {
f.confLock.Lock()
defer f.confLock.Unlock()
f.partLimit = nil
}
func (f *File) initiateMultipartUploadResp(ctx context.Context, req *third.InitiateMultipartUploadReq) (*third.InitiateMultipartUploadResp, error) {
return util.CallApi[third.InitiateMultipartUploadResp](ctx, constant.ObjectInitiateMultipartUpload, req)
}
func (f *File) authSign(ctx context.Context, req *third.AuthSignReq) (*third.AuthSignResp, error) {
if len(req.PartNumbers) == 0 {
return nil, errs.ErrArgs.WrapMsg("partNumbers is empty")
}
return util.CallApi[third.AuthSignResp](ctx, constant.ObjectAuthSign, req)
}
func (f *File) completeMultipartUpload(ctx context.Context, req *third.CompleteMultipartUploadReq) (*third.CompleteMultipartUploadResp, error) {
return util.CallApi[third.CompleteMultipartUploadResp](ctx, constant.ObjectCompleteMultipartUpload, req)
}
func (f *File) getPartNum(fileSize int64, partSize int64) int {
partNum := fileSize / partSize
if fileSize%partSize != 0 {
partNum++
}
return int(partNum)
}
func (f *File) partSize(ctx context.Context, size int64) (int64, error) {
f.confLock.Lock()
defer f.confLock.Unlock()
if f.partLimit == nil {
resp, err := util.CallApi[third.PartLimitResp](ctx, constant.ObjectPartLimit, &third.PartLimitReq{})
if err != nil {
return 0, err
}
f.partLimit = resp
}
if size <= 0 {
return 0, errors.New("size must be greater than 0")
}
if size > f.partLimit.MaxPartSize*int64(f.partLimit.MaxNumSize) {
return 0, fmt.Errorf("size must be less than %db", f.partLimit.MaxPartSize*int64(f.partLimit.MaxNumSize))
}
if size <= f.partLimit.MinPartSize*int64(f.partLimit.MaxNumSize) {
return f.partLimit.MinPartSize, nil
}
partSize := size / int64(f.partLimit.MaxNumSize)
if size%int64(f.partLimit.MaxNumSize) != 0 {
partSize++
}
return partSize, nil
}
func (f *File) accessURL(ctx context.Context, req *third.AccessURLReq) (*third.AccessURLResp, error) {
return util.CallApi[third.AccessURLResp](ctx, constant.ObjectAccessURL, req)
}
func (f *File) doHttpReq(req *http.Request) ([]byte, *http.Response, error) {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
return data, resp, nil
}
func (f *File) partMD5(parts []string) string {
s := strings.Join(parts, ",")
md5Sum := md5.Sum([]byte(s))
return hex.EncodeToString(md5Sum[:])
}
type AuthSignParts struct {
Sign *third.SignPart
Times []time.Time
}
type UploadInfo struct {
PartNum int
Bitmap *Bitmap
DBInfo *model_struct.LocalUpload
Resp *third.InitiateMultipartUploadResp
//Signs *AuthSignParts
CreateTime time.Time
BatchSignNum int32
f *File
}
func (u *UploadInfo) getIndex(partNumber int32) int {
if u.Resp.Upload.Sign == nil {
return -1
} else {
if u.CreateTime.IsZero() {
return -1
} else {
if time.Since(u.CreateTime) > time.Minute {
return -1
}
}
}
for i, part := range u.Resp.Upload.Sign.Parts {
if part.PartNumber == partNumber {
return i
}
}
return -1
}
func (u *UploadInfo) buildRequest(i int) (*url.URL, http.Header, error) {
sign := u.Resp.Upload.Sign
part := sign.Parts[i]
rawURL := sign.Url
if part.Url != "" {
rawURL = part.Url
}
urlval, err := url.Parse(rawURL)
if err != nil {
return nil, nil, err
}
if len(sign.Query)+len(part.Query) > 0 {
query := urlval.Query()
for i := range sign.Query {
v := sign.Query[i]
query[v.Key] = v.Values
}
for i := range part.Query {
v := part.Query[i]
query[v.Key] = v.Values
}
urlval.RawQuery = query.Encode()
}
header := make(http.Header)
for i := range sign.Header {
v := sign.Header[i]
header[v.Key] = v.Values
}
for i := range part.Header {
v := part.Header[i]
header[v.Key] = v.Values
}
return urlval, header, nil
}
func (u *UploadInfo) GetPartSign(ctx context.Context, partNumber int32) (*url.URL, http.Header, error) {
if partNumber < 1 || int(partNumber) > u.PartNum {
return nil, nil, errors.New("invalid partNumber")
}
if index := u.getIndex(partNumber); index >= 0 {
return u.buildRequest(index)
}
partNumbers := make([]int32, 0, u.BatchSignNum)
for i := int32(0); i < u.BatchSignNum; i++ {
if int(partNumber+i) > u.PartNum {
break
}
partNumbers = append(partNumbers, partNumber+i)
}
authSignResp, err := u.f.authSign(ctx, &third.AuthSignReq{
UploadID: u.Resp.Upload.UploadID,
PartNumbers: partNumbers,
})
if err != nil {
return nil, nil, err
}
u.Resp.Upload.Sign.Url = authSignResp.Url
u.Resp.Upload.Sign.Query = authSignResp.Query
u.Resp.Upload.Sign.Header = authSignResp.Header
u.Resp.Upload.Sign.Parts = authSignResp.Parts
u.CreateTime = time.Now()
index := u.getIndex(partNumber)
if index < 0 {
return nil, nil, errs.ErrInternalServer.WrapMsg("server part sign invalid")
}
return u.buildRequest(index)
}
func (f *File) getUpload(ctx context.Context, req *third.InitiateMultipartUploadReq) (*UploadInfo, error) {
partNum := f.getPartNum(req.Size, req.PartSize)
var bitmap *Bitmap
if f.database != nil {
dbUpload, err := f.database.GetUpload(ctx, req.Hash)
if err == nil {
bitmapBytes, err := base64.StdEncoding.DecodeString(dbUpload.UploadInfo)
if err != nil || len(bitmapBytes) == 0 || partNum <= 1 || dbUpload.ExpireTime-3600*1000 < time.Now().UnixMilli() {
if err := f.database.DeleteUpload(ctx, req.Hash); err != nil {
return nil, err
}
dbUpload = nil
}
if dbUpload == nil {
bitmap = NewBitmap(partNum)
} else {
bitmap = ParseBitmap(bitmapBytes, partNum)
}
tUpInfo := &third.UploadInfo{
PartSize: req.PartSize,
Sign: &third.AuthSignParts{},
}
if dbUpload != nil {
tUpInfo.UploadID = dbUpload.UploadID
tUpInfo.ExpireTime = dbUpload.ExpireTime
}
return &UploadInfo{
PartNum: partNum,
Bitmap: bitmap,
DBInfo: dbUpload,
Resp: &third.InitiateMultipartUploadResp{
Upload: tUpInfo,
},
BatchSignNum: req.MaxParts,
f: f,
}, nil
}
log.ZError(ctx, "get upload db", err, "pratsMd5", req.Hash)
}
resp, err := f.initiateMultipartUploadResp(ctx, req)
if err != nil {
return nil, err
}
if resp.Upload == nil {
return &UploadInfo{
Resp: resp,
}, nil
}
bitmap = NewBitmap(partNum)
var dbUpload *model_struct.LocalUpload
if f.database != nil {
dbUpload = &model_struct.LocalUpload{
PartHash: req.Hash,
UploadID: resp.Upload.UploadID,
UploadInfo: base64.StdEncoding.EncodeToString(bitmap.Serialize()),
ExpireTime: resp.Upload.ExpireTime,
CreateTime: time.Now().UnixMilli(),
}
if err := f.database.InsertUpload(ctx, dbUpload); err != nil {
log.ZError(ctx, "insert upload db", err, "pratsHash", req.Hash, "name", req.Name)
}
}
if req.MaxParts >= 0 && len(resp.Upload.Sign.Parts) != int(req.MaxParts) {
resp.Upload.Sign.Parts = nil
}
return &UploadInfo{
PartNum: partNum,
Bitmap: bitmap,
DBInfo: dbUpload,
Resp: resp,
CreateTime: time.Now(),
BatchSignNum: req.MaxParts,
f: f,
}, nil
}
func (f *File) doPut(ctx context.Context, client *http.Client, url *url.URL, header http.Header, reader io.Reader, size int64) error {
rawURL := url.String()
req, err := http.NewRequestWithContext(ctx, http.MethodPut, rawURL, reader)
if err != nil {
return err
}
for key := range header {
req.Header[key] = header[key]
}
req.ContentLength = size
log.ZDebug(ctx, "do put req", "url", rawURL, "contentLength", size, "header", req.Header)
resp, err := client.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
log.ZDebug(ctx, "do put resp status", "url", rawURL, "status", resp.Status, "contentLength", resp.ContentLength, "header", resp.Header)
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
log.ZDebug(ctx, "do put resp body", "url", rawURL, "body", string(body))
if resp.StatusCode/200 != 1 {
return fmt.Errorf("PUT %s failed, status code %d, body %s", rawURL, resp.StatusCode, string(body))
}
return nil
}
func (f *File) getPartInfo(ctx context.Context, r io.Reader, fileSize int64, cb UploadFileCallback) (*partInfo, error) {
partSize, err := f.partSize(ctx, fileSize)
if err != nil {
return nil, err
}
partNum := int(fileSize / partSize)
if fileSize%partSize != 0 {
partNum++
}
cb.PartSize(partSize, partNum)
partSizes := make([]int64, partNum)
for i := 0; i < partNum; i++ {
partSizes[i] = partSize
}
partSizes[partNum-1] = fileSize - partSize*(int64(partNum)-1)
partMd5s := make([]string, partNum)
buf := make([]byte, 1024*8)
fileMd5 := md5.New()
var contentType string
for i := 0; i < partNum; i++ {
h := md5.New()
r := io.LimitReader(r, partSize)
for {
if n, err := r.Read(buf); err == nil {
if contentType == "" {
contentType = http.DetectContentType(buf[:n])
}
h.Write(buf[:n])
fileMd5.Write(buf[:n])
} else if err == io.EOF {
break
} else {
return nil, err
}
}
partMd5s[i] = hex.EncodeToString(h.Sum(nil))
cb.HashPartProgress(i, partSizes[i], partMd5s[i])
}
partMd5Val := f.partMD5(partMd5s)
fileMd5val := hex.EncodeToString(fileMd5.Sum(nil))
cb.HashPartComplete(f.partMD5(partMd5s), hex.EncodeToString(fileMd5.Sum(nil)))
return &partInfo{
ContentType: contentType,
PartSize: partSize,
PartNum: partNum,
FileMd5: fileMd5val,
PartMd5: partMd5Val,
PartSizes: partSizes,
PartMd5s: partMd5s,
}, nil
}

View File

@@ -0,0 +1,71 @@
// 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 friend
import (
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/protocol/sdkws"
)
func ServerFriendRequestToLocalFriendRequest(info *sdkws.FriendRequest) *model_struct.LocalFriendRequest {
return &model_struct.LocalFriendRequest{
FromUserID: info.FromUserID,
FromNickname: info.FromNickname,
FromFaceURL: info.FromFaceURL,
//FromGender: info.FromGender,
ToUserID: info.ToUserID,
ToNickname: info.ToNickname,
ToFaceURL: info.ToFaceURL,
//ToGender: info.ToGender,
HandleResult: info.HandleResult,
ReqMsg: info.ReqMsg,
CreateTime: info.CreateTime,
HandlerUserID: info.HandlerUserID,
HandleMsg: info.HandleMsg,
HandleTime: info.HandleTime,
Ex: info.Ex,
//AttachedInfo: info.AttachedInfo,
}
}
func ServerFriendToLocalFriend(info *sdkws.FriendInfo) *model_struct.LocalFriend {
return &model_struct.LocalFriend{
OwnerUserID: info.OwnerUserID,
FriendUserID: info.FriendUser.UserID,
Remark: info.Remark,
CreateTime: info.CreateTime,
AddSource: info.AddSource,
OperatorUserID: info.OperatorUserID,
Nickname: info.FriendUser.Nickname,
FaceURL: info.FriendUser.FaceURL,
Ex: info.Ex,
//AttachedInfo: info.FriendUser.AttachedInfo,
IsPinned: info.IsPinned,
}
}
func ServerBlackToLocalBlack(info *sdkws.BlackInfo) *model_struct.LocalBlack {
return &model_struct.LocalBlack{
OwnerUserID: info.OwnerUserID,
BlockUserID: info.BlackUserInfo.UserID,
CreateTime: info.CreateTime,
AddSource: info.AddSource,
OperatorUserID: info.OperatorUserID,
Nickname: info.BlackUserInfo.Nickname,
FaceURL: info.BlackUserInfo.FaceURL,
Ex: info.Ex,
//AttachedInfo: info.FriendUser.AttachedInfo,
}
}

View File

@@ -0,0 +1,199 @@
// Copyright 2021 OpenIM Corporation
//
// 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 friend
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/internal/user"
"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/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/page"
"github.com/openimsdk/openim-sdk-core/v3/pkg/syncer"
friend "github.com/openimsdk/protocol/relation"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func NewFriend(loginUserID string, db db_interface.DataBase, user *user.User, conversationCh chan common.Cmd2Value) *Friend {
f := &Friend{loginUserID: loginUserID, db: db, user: user, conversationCh: conversationCh}
f.initSyncer()
return f
}
type Friend struct {
friendListener open_im_sdk_callback.OnFriendshipListenerSdk
loginUserID string
db db_interface.DataBase
user *user.User
friendSyncer *syncer.Syncer[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string]
blockSyncer *syncer.Syncer[*model_struct.LocalBlack, syncer.NoResp, [2]string]
requestRecvSyncer *syncer.Syncer[*model_struct.LocalFriendRequest, syncer.NoResp, [2]string]
requestSendSyncer *syncer.Syncer[*model_struct.LocalFriendRequest, syncer.NoResp, [2]string]
conversationCh chan common.Cmd2Value
listenerForService open_im_sdk_callback.OnListenerForService
}
func (f *Friend) initSyncer() {
f.friendSyncer = syncer.New2[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](
syncer.WithInsert[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(ctx context.Context, value *model_struct.LocalFriend) error {
return f.db.InsertFriend(ctx, value)
}),
syncer.WithDelete[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(ctx context.Context, value *model_struct.LocalFriend) error {
return f.db.DeleteFriendDB(ctx, value.FriendUserID)
}),
syncer.WithUpdate[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(ctx context.Context, server, local *model_struct.LocalFriend) error {
return f.db.UpdateFriend(ctx, server)
}),
syncer.WithUUID[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(value *model_struct.LocalFriend) [2]string {
return [...]string{value.OwnerUserID, value.FriendUserID}
}),
syncer.WithNotice[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(ctx context.Context, state int, server, local *model_struct.LocalFriend) error {
switch state {
case syncer.Insert:
f.friendListener.OnFriendAdded(*server)
case syncer.Delete:
log.ZDebug(ctx, "syncer OnFriendDeleted", "local", local)
f.friendListener.OnFriendDeleted(*local)
case syncer.Update:
f.friendListener.OnFriendInfoChanged(*server)
if local.Nickname != server.Nickname || local.FaceURL != server.FaceURL || local.Remark != server.Remark {
if server.Remark != "" {
server.Nickname = server.Remark
}
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{
Action: constant.UpdateConFaceUrlAndNickName,
Args: common.SourceIDAndSessionType{
SourceID: server.FriendUserID,
SessionType: constant.SingleChatType,
FaceURL: server.FaceURL,
Nickname: server.Nickname,
},
}, f.conversationCh)
_ = common.TriggerCmdUpdateMessage(ctx, common.UpdateMessageNode{
Action: constant.UpdateMsgFaceUrlAndNickName,
Args: common.UpdateMessageInfo{
SessionType: constant.SingleChatType,
UserID: server.FriendUserID,
FaceURL: server.FaceURL,
Nickname: server.Nickname,
},
}, f.conversationCh)
}
}
return nil
}),
syncer.WithBatchInsert[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(ctx context.Context, values []*model_struct.LocalFriend) error {
log.ZDebug(ctx, "BatchInsertFriend", "length", len(values))
return f.db.BatchInsertFriend(ctx, values)
}),
syncer.WithDeleteAll[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(ctx context.Context, _ string) error {
return f.db.DeleteAllFriend(ctx)
}),
syncer.WithBatchPageReq[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(entityID string) page.PageReq {
return &friend.GetPaginationFriendsReq{UserID: entityID,
Pagination: &sdkws.RequestPagination{ShowNumber: 100}}
}),
syncer.WithBatchPageRespConvertFunc[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(resp *friend.GetPaginationFriendsResp) []*model_struct.LocalFriend {
return datautil.Batch(ServerFriendToLocalFriend, resp.FriendsInfo)
}),
syncer.WithReqApiRouter[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](constant.GetFriendListRouter),
)
f.blockSyncer = syncer.New[*model_struct.LocalBlack, syncer.NoResp, [2]string](func(ctx context.Context, value *model_struct.LocalBlack) error {
return f.db.InsertBlack(ctx, value)
}, func(ctx context.Context, value *model_struct.LocalBlack) error {
return f.db.DeleteBlack(ctx, value.BlockUserID)
}, func(ctx context.Context, server *model_struct.LocalBlack, local *model_struct.LocalBlack) error {
return f.db.UpdateBlack(ctx, server)
}, func(value *model_struct.LocalBlack) [2]string {
return [...]string{value.OwnerUserID, value.BlockUserID}
}, nil, func(ctx context.Context, state int, server, local *model_struct.LocalBlack) error {
switch state {
case syncer.Insert:
f.friendListener.OnBlackAdded(*server)
case syncer.Delete:
f.friendListener.OnBlackDeleted(*local)
}
return nil
})
f.requestRecvSyncer = syncer.New[*model_struct.LocalFriendRequest, syncer.NoResp, [2]string](func(ctx context.Context, value *model_struct.LocalFriendRequest) error {
return f.db.InsertFriendRequest(ctx, value)
}, func(ctx context.Context, value *model_struct.LocalFriendRequest) error {
return f.db.DeleteFriendRequestBothUserID(ctx, value.FromUserID, value.ToUserID)
}, func(ctx context.Context, server *model_struct.LocalFriendRequest, local *model_struct.LocalFriendRequest) error {
return f.db.UpdateFriendRequest(ctx, server)
}, func(value *model_struct.LocalFriendRequest) [2]string {
return [...]string{value.FromUserID, value.ToUserID}
}, nil, func(ctx context.Context, state int, server, local *model_struct.LocalFriendRequest) error {
switch state {
case syncer.Insert:
f.friendListener.OnFriendApplicationAdded(*server)
case syncer.Delete:
f.friendListener.OnFriendApplicationDeleted(*local)
case syncer.Update:
switch server.HandleResult {
case constant.FriendResponseAgree:
f.friendListener.OnFriendApplicationAccepted(*server)
case constant.FriendResponseRefuse:
f.friendListener.OnFriendApplicationRejected(*server)
case constant.FriendResponseDefault:
f.friendListener.OnFriendApplicationAdded(*server)
}
}
return nil
})
f.requestSendSyncer = syncer.New[*model_struct.LocalFriendRequest, syncer.NoResp, [2]string](func(ctx context.Context, value *model_struct.LocalFriendRequest) error {
return f.db.InsertFriendRequest(ctx, value)
}, func(ctx context.Context, value *model_struct.LocalFriendRequest) error {
return f.db.DeleteFriendRequestBothUserID(ctx, value.FromUserID, value.ToUserID)
}, func(ctx context.Context, server *model_struct.LocalFriendRequest, local *model_struct.LocalFriendRequest) error {
return f.db.UpdateFriendRequest(ctx, server)
}, func(value *model_struct.LocalFriendRequest) [2]string {
return [...]string{value.FromUserID, value.ToUserID}
}, nil, func(ctx context.Context, state int, server, local *model_struct.LocalFriendRequest) error {
switch state {
case syncer.Insert:
f.friendListener.OnFriendApplicationAdded(*server)
case syncer.Delete:
f.friendListener.OnFriendApplicationDeleted(*local)
case syncer.Update:
switch server.HandleResult {
case constant.FriendResponseAgree:
f.friendListener.OnFriendApplicationAccepted(*server)
case constant.FriendResponseRefuse:
f.friendListener.OnFriendApplicationRejected(*server)
}
}
return nil
})
}
func (f *Friend) Db() db_interface.DataBase {
return f.db
}
func (f *Friend) SetListener(listener func() open_im_sdk_callback.OnFriendshipListener) {
f.friendListener = open_im_sdk_callback.NewOnFriendshipListenerSdk(listener)
}
func (f *Friend) SetListenerForService(listener open_im_sdk_callback.OnListenerForService) {
f.listenerForService = listener
}

View File

@@ -0,0 +1,33 @@
package friend
import (
"crypto/md5"
"encoding/binary"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/protocol/constant"
"github.com/openimsdk/tools/utils/datautil"
"strconv"
"strings"
)
func (f *Friend) CalculateHash(friends []*model_struct.LocalFriend) uint64 {
datautil.SortAny(friends, func(a, b *model_struct.LocalFriend) bool {
return a.CreateTime > b.CreateTime
})
if len(friends) > constant.MaxSyncPullNumber {
friends = friends[:constant.MaxSyncPullNumber]
}
hashStr := strings.Join(datautil.Slice(friends, func(f *model_struct.LocalFriend) string {
return strings.Join([]string{
f.FriendUserID,
f.Remark,
strconv.FormatInt(f.CreateTime, 10),
strconv.Itoa(int(f.AddSource)),
f.OperatorUserID,
f.Ex,
strconv.FormatBool(f.IsPinned),
}, ",")
}), ";")
sum := md5.Sum([]byte(hashStr))
return binary.BigEndian.Uint64(sum[:])
}

View File

@@ -0,0 +1,136 @@
// 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 friend
import (
"context"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (f *Friend) DoNotification(ctx context.Context, msg *sdkws.MsgData) {
go func() {
if err := f.doNotification(ctx, msg); err != nil {
log.ZError(ctx, "doNotification error", err, "msg", msg)
}
}()
}
func (f *Friend) doNotification(ctx context.Context, msg *sdkws.MsgData) error {
switch msg.ContentType {
case constant.FriendApplicationNotification:
tips := sdkws.FriendApplicationTips{}
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
return f.SyncBothFriendRequest(ctx,
tips.FromToUserID.FromUserID, tips.FromToUserID.ToUserID)
case constant.FriendApplicationApprovedNotification:
var tips sdkws.FriendApplicationApprovedTips
err := utils.UnmarshalNotificationElem(msg.Content, &tips)
if err != nil {
return err
}
if tips.FromToUserID.FromUserID == f.loginUserID {
err = f.SyncFriends(ctx, []string{tips.FromToUserID.ToUserID})
} else if tips.FromToUserID.ToUserID == f.loginUserID {
err = f.SyncFriends(ctx, []string{tips.FromToUserID.FromUserID})
}
if err != nil {
return err
}
return f.SyncBothFriendRequest(ctx, tips.FromToUserID.FromUserID, tips.FromToUserID.ToUserID)
case constant.FriendApplicationRejectedNotification:
var tips sdkws.FriendApplicationRejectedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
return f.SyncBothFriendRequest(ctx, tips.FromToUserID.FromUserID, tips.FromToUserID.ToUserID)
case constant.FriendAddedNotification:
var tips sdkws.FriendAddedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.Friend != nil && tips.Friend.FriendUser != nil {
if tips.Friend.FriendUser.UserID == f.loginUserID {
return f.SyncFriends(ctx, []string{tips.Friend.OwnerUserID})
} else if tips.Friend.OwnerUserID == f.loginUserID {
return f.SyncFriends(ctx, []string{tips.Friend.FriendUser.UserID})
}
}
case constant.FriendDeletedNotification:
var tips sdkws.FriendDeletedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.FromToUserID != nil {
if tips.FromToUserID.FromUserID == f.loginUserID {
return f.deleteFriend(ctx, tips.FromToUserID.ToUserID)
}
}
case constant.FriendRemarkSetNotification:
var tips sdkws.FriendInfoChangedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.FromToUserID != nil {
if tips.FromToUserID.FromUserID == f.loginUserID {
return f.SyncFriends(ctx, []string{tips.FromToUserID.ToUserID})
}
}
case constant.FriendInfoUpdatedNotification:
var tips sdkws.UserInfoUpdatedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.UserID != f.loginUserID {
return f.SyncFriends(ctx, []string{tips.UserID})
}
case constant.BlackAddedNotification:
var tips sdkws.BlackAddedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.FromToUserID.FromUserID == f.loginUserID {
return f.SyncAllBlackList(ctx)
}
case constant.BlackDeletedNotification:
var tips sdkws.BlackDeletedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.FromToUserID.FromUserID == f.loginUserID {
return f.SyncAllBlackList(ctx)
}
case constant.FriendsInfoUpdateNotification:
var tips sdkws.FriendsInfoUpdateTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.FromToUserID.ToUserID == f.loginUserID {
return f.SyncFriends(ctx, tips.FriendIDs)
}
default:
return fmt.Errorf("type failed %d", msg.ContentType)
}
return nil
}

View File

@@ -0,0 +1,344 @@
// Copyright 2021 OpenIM Corporation
//
// 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 friend
import (
"context"
friend "github.com/openimsdk/protocol/relation"
"github.com/openimsdk/protocol/wrapperspb"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/datafetcher"
"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/sdkerrs"
"github.com/openimsdk/openim-sdk-core/v3/pkg/server_api_params"
"github.com/openimsdk/tools/log"
)
func (f *Friend) GetSpecifiedFriendsInfo(ctx context.Context, friendUserIDList []string) ([]*server_api_params.FullUserInfo, error) {
datafetcher := datafetcher.NewDataFetcher(
f.db,
f.friendListTableName(),
f.loginUserID,
func(localFriend *model_struct.LocalFriend) string {
return localFriend.FriendUserID
},
func(ctx context.Context, values []*model_struct.LocalFriend) error {
return f.db.BatchInsertFriend(ctx, values)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalFriend, error) {
return f.db.GetFriendInfoList(ctx, userIDs)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalFriend, error) {
serverFriend, err := f.GetDesignatedFriends(ctx, userIDs)
if err != nil {
return nil, err
}
return datautil.Batch(ServerFriendToLocalFriend, serverFriend), nil
},
)
localFriendList, err := datafetcher.FetchMissingAndFillLocal(ctx, friendUserIDList)
if err != nil {
return nil, err
}
log.ZDebug(ctx, "GetDesignatedFriendsInfo", "localFriendList", localFriendList)
blackList, err := f.db.GetBlackInfoList(ctx, friendUserIDList)
if err != nil {
return nil, err
}
log.ZDebug(ctx, "GetDesignatedFriendsInfo", "blackList", blackList)
m := make(map[string]*model_struct.LocalBlack)
for i, black := range blackList {
m[black.BlockUserID] = blackList[i]
}
res := make([]*server_api_params.FullUserInfo, 0, len(localFriendList))
for _, localFriend := range localFriendList {
res = append(res, &server_api_params.FullUserInfo{
PublicInfo: nil,
FriendInfo: localFriend,
BlackInfo: m[localFriend.FriendUserID],
})
}
return res, nil
}
func (f *Friend) AddFriend(ctx context.Context, userIDReqMsg *friend.ApplyToAddFriendReq) error {
if userIDReqMsg.FromUserID == "" {
userIDReqMsg.FromUserID = f.loginUserID
}
if err := util.ApiPost(ctx, constant.AddFriendRouter, userIDReqMsg, nil); err != nil {
return err
}
return f.SyncAllFriendApplication(ctx)
}
func (f *Friend) GetFriendApplicationListAsRecipient(ctx context.Context) ([]*model_struct.LocalFriendRequest, error) {
return f.db.GetRecvFriendApplication(ctx)
}
func (f *Friend) GetFriendApplicationListAsApplicant(ctx context.Context) ([]*model_struct.LocalFriendRequest, error) {
return f.db.GetSendFriendApplication(ctx)
}
func (f *Friend) AcceptFriendApplication(ctx context.Context, userIDHandleMsg *sdk.ProcessFriendApplicationParams) error {
return f.RespondFriendApply(ctx, &friend.RespondFriendApplyReq{FromUserID: userIDHandleMsg.ToUserID, ToUserID: f.loginUserID, HandleResult: constant.FriendResponseAgree, HandleMsg: userIDHandleMsg.HandleMsg})
}
func (f *Friend) RefuseFriendApplication(ctx context.Context, userIDHandleMsg *sdk.ProcessFriendApplicationParams) error {
return f.RespondFriendApply(ctx, &friend.RespondFriendApplyReq{FromUserID: userIDHandleMsg.ToUserID, ToUserID: f.loginUserID, HandleResult: constant.FriendResponseRefuse, HandleMsg: userIDHandleMsg.HandleMsg})
}
func (f *Friend) RespondFriendApply(ctx context.Context, req *friend.RespondFriendApplyReq) error {
if req.ToUserID == "" {
req.ToUserID = f.loginUserID
}
if err := util.ApiPost(ctx, constant.AddFriendResponse, req, nil); err != nil {
return err
}
if req.HandleResult == constant.FriendResponseAgree {
_ = f.SyncFriends(ctx, []string{req.FromUserID})
}
_ = f.SyncAllFriendApplication(ctx)
return nil
// return f.SyncFriendApplication(ctx)
}
func (f *Friend) CheckFriend(ctx context.Context, friendUserIDList []string) ([]*server_api_params.UserIDResult, error) {
friendList, err := f.db.GetFriendInfoList(ctx, friendUserIDList)
if err != nil {
return nil, err
}
blackList, err := f.db.GetBlackInfoList(ctx, friendUserIDList)
if err != nil {
return nil, err
}
res := make([]*server_api_params.UserIDResult, 0, len(friendUserIDList))
for _, v := range friendUserIDList {
var r server_api_params.UserIDResult
isBlack := false
isFriend := false
for _, b := range blackList {
if v == b.BlockUserID {
isBlack = true
break
}
}
for _, f := range friendList {
if v == f.FriendUserID {
isFriend = true
break
}
}
r.UserID = v
if isFriend && !isBlack {
r.Result = 1
} else {
r.Result = 0
}
res = append(res, &r)
}
return res, nil
}
func (f *Friend) DeleteFriend(ctx context.Context, friendUserID string) error {
if err := util.ApiPost(ctx, constant.DeleteFriendRouter, &friend.DeleteFriendReq{OwnerUserID: f.loginUserID, FriendUserID: friendUserID}, nil); err != nil {
return err
}
return f.deleteFriend(ctx, friendUserID)
}
func (f *Friend) GetFriendList(ctx context.Context) ([]*server_api_params.FullUserInfo, error) {
localFriendList, err := f.db.GetAllFriendList(ctx)
if err != nil {
return nil, err
}
localBlackList, err := f.db.GetBlackListDB(ctx)
if err != nil {
return nil, err
}
m := make(map[string]*model_struct.LocalBlack)
for i, black := range localBlackList {
m[black.BlockUserID] = localBlackList[i]
}
res := make([]*server_api_params.FullUserInfo, 0, len(localFriendList))
for _, localFriend := range localFriendList {
res = append(res, &server_api_params.FullUserInfo{
PublicInfo: nil,
FriendInfo: localFriend,
BlackInfo: m[localFriend.FriendUserID],
})
}
return res, nil
}
func (f *Friend) GetFriendListPage(ctx context.Context, offset, count int32) ([]*server_api_params.FullUserInfo, error) {
dataFetcher := datafetcher.NewDataFetcher(
f.db,
f.friendListTableName(),
f.loginUserID,
func(localFriend *model_struct.LocalFriend) string {
return localFriend.FriendUserID
},
func(ctx context.Context, values []*model_struct.LocalFriend) error {
return f.db.BatchInsertFriend(ctx, values)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalFriend, error) {
return f.db.GetFriendInfoList(ctx, userIDs)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalFriend, error) {
serverFriend, err := f.GetDesignatedFriends(ctx, userIDs)
if err != nil {
return nil, err
}
return datautil.Batch(ServerFriendToLocalFriend, serverFriend), nil
},
)
localFriendList, err := dataFetcher.FetchWithPagination(ctx, int(offset), int(count))
if err != nil {
return nil, err
}
// don't need extra handle. only full pull.
localBlackList, err := f.db.GetBlackListDB(ctx)
if err != nil {
return nil, err
}
m := make(map[string]*model_struct.LocalBlack)
for i, black := range localBlackList {
m[black.BlockUserID] = localBlackList[i]
}
res := make([]*server_api_params.FullUserInfo, 0, len(localFriendList))
for _, localFriend := range localFriendList {
res = append(res, &server_api_params.FullUserInfo{
PublicInfo: nil,
FriendInfo: localFriend,
BlackInfo: m[localFriend.FriendUserID],
})
}
return res, nil
}
func (f *Friend) SearchFriends(ctx context.Context, param *sdk.SearchFriendsParam) ([]*sdk.SearchFriendItem, error) {
if len(param.KeywordList) == 0 || (!param.IsSearchNickname && !param.IsSearchUserID && !param.IsSearchRemark) {
return nil, sdkerrs.ErrArgs.WrapMsg("keyword is null or search field all false")
}
localFriendList, err := f.db.SearchFriendList(ctx, param.KeywordList[0], param.IsSearchUserID, param.IsSearchNickname, param.IsSearchRemark)
if err != nil {
return nil, err
}
localBlackList, err := f.db.GetBlackListDB(ctx)
if err != nil {
return nil, err
}
m := make(map[string]struct{})
for _, black := range localBlackList {
m[black.BlockUserID] = struct{}{}
}
res := make([]*sdk.SearchFriendItem, 0, len(localFriendList))
for i, localFriend := range localFriendList {
var relationship int
if _, ok := m[localFriend.FriendUserID]; ok {
relationship = constant.BlackRelationship
} else {
relationship = constant.FriendRelationship
}
res = append(res, &sdk.SearchFriendItem{
LocalFriend: *localFriendList[i],
Relationship: relationship,
})
}
return res, nil
}
func (f *Friend) SetFriendRemark(ctx context.Context, userIDRemark *sdk.SetFriendRemarkParams) error {
if err := util.ApiPost(ctx, constant.SetFriendRemark, &friend.SetFriendRemarkReq{OwnerUserID: f.loginUserID, FriendUserID: userIDRemark.ToUserID, Remark: userIDRemark.Remark}, nil); err != nil {
return err
}
return f.SyncFriends(ctx, []string{userIDRemark.ToUserID})
}
func (f *Friend) PinFriends(ctx context.Context, friends *sdk.SetFriendPinParams) error {
if err := util.ApiPost(ctx, constant.UpdateFriends, &friend.UpdateFriendsReq{OwnerUserID: f.loginUserID, FriendUserIDs: friends.ToUserIDs, IsPinned: friends.IsPinned}, nil); err != nil {
return err
}
return f.SyncFriends(ctx, friends.ToUserIDs)
}
func (f *Friend) AddBlack(ctx context.Context, blackUserID string, ex string) error {
if err := util.ApiPost(ctx, constant.AddBlackRouter, &friend.AddBlackReq{OwnerUserID: f.loginUserID, BlackUserID: blackUserID, Ex: ex}, nil); err != nil {
return err
}
return f.SyncAllBlackList(ctx)
}
func (f *Friend) RemoveBlack(ctx context.Context, blackUserID string) error {
if err := util.ApiPost(ctx, constant.RemoveBlackRouter, &friend.RemoveBlackReq{OwnerUserID: f.loginUserID, BlackUserID: blackUserID}, nil); err != nil {
return err
}
return f.SyncAllBlackList(ctx)
}
func (f *Friend) GetBlackList(ctx context.Context) ([]*model_struct.LocalBlack, error) {
return f.db.GetBlackListDB(ctx)
}
func (f *Friend) SetFriendsEx(ctx context.Context, friendIDs []string, ex string) error {
if err := util.ApiPost(ctx, constant.UpdateFriends, &friend.UpdateFriendsReq{OwnerUserID: f.loginUserID, FriendUserIDs: friendIDs, Ex: &wrapperspb.StringValue{
Value: ex,
}}, nil); err != nil {
return err
}
// Check if the specified ID is a friend
friendResults, err := f.CheckFriend(ctx, friendIDs)
if err != nil {
return errs.WrapMsg(err, "Error checking friend status")
}
// Determine if friendID is indeed a friend
// Iterate over each friendID
for _, friendID := range friendIDs {
isFriend := false
// Check if this friendID is in the friendResults
for _, result := range friendResults {
if result.UserID == friendID && result.Result == 1 { // Assuming result 1 means they are friends
isFriend = true
break
}
}
// If this friendID is not a friend, return an error
if !isFriend {
return errs.ErrRecordNotFound.WrapMsg("Not friend")
}
}
// If the code reaches here, all friendIDs are confirmed as friends
// Update friend information if they are friends
updateErr := f.db.UpdateColumnsFriend(ctx, friendIDs, map[string]interface{}{"Ex": ex})
if updateErr != nil {
return errs.WrapMsg(updateErr, "Error updating friend information")
}
return nil
}

View File

@@ -0,0 +1,180 @@
// 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 friend
import (
"context"
"fmt"
"github.com/openimsdk/tools/utils/datautil"
"time"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
friend "github.com/openimsdk/protocol/relation"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (f *Friend) SyncBothFriendRequest(ctx context.Context, fromUserID, toUserID string) error {
var resp friend.GetDesignatedFriendsApplyResp
if err := util.ApiPost(ctx, constant.GetDesignatedFriendsApplyRouter, &friend.GetDesignatedFriendsApplyReq{FromUserID: fromUserID, ToUserID: toUserID}, &resp); err != nil {
return nil
}
localData, err := f.db.GetBothFriendReq(ctx, fromUserID, toUserID)
if err != nil {
return err
}
if toUserID == f.loginUserID {
return f.requestRecvSyncer.Sync(ctx, datautil.Batch(ServerFriendRequestToLocalFriendRequest, resp.FriendRequests), localData, nil)
} else if fromUserID == f.loginUserID {
return f.requestSendSyncer.Sync(ctx, datautil.Batch(ServerFriendRequestToLocalFriendRequest, resp.FriendRequests), localData, nil)
}
return nil
}
// send
func (f *Friend) SyncAllSelfFriendApplication(ctx context.Context) error {
req := &friend.GetPaginationFriendsApplyFromReq{UserID: f.loginUserID, Pagination: &sdkws.RequestPagination{}}
fn := func(resp *friend.GetPaginationFriendsApplyFromResp) []*sdkws.FriendRequest {
return resp.FriendRequests
}
requests, err := util.GetPageAll(ctx, constant.GetSelfFriendApplicationListRouter, req, fn)
if err != nil {
return err
}
localData, err := f.db.GetSendFriendApplication(ctx)
if err != nil {
return err
}
return f.requestSendSyncer.Sync(ctx, datautil.Batch(ServerFriendRequestToLocalFriendRequest, requests), localData, nil)
}
// recv
func (f *Friend) SyncAllFriendApplication(ctx context.Context) error {
req := &friend.GetPaginationFriendsApplyToReq{UserID: f.loginUserID, Pagination: &sdkws.RequestPagination{}}
fn := func(resp *friend.GetPaginationFriendsApplyToResp) []*sdkws.FriendRequest { return resp.FriendRequests }
requests, err := util.GetPageAll(ctx, constant.GetFriendApplicationListRouter, req, fn)
if err != nil {
return err
}
localData, err := f.db.GetRecvFriendApplication(ctx)
if err != nil {
return err
}
return f.requestRecvSyncer.Sync(ctx, datautil.Batch(ServerFriendRequestToLocalFriendRequest, requests), localData, nil)
}
func (f *Friend) SyncAllFriendList(ctx context.Context) error {
t := time.Now()
defer func(start time.Time) {
elapsed := time.Since(start).Milliseconds()
log.ZDebug(ctx, "SyncAllFriendList fn call end", "cost time", fmt.Sprintf("%d ms", elapsed))
}(t)
return f.IncrSyncFriends(ctx)
//req := &friend.GetPaginationFriendsReq{UserID: f.loginUserID, Pagination: &sdkws.RequestPagination{}}
//fn := func(resp *friend.GetPaginationFriendsResp) []*sdkws.FriendInfo { return resp.FriendsInfo }
//friends, err := util.GetPageAll(ctx, constant.GetFriendListRouter, req, fn)
//if err != nil {
// return err
//}
//localData, err := f.db.GetAllFriendList(ctx)
//if err != nil {
// return err
//}
//log.ZDebug(ctx, "sync friend", "data from server", friends, "data from local", localData)
//return f.friendSyncer.Sync(ctx, util.Batch(ServerFriendToLocalFriend, friends), localData, nil)
}
func (f *Friend) deleteFriend(ctx context.Context, friendUserID string) error {
return f.IncrSyncFriends(ctx)
//friends, err := f.db.GetFriendInfoList(ctx, []string{friendUserID})
//if err != nil {
// return err
//}
//if len(friends) == 0 {
// return sdkerrs.ErrUserIDNotFound.WrapMsg("friendUserID not found")
//}
//if err := f.db.DeleteFriendDB(ctx, friendUserID); err != nil {
// return err
//}
//f.friendListener.OnFriendDeleted(*friends[0])
//return nil
}
func (f *Friend) SyncFriends(ctx context.Context, friendIDs []string) error {
return f.IncrSyncFriends(ctx)
//var resp friend.GetDesignatedFriendsResp
//if err := util.ApiPost(ctx, constant.GetDesignatedFriendsRouter, &friend.GetDesignatedFriendsReq{OwnerUserID: f.loginUserID, FriendUserIDs: friendIDs}, &resp); err != nil {
// return err
//}
//localData, err := f.db.GetFriendInfoList(ctx, friendIDs)
//if err != nil {
// return err
//}
//log.ZDebug(ctx, "sync friend", "data from server", resp.FriendsInfo, "data from local", localData)
//return f.friendSyncer.Sync(ctx, util.Batch(ServerFriendToLocalFriend, resp.FriendsInfo), localData, nil)
}
//func (f *Friend) SyncFriendPart(ctx context.Context) error {
// hashResp, err := util.CallApi[friend.GetFriendHashResp](ctx, constant.GetFriendHash, &friend.GetFriendHashReq{UserID: f.loginUserID})
// if err != nil {
// return err
// }
// friends, err := f.db.GetAllFriendList(ctx)
// if err != nil {
// return err
// }
// hashCode := f.CalculateHash(friends)
// log.ZDebug(ctx, "SyncFriendPart", "serverHash", hashResp.Hash, "serverTotal", hashResp.Total, "localHash", hashCode, "localTotal", len(friends))
// if hashCode == hashResp.Hash {
// return nil
// }
// req := &friend.GetPaginationFriendsReq{
// UserID: f.loginUserID,
// Pagination: &sdkws.RequestPagination{PageNumber: pconstant.FirstPageNumber, ShowNumber: pconstant.MaxSyncPullNumber},
// }
// resp, err := util.CallApi[friend.GetPaginationFriendsResp](ctx, constant.GetFriendListRouter, req)
// if err != nil {
// return err
// }
// serverFriends := util.Batch(ServerFriendToLocalFriend, resp.FriendsInfo)
// return f.friendSyncer.Sync(ctx, serverFriends, friends, nil)
//}
func (f *Friend) SyncAllBlackList(ctx context.Context) error {
req := &friend.GetPaginationBlacksReq{UserID: f.loginUserID, Pagination: &sdkws.RequestPagination{}}
fn := func(resp *friend.GetPaginationBlacksResp) []*sdkws.BlackInfo { return resp.Blacks }
serverData, err := util.GetPageAll(ctx, constant.GetBlackListRouter, req, fn)
if err != nil {
return err
}
log.ZDebug(ctx, "black from server", "data", serverData)
localData, err := f.db.GetBlackListDB(ctx)
if err != nil {
return err
}
log.ZDebug(ctx, "black from local", "data", localData)
return f.blockSyncer.Sync(ctx, datautil.Batch(ServerBlackToLocalBlack, serverData), localData, nil)
}
func (f *Friend) GetDesignatedFriends(ctx context.Context, friendIDs []string) ([]*sdkws.FriendInfo, error) {
resp := &friend.GetDesignatedFriendsResp{}
if err := util.ApiPost(ctx, constant.GetDesignatedFriendsRouter, &friend.GetDesignatedFriendsReq{OwnerUserID: f.loginUserID, FriendUserIDs: friendIDs}, &resp); err != nil {
return nil, err
}
return resp.FriendsInfo, nil
}

View File

@@ -0,0 +1,72 @@
package friend
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/internal/incrversion"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
friend "github.com/openimsdk/protocol/relation"
"github.com/openimsdk/tools/utils/datautil"
)
const (
LocalFriendSyncMaxNum = 1000
)
func (f *Friend) IncrSyncFriends(ctx context.Context) error {
friendSyncer := incrversion.VersionSynchronizer[*model_struct.LocalFriend, *friend.GetIncrementalFriendsResp]{
Ctx: ctx,
DB: f.db,
TableName: f.friendListTableName(),
EntityID: f.loginUserID,
Key: func(localFriend *model_struct.LocalFriend) string {
return localFriend.FriendUserID
},
Local: func() ([]*model_struct.LocalFriend, error) {
return f.db.GetAllFriendList(ctx)
},
Server: func(version *model_struct.LocalVersionSync) (*friend.GetIncrementalFriendsResp, error) {
return util.CallApi[friend.GetIncrementalFriendsResp](ctx, constant.GetIncrementalFriends, &friend.GetIncrementalFriendsReq{
UserID: f.loginUserID,
Version: version.Version,
VersionID: version.VersionID,
})
},
Full: func(resp *friend.GetIncrementalFriendsResp) bool {
return resp.Full
},
Version: func(resp *friend.GetIncrementalFriendsResp) (string, uint64) {
return resp.VersionID, resp.Version
},
Delete: func(resp *friend.GetIncrementalFriendsResp) []string {
return resp.Delete
},
Update: func(resp *friend.GetIncrementalFriendsResp) []*model_struct.LocalFriend {
return datautil.Batch(ServerFriendToLocalFriend, resp.Update)
},
Insert: func(resp *friend.GetIncrementalFriendsResp) []*model_struct.LocalFriend {
return datautil.Batch(ServerFriendToLocalFriend, resp.Insert)
},
Syncer: func(server, local []*model_struct.LocalFriend) error {
return f.friendSyncer.Sync(ctx, server, local, nil)
},
FullSyncer: func(ctx context.Context) error {
return f.friendSyncer.FullSync(ctx, f.loginUserID)
},
FullID: func(ctx context.Context) ([]string, error) {
resp, err := util.CallApi[friend.GetFullFriendUserIDsResp](ctx, constant.GetFullFriendUserIDs, &friend.GetFullFriendUserIDsReq{
UserID: f.loginUserID,
})
if err != nil {
return nil, err
}
return resp.UserIDs, nil
},
}
return friendSyncer.Sync()
}
func (f *Friend) friendListTableName() string {
return model_struct.LocalFriend{}.TableName()
}

View File

@@ -0,0 +1,13 @@
package friend
import (
"fmt"
"testing"
)
func Test_main(t *testing.T) {
a := []int{1, 2, 3, 4, 5}
fmt.Println(a[:3])
fmt.Println(a[3:])
fmt.Println(a[2:4])
}

View File

@@ -0,0 +1,62 @@
// 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 full
import (
"context"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/internal/friend"
"github.com/openimsdk/openim-sdk-core/v3/internal/group"
"github.com/openimsdk/openim-sdk-core/v3/internal/user"
"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"
)
type Full struct {
user *user.User
friend *friend.Friend
group *group.Group
ch chan common.Cmd2Value
db db_interface.DataBase
}
func (u *Full) Group() *group.Group {
return u.group
}
func NewFull(user *user.User, friend *friend.Friend, group *group.Group, ch chan common.Cmd2Value,
db db_interface.DataBase) *Full {
return &Full{user: user, friend: friend, group: group, ch: ch, db: db}
}
func (u *Full) GetGroupInfoFromLocal2Svr(ctx context.Context, groupID string, sessionType int32) (*model_struct.LocalGroup, error) {
switch sessionType {
case constant.GroupChatType:
return u.group.GetGroupInfoFromLocal2Svr(ctx, groupID)
case constant.SuperGroupChatType:
return u.GetGroupInfoByGroupID(ctx, groupID)
default:
return nil, fmt.Errorf("sessionType is not support %d", sessionType)
}
}
func (u *Full) GetReadDiffusionGroupIDList(ctx context.Context) ([]string, error) {
g, err := u.group.GetJoinedDiffusionGroupIDListFromSvr(ctx)
if err != nil {
return nil, err
}
return g, err
}

View File

@@ -0,0 +1,29 @@
// 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 full
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
)
func (u *Full) GetGroupInfoByGroupID(ctx context.Context, groupID string) (*model_struct.LocalGroup, error) {
g2, err := u.group.GetGroupInfoFromLocal2Svr(ctx, groupID)
return g2, err
}
func (u *Full) GetGroupsInfo(ctx context.Context, groupIDs ...string) (map[string]*model_struct.LocalGroup, error) {
return u.group.GetGroupsInfoFromLocal2Svr(ctx, groupIDs...)
}

View File

@@ -0,0 +1,199 @@
// 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 full
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"
api "github.com/openimsdk/openim-sdk-core/v3/pkg/server_api_params"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (u *Full) GetUsersInfo(ctx context.Context, userIDs []string) ([]*api.FullUserInfo, error) {
friendList, err := u.db.GetFriendInfoList(ctx, userIDs)
if err != nil {
return nil, err
}
blackList, err := u.db.GetBlackInfoList(ctx, userIDs)
if err != nil {
return nil, err
}
users, err := u.user.GetServerUserInfo(ctx, userIDs)
if err != nil {
return nil, err
}
friendMap := make(map[string]*model_struct.LocalFriend)
for i, f := range friendList {
friendMap[f.FriendUserID] = friendList[i]
}
blackMap := make(map[string]*model_struct.LocalBlack)
for i, b := range blackList {
blackMap[b.BlockUserID] = blackList[i]
}
userMap := make(map[string]*api.PublicUser)
for _, info := range users {
userMap[info.UserID] = &api.PublicUser{
UserID: info.UserID,
Nickname: info.Nickname,
FaceURL: info.FaceURL,
Ex: info.Ex,
CreateTime: info.CreateTime,
}
}
res := make([]*api.FullUserInfo, 0, len(users))
for _, userID := range userIDs {
info, ok := userMap[userID]
if !ok {
continue
}
res = append(res, &api.FullUserInfo{
PublicInfo: info,
FriendInfo: friendMap[userID],
BlackInfo: blackMap[userID],
})
// update single conversation
conversation, err := u.db.GetConversationByUserID(ctx, userID)
if err != nil {
log.ZWarn(ctx, "GetConversationByUserID failed", err, "userID", userID)
} else {
if _, ok := friendMap[userID]; ok {
continue
}
log.ZDebug(ctx, "GetConversationByUserID", "conversation", conversation)
if conversation.ShowName != info.Nickname || conversation.FaceURL != info.FaceURL {
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{Action: constant.UpdateConFaceUrlAndNickName,
Args: common.SourceIDAndSessionType{SourceID: userID, SessionType: conversation.ConversationType, FaceURL: info.FaceURL, Nickname: info.Nickname}}, u.ch)
_ = common.TriggerCmdUpdateMessage(ctx, common.UpdateMessageNode{Action: constant.UpdateMsgFaceUrlAndNickName,
Args: common.UpdateMessageInfo{SessionType: conversation.ConversationType, UserID: userID, FaceURL: info.FaceURL, Nickname: info.Nickname}}, u.ch)
}
}
}
return res, nil
}
func (u *Full) GetUsersInfoWithCache(ctx context.Context, userIDs []string, groupID string) ([]*api.FullUserInfoWithCache, error) {
friendList, err := u.db.GetFriendInfoList(ctx, userIDs)
if err != nil {
return nil, err
}
blackList, err := u.db.GetBlackInfoList(ctx, userIDs)
if err != nil {
return nil, err
}
users, err := u.user.GetServerUserInfo(ctx, userIDs)
if err == nil {
var strangers []*model_struct.LocalStranger
for _, val := range users {
strangerTemp := &model_struct.LocalStranger{
UserID: val.UserID,
Nickname: val.Nickname,
FaceURL: val.FaceURL,
CreateTime: val.CreateTime,
AppMangerLevel: val.AppMangerLevel,
Ex: val.Ex,
AttachedInfo: val.Ex,
GlobalRecvMsgOpt: val.GlobalRecvMsgOpt,
}
strangers = append(strangers, strangerTemp)
}
err := u.db.SetStrangerInfo(ctx, strangers)
if err != nil {
return nil, err
}
} else {
strangerList, err := u.db.GetStrangerInfo(ctx, userIDs)
if err != nil {
return nil, err
}
for _, val := range strangerList {
userTemp := &sdkws.UserInfo{
UserID: val.UserID,
Nickname: val.Nickname,
FaceURL: val.FaceURL,
Ex: val.Ex,
CreateTime: val.CreateTime,
AppMangerLevel: val.AppMangerLevel,
GlobalRecvMsgOpt: val.GlobalRecvMsgOpt,
}
users = append(users, userTemp)
}
}
var groupMemberList []*model_struct.LocalGroupMember
if groupID != "" {
groupMemberList, err = u.db.GetGroupSomeMemberInfo(ctx, groupID, userIDs)
if err != nil {
return nil, err
}
}
friendMap := make(map[string]*model_struct.LocalFriend)
for i, f := range friendList {
friendMap[f.FriendUserID] = friendList[i]
}
blackMap := make(map[string]*model_struct.LocalBlack)
for i, b := range blackList {
blackMap[b.BlockUserID] = blackList[i]
}
groupMemberMap := make(map[string]*model_struct.LocalGroupMember)
for i, b := range groupMemberList {
groupMemberMap[b.UserID] = groupMemberList[i]
}
userMap := make(map[string]*api.PublicUser)
for _, info := range users {
userMap[info.UserID] = &api.PublicUser{
UserID: info.UserID,
Nickname: info.Nickname,
FaceURL: info.FaceURL,
Ex: info.Ex,
CreateTime: info.CreateTime,
}
}
res := make([]*api.FullUserInfoWithCache, 0, len(users))
for _, userID := range userIDs {
info, ok := userMap[userID]
if !ok {
continue
}
res = append(res, &api.FullUserInfoWithCache{
PublicInfo: info,
FriendInfo: friendMap[userID],
BlackInfo: blackMap[userID],
GroupMemberInfo: groupMemberMap[userID],
})
// update single conversation
conversation, err := u.db.GetConversationByUserID(ctx, userID)
if err != nil {
log.ZWarn(ctx, "GetConversationByUserID failed", err, "userID", userID)
} else {
if _, ok := friendMap[userID]; ok {
continue
}
log.ZDebug(ctx, "GetConversationByUserID", "conversation", conversation)
if conversation.ShowName != info.Nickname || conversation.FaceURL != info.FaceURL {
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{Action: constant.UpdateConFaceUrlAndNickName,
Args: common.SourceIDAndSessionType{SourceID: userID, SessionType: conversation.ConversationType, FaceURL: info.FaceURL, Nickname: info.Nickname}}, u.ch)
_ = common.TriggerCmdUpdateMessage(ctx, common.UpdateMessageNode{Action: constant.UpdateMsgFaceUrlAndNickName,
Args: common.UpdateMessageInfo{SessionType: conversation.ConversationType, UserID: userID, FaceURL: info.FaceURL, Nickname: info.Nickname}}, u.ch)
}
}
}
return res, nil
}

View File

@@ -0,0 +1,97 @@
// 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 group
import (
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/protocol/sdkws"
)
func ServerGroupToLocalGroup(info *sdkws.GroupInfo) *model_struct.LocalGroup {
return &model_struct.LocalGroup{
GroupID: info.GroupID,
GroupName: info.GroupName,
Notification: info.Notification,
Introduction: info.Introduction,
FaceURL: info.FaceURL,
CreateTime: info.CreateTime,
Status: info.Status,
CreatorUserID: info.CreatorUserID,
GroupType: info.GroupType,
OwnerUserID: info.OwnerUserID,
MemberCount: int32(info.MemberCount),
Ex: info.Ex,
NeedVerification: info.NeedVerification,
LookMemberInfo: info.LookMemberInfo,
ApplyMemberFriend: info.ApplyMemberFriend,
NotificationUpdateTime: info.NotificationUpdateTime,
NotificationUserID: info.NotificationUserID,
//AttachedInfo: info.AttachedInfo, // TODO
}
}
func ServerGroupMemberToLocalGroupMember(info *sdkws.GroupMemberFullInfo) *model_struct.LocalGroupMember {
return &model_struct.LocalGroupMember{
GroupID: info.GroupID,
UserID: info.UserID,
Nickname: info.Nickname,
FaceURL: info.FaceURL,
RoleLevel: info.RoleLevel,
JoinTime: info.JoinTime,
JoinSource: info.JoinSource,
InviterUserID: info.InviterUserID,
MuteEndTime: info.MuteEndTime,
OperatorUserID: info.OperatorUserID,
Ex: info.Ex,
//AttachedInfo: info.AttachedInfo, // todo
}
}
func ServerGroupRequestToLocalGroupRequest(info *sdkws.GroupRequest) *model_struct.LocalGroupRequest {
return &model_struct.LocalGroupRequest{
GroupID: info.GroupInfo.GroupID,
GroupName: info.GroupInfo.GroupName,
Notification: info.GroupInfo.Notification,
Introduction: info.GroupInfo.Introduction,
GroupFaceURL: info.GroupInfo.FaceURL,
CreateTime: info.GroupInfo.CreateTime,
Status: info.GroupInfo.Status,
CreatorUserID: info.GroupInfo.CreatorUserID,
GroupType: info.GroupInfo.GroupType,
OwnerUserID: info.GroupInfo.OwnerUserID,
MemberCount: int32(info.GroupInfo.MemberCount),
UserID: info.UserInfo.UserID,
Nickname: info.UserInfo.Nickname,
UserFaceURL: info.UserInfo.FaceURL,
//Gender: info.UserInfo.Gender,
HandleResult: info.HandleResult,
ReqMsg: info.ReqMsg,
HandledMsg: info.HandleMsg,
ReqTime: info.ReqTime,
HandleUserID: info.HandleUserID,
HandledTime: info.HandleTime,
Ex: info.Ex,
//AttachedInfo: info.AttachedInfo,
JoinSource: info.JoinSource,
InviterUserID: info.InviterUserID,
}
}
func ServerGroupRequestToLocalAdminGroupRequest(info *sdkws.GroupRequest) *model_struct.LocalAdminGroupRequest {
return &model_struct.LocalAdminGroupRequest{
LocalGroupRequest: *ServerGroupRequestToLocalGroupRequest(info),
}
}

View File

@@ -0,0 +1,343 @@
// 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 group
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"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/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/page"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/openim-sdk-core/v3/pkg/syncer"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/protocol/group"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
)
func NewGroup(loginUserID string, db db_interface.DataBase,
conversationCh chan common.Cmd2Value) *Group {
g := &Group{
loginUserID: loginUserID,
db: db,
conversationCh: conversationCh,
}
g.initSyncer()
return g
}
// //utils.GetCurrentTimestampByMill()
type Group struct {
listener func() open_im_sdk_callback.OnGroupListener
loginUserID string
db db_interface.DataBase
groupSyncer *syncer.Syncer[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string]
groupMemberSyncer *syncer.Syncer[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string]
groupRequestSyncer *syncer.Syncer[*model_struct.LocalGroupRequest, syncer.NoResp, [2]string]
groupAdminRequestSyncer *syncer.Syncer[*model_struct.LocalAdminGroupRequest, syncer.NoResp, [2]string]
joinedSuperGroupCh chan common.Cmd2Value
heartbeatCmdCh chan common.Cmd2Value
conversationCh chan common.Cmd2Value
// memberSyncMutex sync.RWMutex
listenerForService open_im_sdk_callback.OnListenerForService
}
func (g *Group) initSyncer() {
g.groupSyncer = syncer.New2[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](
syncer.WithInsert[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(ctx context.Context, value *model_struct.LocalGroup) error {
return g.db.InsertGroup(ctx, value)
}),
syncer.WithDelete[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(ctx context.Context, value *model_struct.LocalGroup) error {
if err := g.db.DeleteGroupAllMembers(ctx, value.GroupID); err != nil {
return err
}
if err := g.db.DeleteVersionSync(ctx, g.groupAndMemberVersionTableName(), value.GroupID); err != nil {
return err
}
return g.db.DeleteGroup(ctx, value.GroupID)
}),
syncer.WithUpdate[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(ctx context.Context, server, local *model_struct.LocalGroup) error {
log.ZInfo(ctx, "groupSyncer trigger update function", "groupID", server.GroupID, "server", server, "local", local)
return g.db.UpdateGroup(ctx, server)
}),
syncer.WithUUID[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(value *model_struct.LocalGroup) string {
return value.GroupID
}),
syncer.WithNotice[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(ctx context.Context, state int, server, local *model_struct.LocalGroup) error {
switch state {
case syncer.Insert:
// when a user kicked to the group and invited to the group again, group info maybe updated,
// so conversation info need to be updated
g.listener().OnJoinedGroupAdded(utils.StructToJsonString(server))
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{
Action: constant.UpdateConFaceUrlAndNickName,
Args: common.SourceIDAndSessionType{
SourceID: server.GroupID, SessionType: constant.SuperGroupChatType,
FaceURL: server.FaceURL, Nickname: server.GroupName,
},
}, g.conversationCh)
case syncer.Delete:
g.listener().OnJoinedGroupDeleted(utils.StructToJsonString(local))
case syncer.Update:
log.ZInfo(ctx, "groupSyncer trigger update", "groupID",
server.GroupID, "data", server, "isDismissed", server.Status == constant.GroupStatusDismissed)
if server.Status == constant.GroupStatusDismissed {
if err := g.db.DeleteGroupAllMembers(ctx, server.GroupID); err != nil {
log.ZError(ctx, "delete group all members failed", err)
}
g.listener().OnGroupDismissed(utils.StructToJsonString(server))
} else {
g.listener().OnGroupInfoChanged(utils.StructToJsonString(server))
if server.GroupName != local.GroupName || local.FaceURL != server.FaceURL {
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{
Action: constant.UpdateConFaceUrlAndNickName,
Args: common.SourceIDAndSessionType{
SourceID: server.GroupID, SessionType: constant.SuperGroupChatType,
FaceURL: server.FaceURL, Nickname: server.GroupName,
},
}, g.conversationCh)
}
}
}
return nil
}),
syncer.WithBatchInsert[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(ctx context.Context, values []*model_struct.LocalGroup) error {
return g.db.BatchInsertGroup(ctx, values)
}),
syncer.WithDeleteAll[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(ctx context.Context, _ string) error {
return g.db.DeleteAllGroup(ctx)
}),
syncer.WithBatchPageReq[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(entityID string) page.PageReq {
return &group.GetJoinedGroupListReq{FromUserID: entityID,
Pagination: &sdkws.RequestPagination{ShowNumber: 100}}
}),
syncer.WithBatchPageRespConvertFunc[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(resp *group.GetJoinedGroupListResp) []*model_struct.LocalGroup {
return datautil.Batch(ServerGroupToLocalGroup, resp.Groups)
}),
syncer.WithReqApiRouter[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](constant.GetJoinedGroupListRouter),
)
g.groupMemberSyncer = syncer.New2[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](
syncer.WithInsert[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(ctx context.Context, value *model_struct.LocalGroupMember) error {
return g.db.InsertGroupMember(ctx, value)
}),
syncer.WithDelete[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(ctx context.Context, value *model_struct.LocalGroupMember) error {
return g.db.DeleteGroupMember(ctx, value.GroupID, value.UserID)
}),
syncer.WithUpdate[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(ctx context.Context, server, local *model_struct.LocalGroupMember) error {
return g.db.UpdateGroupMember(ctx, server)
}),
syncer.WithUUID[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(value *model_struct.LocalGroupMember) [2]string {
return [...]string{value.GroupID, value.UserID}
}),
syncer.WithNotice[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(ctx context.Context, state int, server, local *model_struct.LocalGroupMember) error {
switch state {
case syncer.Insert:
g.listener().OnGroupMemberAdded(utils.StructToJsonString(server))
// When a user is kicked and invited to the group again, group member info will be updated.
_ = common.TriggerCmdUpdateMessage(ctx,
common.UpdateMessageNode{
Action: constant.UpdateMsgFaceUrlAndNickName,
Args: common.UpdateMessageInfo{
SessionType: constant.SuperGroupChatType, UserID: server.UserID, FaceURL: server.FaceURL,
Nickname: server.Nickname, GroupID: server.GroupID,
},
}, g.conversationCh)
case syncer.Delete:
g.listener().OnGroupMemberDeleted(utils.StructToJsonString(local))
case syncer.Update:
g.listener().OnGroupMemberInfoChanged(utils.StructToJsonString(server))
if server.Nickname != local.Nickname || server.FaceURL != local.FaceURL {
_ = common.TriggerCmdUpdateMessage(ctx,
common.UpdateMessageNode{
Action: constant.UpdateMsgFaceUrlAndNickName,
Args: common.UpdateMessageInfo{
SessionType: constant.SuperGroupChatType, UserID: server.UserID, FaceURL: server.FaceURL,
Nickname: server.Nickname, GroupID: server.GroupID,
},
}, g.conversationCh)
}
}
return nil
}),
syncer.WithBatchInsert[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(ctx context.Context, values []*model_struct.LocalGroupMember) error {
return g.db.BatchInsertGroupMember(ctx, values)
}),
syncer.WithDeleteAll[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(ctx context.Context, groupID string) error {
return g.db.DeleteGroupAllMembers(ctx, groupID)
}),
syncer.WithBatchPageReq[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(entityID string) page.PageReq {
return &group.GetGroupMemberListReq{GroupID: entityID, Pagination: &sdkws.RequestPagination{ShowNumber: 100}}
}),
syncer.WithBatchPageRespConvertFunc[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(resp *group.GetGroupMemberListResp) []*model_struct.LocalGroupMember {
return datautil.Batch(ServerGroupMemberToLocalGroupMember, resp.Members)
}),
syncer.WithReqApiRouter[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](constant.GetGroupMemberListRouter),
)
g.groupRequestSyncer = syncer.New[*model_struct.LocalGroupRequest, syncer.NoResp, [2]string](func(ctx context.Context, value *model_struct.LocalGroupRequest) error {
return g.db.InsertGroupRequest(ctx, value)
}, func(ctx context.Context, value *model_struct.LocalGroupRequest) error {
return g.db.DeleteGroupRequest(ctx, value.GroupID, value.UserID)
}, func(ctx context.Context, server, local *model_struct.LocalGroupRequest) error {
return g.db.UpdateGroupRequest(ctx, server)
}, func(value *model_struct.LocalGroupRequest) [2]string {
return [...]string{value.GroupID, value.UserID}
}, nil, func(ctx context.Context, state int, server, local *model_struct.LocalGroupRequest) error {
switch state {
case syncer.Insert:
g.listener().OnGroupApplicationAdded(utils.StructToJsonString(server))
case syncer.Update:
switch server.HandleResult {
case constant.FriendResponseAgree:
g.listener().OnGroupApplicationAccepted(utils.StructToJsonString(server))
case constant.FriendResponseRefuse:
g.listener().OnGroupApplicationRejected(utils.StructToJsonString(server))
default:
g.listener().OnGroupApplicationAdded(utils.StructToJsonString(server))
}
}
return nil
})
g.groupAdminRequestSyncer = syncer.New[*model_struct.LocalAdminGroupRequest, syncer.NoResp, [2]string](func(ctx context.Context, value *model_struct.LocalAdminGroupRequest) error {
return g.db.InsertAdminGroupRequest(ctx, value)
}, func(ctx context.Context, value *model_struct.LocalAdminGroupRequest) error {
return g.db.DeleteAdminGroupRequest(ctx, value.GroupID, value.UserID)
}, func(ctx context.Context, server, local *model_struct.LocalAdminGroupRequest) error {
return g.db.UpdateAdminGroupRequest(ctx, server)
}, func(value *model_struct.LocalAdminGroupRequest) [2]string {
return [...]string{value.GroupID, value.UserID}
}, nil, func(ctx context.Context, state int, server, local *model_struct.LocalAdminGroupRequest) error {
switch state {
case syncer.Insert:
g.listener().OnGroupApplicationAdded(utils.StructToJsonString(server))
case syncer.Update:
switch server.HandleResult {
case constant.FriendResponseAgree:
g.listener().OnGroupApplicationAccepted(utils.StructToJsonString(server))
case constant.FriendResponseRefuse:
g.listener().OnGroupApplicationRejected(utils.StructToJsonString(server))
default:
g.listener().OnGroupApplicationAdded(utils.StructToJsonString(server))
}
}
return nil
})
}
func (g *Group) SetGroupListener(listener func() open_im_sdk_callback.OnGroupListener) {
g.listener = listener
}
func (g *Group) SetListenerForService(listener open_im_sdk_callback.OnListenerForService) {
g.listenerForService = listener
}
func (g *Group) GetGroupOwnerIDAndAdminIDList(ctx context.Context, groupID string) (ownerID string, adminIDList []string, err error) {
localGroup, err := g.db.GetGroupInfoByGroupID(ctx, groupID)
if err != nil {
return "", nil, err
}
adminIDList, err = g.db.GetGroupAdminID(ctx, groupID)
if err != nil {
return "", nil, err
}
return localGroup.OwnerUserID, adminIDList, nil
}
func (g *Group) GetGroupInfoFromLocal2Svr(ctx context.Context, groupID string) (*model_struct.LocalGroup, error) {
localGroup, err := g.db.GetGroupInfoByGroupID(ctx, groupID)
if err == nil {
return localGroup, nil
}
svrGroup, err := g.getGroupsInfoFromSvr(ctx, []string{groupID})
if err != nil {
return nil, err
}
if len(svrGroup) == 0 {
return nil, sdkerrs.ErrGroupIDNotFound.WrapMsg("server not this group")
}
return ServerGroupToLocalGroup(svrGroup[0]), nil
}
func (g *Group) GetGroupsInfoFromLocal2Svr(ctx context.Context, groupIDs ...string) (map[string]*model_struct.LocalGroup, error) {
groupMap := make(map[string]*model_struct.LocalGroup)
if len(groupIDs) == 0 {
return groupMap, nil
}
groups, err := g.db.GetGroups(ctx, groupIDs)
if err != nil {
return nil, err
}
var groupIDsNeedSync []string
localGroupIDs := datautil.Slice(groups, func(group *model_struct.LocalGroup) string {
return group.GroupID
})
for _, groupID := range groupIDs {
if !datautil.Contain(groupID, localGroupIDs...) {
groupIDsNeedSync = append(groupIDsNeedSync, groupID)
}
}
if len(groupIDsNeedSync) > 0 {
svrGroups, err := g.getGroupsInfoFromSvr(ctx, groupIDsNeedSync)
if err != nil {
return nil, err
}
for _, svrGroup := range svrGroups {
groups = append(groups, ServerGroupToLocalGroup(svrGroup))
}
}
for _, group := range groups {
groupMap[group.GroupID] = group
}
return groupMap, nil
}
func (g *Group) getGroupsInfoFromSvr(ctx context.Context, groupIDs []string) ([]*sdkws.GroupInfo, error) {
resp, err := util.CallApi[group.GetGroupsInfoResp](ctx, constant.GetGroupsInfoRouter, &group.GetGroupsInfoReq{GroupIDs: groupIDs})
if err != nil {
return nil, err
}
return resp.GroupInfos, nil
}
func (g *Group) getGroupAbstractInfoFromSvr(ctx context.Context, groupIDs []string) (*group.GetGroupAbstractInfoResp, error) {
return util.CallApi[group.GetGroupAbstractInfoResp](ctx, constant.GetGroupAbstractInfoRouter, &group.GetGroupAbstractInfoReq{GroupIDs: groupIDs})
}
func (g *Group) GetJoinedDiffusionGroupIDListFromSvr(ctx context.Context) ([]string, error) {
groups, err := g.GetServerJoinGroup(ctx)
if err != nil {
return nil, err
}
var groupIDs []string
for _, g := range groups {
if g.GroupType == constant.WorkingGroup {
groupIDs = append(groupIDs, g.GroupID)
}
}
return groupIDs, 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 group
import (
"context"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (g *Group) DoNotification(ctx context.Context, msg *sdkws.MsgData) {
go func() {
if err := g.doNotification(ctx, msg); err != nil {
log.ZError(ctx, "DoGroupNotification failed", err)
}
}()
}
func (g *Group) doNotification(ctx context.Context, msg *sdkws.MsgData) error {
switch msg.ContentType {
case constant.GroupCreatedNotification: // 1501
var detail sdkws.GroupCreatedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
if err := g.IncrSyncJoinGroup(ctx); err != nil {
return err
}
return g.IncrSyncGroupAndMember(ctx, detail.Group.GroupID)
case constant.GroupInfoSetNotification: // 1502
var detail sdkws.GroupInfoSetTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
nil, nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.JoinGroupApplicationNotification: // 1503
var detail sdkws.JoinGroupApplicationTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
if detail.Applicant.UserID == g.loginUserID {
return g.SyncSelfGroupApplications(ctx, detail.Group.GroupID)
} else {
return g.SyncAdminGroupApplications(ctx, detail.Group.GroupID)
}
case constant.MemberQuitNotification: // 1504
var detail sdkws.MemberQuitTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
if detail.QuitUser.UserID == g.loginUserID {
return g.IncrSyncJoinGroup(ctx)
} else {
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, []*sdkws.GroupMemberFullInfo{detail.QuitUser},
nil, nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
}
case constant.GroupApplicationAcceptedNotification: // 1505
var detail sdkws.GroupApplicationAcceptedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
switch detail.ReceiverAs {
case 0:
return g.SyncAllSelfGroupApplication(ctx)
case 1:
return g.SyncAdminGroupApplications(ctx, detail.Group.GroupID)
default:
return errs.New(fmt.Sprintf("GroupApplicationAcceptedNotification ReceiverAs unknown %d", detail.ReceiverAs)).Wrap()
}
case constant.GroupApplicationRejectedNotification: // 1506
var detail sdkws.GroupApplicationRejectedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
switch detail.ReceiverAs {
case 0:
return g.SyncAllSelfGroupApplication(ctx)
case 1:
return g.SyncAdminGroupApplications(ctx, detail.Group.GroupID)
default:
return errs.New(fmt.Sprintf("GroupApplicationRejectedNotification ReceiverAs unknown %d", detail.ReceiverAs)).Wrap()
}
case constant.GroupOwnerTransferredNotification: // 1507
var detail sdkws.GroupOwnerTransferredTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
if detail.Group == nil {
return errs.New(fmt.Sprintf("group is nil, groupID: %s", detail.Group.GroupID)).Wrap()
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
[]*sdkws.GroupMemberFullInfo{detail.NewGroupOwner, detail.OldGroupOwnerInfo}, nil,
detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.MemberKickedNotification: // 1508
var detail sdkws.MemberKickedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
var self bool
for _, info := range detail.KickedUserList {
if info.UserID == g.loginUserID {
self = true
break
}
}
if self {
return g.IncrSyncJoinGroup(ctx)
} else {
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, detail.KickedUserList, nil,
nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
}
case constant.MemberInvitedNotification: // 1509
var detail sdkws.MemberInvitedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
userIDMap := datautil.SliceSetAny(detail.InvitedUserList, func(e *sdkws.GroupMemberFullInfo) string {
return e.UserID
})
//自己也是被邀请的一员
if _, ok := userIDMap[g.loginUserID]; ok {
if err := g.IncrSyncJoinGroup(ctx); err != nil {
return err
}
return g.IncrSyncGroupAndMember(ctx, detail.Group.GroupID)
} else {
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil, nil,
detail.InvitedUserList, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
}
case constant.MemberEnterNotification: // 1510
var detail sdkws.MemberEnterTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
if detail.EntrantUser.UserID == g.loginUserID {
if err := g.IncrSyncJoinGroup(ctx); err != nil {
return err
}
return g.IncrSyncGroupAndMember(ctx, detail.Group.GroupID)
} else {
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil, nil,
[]*sdkws.GroupMemberFullInfo{detail.EntrantUser}, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
}
case constant.GroupDismissedNotification: // 1511
var detail sdkws.GroupDismissedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
g.listener().OnGroupDismissed(utils.StructToJsonString(detail.Group))
return g.IncrSyncJoinGroup(ctx)
case constant.GroupMemberMutedNotification: // 1512
var detail sdkws.GroupMemberMutedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
[]*sdkws.GroupMemberFullInfo{detail.MutedUser}, nil, nil,
detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupMemberCancelMutedNotification: // 1513
var detail sdkws.GroupMemberCancelMutedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
[]*sdkws.GroupMemberFullInfo{detail.MutedUser}, nil, nil,
detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupMutedNotification: // 1514
var detail sdkws.GroupMutedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil, nil,
nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupCancelMutedNotification: // 1515
var detail sdkws.GroupCancelMutedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil, nil,
nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupMemberInfoSetNotification: // 1516
var detail sdkws.GroupMemberInfoSetTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
[]*sdkws.GroupMemberFullInfo{detail.ChangedUser}, nil, nil,
detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupMemberSetToAdminNotification: // 1517
var detail sdkws.GroupMemberInfoSetTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
[]*sdkws.GroupMemberFullInfo{detail.ChangedUser}, nil, nil,
detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupMemberSetToOrdinaryUserNotification: // 1518
var detail sdkws.GroupMemberInfoSetTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
[]*sdkws.GroupMemberFullInfo{detail.ChangedUser}, nil, nil,
detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupInfoSetAnnouncementNotification: // 1519
var detail sdkws.GroupInfoSetAnnouncementTips //
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil, nil,
nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupInfoSetNameNotification: // 1520
var detail sdkws.GroupInfoSetNameTips //
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
nil, nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
default:
return errs.New("unknown tips type", "contentType", msg.ContentType).Wrap()
}
}

View File

@@ -0,0 +1,383 @@
// 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 group
import (
"context"
"time"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/openim-sdk-core/v3/pkg/datafetcher"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"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/sdk_params_callback"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/protocol/group"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/protocol/wrapperspb"
)
func (g *Group) CreateGroup(ctx context.Context, req *group.CreateGroupReq) (*sdkws.GroupInfo, error) {
if req.OwnerUserID == "" {
req.OwnerUserID = g.loginUserID
}
if req.GroupInfo.GroupType != constant.WorkingGroup {
return nil, sdkerrs.ErrGroupType
}
req.GroupInfo.CreatorUserID = g.loginUserID
resp, err := util.CallApi[group.CreateGroupResp](ctx, constant.CreateGroupRouter, req)
if err != nil {
return nil, err
}
if err := g.IncrSyncJoinGroup(ctx); err != nil {
return nil, err
}
if err := g.IncrSyncGroupAndMember(ctx, resp.GroupInfo.GroupID); err != nil {
return nil, err
}
return resp.GroupInfo, nil
}
func (g *Group) JoinGroup(ctx context.Context, groupID, reqMsg string, joinSource int32, ex string) error {
if err := util.ApiPost(ctx, constant.JoinGroupRouter, &group.JoinGroupReq{GroupID: groupID, ReqMessage: reqMsg, JoinSource: joinSource, InviterUserID: g.loginUserID, Ex: ex}, nil); err != nil {
return err
}
if err := g.SyncSelfGroupApplications(ctx, groupID); err != nil {
return err
}
return nil
}
func (g *Group) QuitGroup(ctx context.Context, groupID string) error {
if err := util.ApiPost(ctx, constant.QuitGroupRouter, &group.QuitGroupReq{GroupID: groupID}, nil); err != nil {
return err
}
return nil
}
func (g *Group) DismissGroup(ctx context.Context, groupID string) error {
if err := util.ApiPost(ctx, constant.DismissGroupRouter, &group.DismissGroupReq{GroupID: groupID}, nil); err != nil {
return err
}
return nil
}
func (g *Group) SetGroupApplyMemberFriend(ctx context.Context, groupID string, rule int32) error {
return g.SetGroupInfo(ctx, &sdkws.GroupInfoForSet{GroupID: groupID, ApplyMemberFriend: wrapperspb.Int32(rule)})
}
func (g *Group) SetGroupLookMemberInfo(ctx context.Context, groupID string, rule int32) error {
return g.SetGroupInfo(ctx, &sdkws.GroupInfoForSet{GroupID: groupID, LookMemberInfo: wrapperspb.Int32(rule)})
}
func (g *Group) SetGroupVerification(ctx context.Context, groupID string, verification int32) error {
return g.SetGroupInfo(ctx, &sdkws.GroupInfoForSet{GroupID: groupID, NeedVerification: wrapperspb.Int32(verification)})
}
func (g *Group) ChangeGroupMute(ctx context.Context, groupID string, isMute bool) (err error) {
if isMute {
err = util.ApiPost(ctx, constant.MuteGroupRouter, &group.MuteGroupReq{GroupID: groupID}, nil)
} else {
err = util.ApiPost(ctx, constant.CancelMuteGroupRouter, &group.CancelMuteGroupReq{GroupID: groupID}, nil)
}
if err != nil {
return err
}
if err := g.IncrSyncGroupAndMember(ctx, groupID); err != nil {
return err
}
return nil
}
func (g *Group) ChangeGroupMemberMute(ctx context.Context, groupID, userID string, mutedSeconds int) (err error) {
if mutedSeconds == 0 {
err = util.ApiPost(ctx, constant.CancelMuteGroupMemberRouter, &group.CancelMuteGroupMemberReq{GroupID: groupID, UserID: userID}, nil)
} else {
err = util.ApiPost(ctx, constant.MuteGroupMemberRouter, &group.MuteGroupMemberReq{GroupID: groupID, UserID: userID, MutedSeconds: uint32(mutedSeconds)}, nil)
}
if err != nil {
return err
}
return nil
}
func (g *Group) TransferGroupOwner(ctx context.Context, groupID, newOwnerUserID string) error {
if err := util.ApiPost(ctx, constant.TransferGroupRouter, &group.TransferGroupOwnerReq{GroupID: groupID, OldOwnerUserID: g.loginUserID, NewOwnerUserID: newOwnerUserID}, nil); err != nil {
return err
}
if err := g.IncrSyncGroupAndMember(ctx, groupID); err != nil {
return err
}
return nil
}
func (g *Group) KickGroupMember(ctx context.Context, groupID string, reason string, userIDList []string) error {
if err := util.ApiPost(ctx, constant.KickGroupMemberRouter, &group.KickGroupMemberReq{GroupID: groupID, KickedUserIDs: userIDList, Reason: reason}, nil); err != nil {
return err
}
return g.IncrSyncGroupAndMember(ctx, groupID)
}
func (g *Group) SetGroupInfo(ctx context.Context, groupInfo *sdkws.GroupInfoForSet) error {
if err := util.ApiPost(ctx, constant.SetGroupInfoRouter, &group.SetGroupInfoReq{GroupInfoForSet: groupInfo}, nil); err != nil {
return err
}
return g.IncrSyncJoinGroup(ctx)
}
func (g *Group) SetGroupMemberInfo(ctx context.Context, groupMemberInfo *group.SetGroupMemberInfo) error {
if err := util.ApiPost(ctx, constant.SetGroupMemberInfoRouter, &group.SetGroupMemberInfoReq{Members: []*group.SetGroupMemberInfo{groupMemberInfo}}, nil); err != nil {
return err
}
return g.IncrSyncGroupAndMember(ctx, groupMemberInfo.GroupID)
}
func (g *Group) SetGroupMemberRoleLevel(ctx context.Context, groupID, userID string, roleLevel int) error {
return g.SetGroupMemberInfo(ctx, &group.SetGroupMemberInfo{GroupID: groupID, UserID: userID, RoleLevel: wrapperspb.Int32(int32(roleLevel))})
}
func (g *Group) SetGroupMemberNickname(ctx context.Context, groupID, userID string, groupMemberNickname string) error {
return g.SetGroupMemberInfo(ctx, &group.SetGroupMemberInfo{GroupID: groupID, UserID: userID, Nickname: wrapperspb.String(groupMemberNickname)})
}
func (g *Group) GetJoinedGroupList(ctx context.Context) ([]*model_struct.LocalGroup, error) {
return g.db.GetJoinedGroupListDB(ctx)
}
func (g *Group) GetJoinedGroupListPage(ctx context.Context, offset, count int32) ([]*model_struct.LocalGroup, error) {
dataFetcher := datafetcher.NewDataFetcher(
g.db,
g.groupTableName(),
g.loginUserID,
func(localGroup *model_struct.LocalGroup) string {
return localGroup.GroupID
},
func(ctx context.Context, values []*model_struct.LocalGroup) error {
return g.db.BatchInsertGroup(ctx, values)
},
func(ctx context.Context, groupIDs []string) ([]*model_struct.LocalGroup, error) {
return g.db.GetGroups(ctx, groupIDs)
},
func(ctx context.Context, groupIDs []string) ([]*model_struct.LocalGroup, error) {
serverGroupInfo, err := g.getGroupsInfoFromSvr(ctx, groupIDs)
if err != nil {
return nil, err
}
return datautil.Batch(ServerGroupToLocalGroup, serverGroupInfo), nil
},
)
return dataFetcher.FetchWithPagination(ctx, int(offset), int(count))
}
func (g *Group) GetSpecifiedGroupsInfo(ctx context.Context, groupIDs []string) ([]*model_struct.LocalGroup, error) {
dataFetcher := datafetcher.NewDataFetcher(
g.db,
g.groupTableName(),
g.loginUserID,
func(localGroup *model_struct.LocalGroup) string {
return localGroup.GroupID
},
func(ctx context.Context, values []*model_struct.LocalGroup) error {
return g.db.BatchInsertGroup(ctx, values)
},
func(ctx context.Context, groupIDs []string) ([]*model_struct.LocalGroup, error) {
return g.db.GetGroups(ctx, groupIDs)
},
func(ctx context.Context, groupIDs []string) ([]*model_struct.LocalGroup, error) {
serverGroupInfo, err := g.getGroupsInfoFromSvr(ctx, groupIDs)
if err != nil {
return nil, err
}
return datautil.Batch(ServerGroupToLocalGroup, serverGroupInfo), nil
},
)
return dataFetcher.FetchMissingAndFillLocal(ctx, groupIDs)
}
func (g *Group) SearchGroups(ctx context.Context, param sdk_params_callback.SearchGroupsParam) ([]*model_struct.LocalGroup, error) {
if len(param.KeywordList) == 0 || (!param.IsSearchGroupName && !param.IsSearchGroupID) {
return nil, sdkerrs.ErrArgs.WrapMsg("keyword is null or search field all false")
}
groups, err := g.db.GetAllGroupInfoByGroupIDOrGroupName(ctx, param.KeywordList[0], param.IsSearchGroupID, param.IsSearchGroupName) // todo param.KeywordList[0]
if err != nil {
return nil, err
}
return groups, nil
}
// funcation (g *Group) SetGroupInfo(ctx context.Context, groupInfo *sdk_params_callback.SetGroupInfoParam, groupID string) error {
// return g.SetGroupInfo(ctx, &sdkws.GroupInfoForSet{
// GroupID: groupID,
// GroupName: groupInfo.GroupName,
// Notification: groupInfo.Notification,
// Introduction: groupInfo.Introduction,
// FaceURL: groupInfo.FaceURL,
// Ex: groupInfo.Ex,
// NeedVerification: wrapperspb.Int32Ptr(groupInfo.NeedVerification),
// })
// }
func (g *Group) GetGroupMemberOwnerAndAdmin(ctx context.Context, groupID string) ([]*model_struct.LocalGroupMember, error) {
return g.db.GetGroupMemberOwnerAndAdminDB(ctx, groupID)
}
func (g *Group) GetGroupMemberListByJoinTimeFilter(ctx context.Context, groupID string, offset, count int32, joinTimeBegin, joinTimeEnd int64, userIDs []string) ([]*model_struct.LocalGroupMember, error) {
if joinTimeEnd == 0 {
joinTimeEnd = time.Now().UnixMilli()
}
return g.db.GetGroupMemberListSplitByJoinTimeFilter(ctx, groupID, int(offset), int(count), joinTimeBegin, joinTimeEnd, userIDs)
}
func (g *Group) GetSpecifiedGroupMembersInfo(ctx context.Context, groupID string, userIDList []string) ([]*model_struct.LocalGroupMember, error) {
dataFetcher := datafetcher.NewDataFetcher(
g.db,
g.groupAndMemberVersionTableName(),
groupID,
func(localGroupMember *model_struct.LocalGroupMember) string {
return localGroupMember.UserID
},
func(ctx context.Context, values []*model_struct.LocalGroupMember) error {
return g.db.BatchInsertGroupMember(ctx, values)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalGroupMember, error) {
return g.db.GetGroupSomeMemberInfo(ctx, groupID, userIDList)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalGroupMember, error) {
serverGroupMember, err := g.GetDesignatedGroupMembers(ctx, groupID, userIDs)
if err != nil {
return nil, err
}
return datautil.Batch(ServerGroupMemberToLocalGroupMember, serverGroupMember), nil
},
)
return dataFetcher.FetchMissingAndFillLocal(ctx, userIDList)
// return g.db.GetGroupSomeMemberInfo(ctx, groupID, userIDList)
}
func (g *Group) GetGroupMemberList(ctx context.Context, groupID string, filter, offset, count int32) ([]*model_struct.LocalGroupMember, error) {
dataFetcher := datafetcher.NewDataFetcher(
g.db,
g.groupAndMemberVersionTableName(),
groupID,
func(localGroupMember *model_struct.LocalGroupMember) string {
return localGroupMember.UserID
},
func(ctx context.Context, values []*model_struct.LocalGroupMember) error {
return g.db.BatchInsertGroupMember(ctx, values)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalGroupMember, error) {
return g.db.GetGroupMemberListByUserIDs(ctx, groupID, filter, userIDs)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalGroupMember, error) {
serverGroupMember, err := g.GetDesignatedGroupMembers(ctx, groupID, userIDs)
if err != nil {
return nil, err
}
return datautil.Batch(ServerGroupMemberToLocalGroupMember, serverGroupMember), nil
},
)
return dataFetcher.FetchWithPagination(ctx, int(offset), int(count))
}
func (g *Group) GetGroupApplicationListAsRecipient(ctx context.Context) ([]*model_struct.LocalAdminGroupRequest, error) {
return g.db.GetAdminGroupApplication(ctx)
}
func (g *Group) GetGroupApplicationListAsApplicant(ctx context.Context) ([]*model_struct.LocalGroupRequest, error) {
return g.db.GetSendGroupApplication(ctx)
}
func (g *Group) SearchGroupMembers(ctx context.Context, searchParam *sdk_params_callback.SearchGroupMembersParam) ([]*model_struct.LocalGroupMember, error) {
return g.db.SearchGroupMembersDB(ctx, searchParam.KeywordList[0], searchParam.GroupID, searchParam.IsSearchMemberNickname, searchParam.IsSearchUserID, searchParam.Offset, searchParam.Count)
}
func (g *Group) IsJoinGroup(ctx context.Context, groupID string) (bool, error) {
groupList, err := g.db.GetJoinedGroupListDB(ctx)
if err != nil {
return false, err
}
for _, localGroup := range groupList {
if localGroup.GroupID == groupID {
return true, nil
}
}
return false, nil
}
func (g *Group) InviteUserToGroup(ctx context.Context, groupID, reason string, userIDList []string) error {
if err := util.ApiPost(ctx, constant.InviteUserToGroupRouter, &group.InviteUserToGroupReq{GroupID: groupID, Reason: reason, InvitedUserIDs: userIDList}, nil); err != nil {
return err
}
if err := g.IncrSyncGroupAndMember(ctx, groupID); err != nil {
return err
}
return nil
}
func (g *Group) AcceptGroupApplication(ctx context.Context, groupID, fromUserID, handleMsg string) error {
return g.HandlerGroupApplication(ctx, &group.GroupApplicationResponseReq{GroupID: groupID, FromUserID: fromUserID, HandledMsg: handleMsg, HandleResult: constant.GroupResponseAgree})
}
func (g *Group) RefuseGroupApplication(ctx context.Context, groupID, fromUserID, handleMsg string) error {
return g.HandlerGroupApplication(ctx, &group.GroupApplicationResponseReq{GroupID: groupID, FromUserID: fromUserID, HandledMsg: handleMsg, HandleResult: constant.GroupResponseRefuse})
}
func (g *Group) HandlerGroupApplication(ctx context.Context, req *group.GroupApplicationResponseReq) error {
if err := util.ApiPost(ctx, constant.AcceptGroupApplicationRouter, req, nil); err != nil {
return err
}
// SyncAdminGroupApplication todo
return nil
}
//func (g *Group) SearchGroupMembersV2(ctx context.Context, req *group.SearchGroupMemberReq) ([]*model_struct.LocalGroupMember, error) {
// if err := req.Check(); err != nil {
// return nil, err
// }
// info, err := g.db.GetGroupInfoByGroupID(ctx, req.GroupID)
// if err != nil {
// return nil, err
// }
// if info.MemberCount <= pconstant.MaxSyncPullNumber {
// return g.db.SearchGroupMembersDB(ctx, req.Keyword, req.GroupID, true, false,
// int((req.Pagination.PageNumber-1)*req.Pagination.ShowNumber), int(req.Pagination.ShowNumber))
// }
// resp, err := util.CallApi[group.SearchGroupMemberResp](ctx, constant.SearchGroupMember, req)
// if err != nil {
// return nil, err
// }
// return datautil.Slice(resp.Members, g.pbGroupMemberToLocal), nil
//}
func (g *Group) pbGroupMemberToLocal(pb *sdkws.GroupMemberFullInfo) *model_struct.LocalGroupMember {
return &model_struct.LocalGroupMember{
GroupID: pb.GroupID,
UserID: pb.UserID,
Nickname: pb.Nickname,
FaceURL: pb.FaceURL,
RoleLevel: pb.RoleLevel,
JoinTime: pb.JoinTime,
JoinSource: pb.JoinSource,
InviterUserID: pb.InviterUserID,
MuteEndTime: pb.MuteEndTime,
OperatorUserID: pb.OperatorUserID,
Ex: pb.Ex,
// AttachedInfo: pb.AttachedInfo,
}
}

View File

@@ -0,0 +1,310 @@
// 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 group
import (
"context"
"crypto/md5"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/protocol/group"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
"time"
)
func (g *Group) getGroupHash(members []*model_struct.LocalGroupMember) uint64 {
userIDs := datautil.Slice(members, func(member *model_struct.LocalGroupMember) string {
return member.UserID
})
datautil.Sort(userIDs, true)
memberMap := make(map[string]*sdkws.GroupMemberFullInfo)
for _, member := range members {
memberMap[member.UserID] = &sdkws.GroupMemberFullInfo{
GroupID: member.GroupID,
UserID: member.UserID,
RoleLevel: member.RoleLevel,
JoinTime: member.JoinTime,
Nickname: member.Nickname,
FaceURL: member.FaceURL,
AppMangerLevel: 0,
JoinSource: member.JoinSource,
OperatorUserID: member.OperatorUserID,
Ex: member.Ex,
MuteEndTime: member.MuteEndTime,
InviterUserID: member.InviterUserID,
}
}
res := make([]*sdkws.GroupMemberFullInfo, 0, len(members))
for _, userID := range userIDs {
res = append(res, memberMap[userID])
}
val, _ := json.Marshal(res)
sum := md5.Sum(val)
return binary.BigEndian.Uint64(sum[:])
}
func (g *Group) SyncAllGroupMember(ctx context.Context, groupID string) error {
absInfo, err := g.GetGroupAbstractInfo(ctx, groupID)
if err != nil {
return err
}
localData, err := g.db.GetGroupMemberListSplit(ctx, groupID, 0, 0, 9999999)
if err != nil {
return err
}
hashCode := g.getGroupHash(localData)
if len(localData) == int(absInfo.GroupMemberNumber) && hashCode == absInfo.GroupMemberListHash {
log.ZDebug(ctx, "SyncAllGroupMember no change in personnel", "groupID", groupID, "hashCode", hashCode, "absInfo.GroupMemberListHash", absInfo.GroupMemberListHash)
return nil
}
members, err := g.GetServerGroupMembers(ctx, groupID)
if err != nil {
return err
}
return g.syncGroupMembers(ctx, groupID, members, localData)
}
func (g *Group) SyncAllGroupMember2(ctx context.Context, groupID string) error {
return g.IncrSyncGroupAndMember(ctx, groupID)
}
func (g *Group) syncGroupMembers(ctx context.Context, groupID string, members []*sdkws.GroupMemberFullInfo, localData []*model_struct.LocalGroupMember) error {
log.ZInfo(ctx, "SyncGroupMember Info", "groupID", groupID, "members", len(members), "localData", len(localData))
err := g.groupMemberSyncer.Sync(ctx, datautil.Batch(ServerGroupMemberToLocalGroupMember, members), localData, nil)
if err != nil {
return err
}
//if len(members) != len(localData) {
log.ZInfo(ctx, "SyncGroupMember Sync Group Member Count", "groupID", groupID, "members", len(members), "localData", len(localData))
gs, err := g.GetSpecifiedGroupsInfo(ctx, []string{groupID})
if err != nil {
return err
}
log.ZInfo(ctx, "SyncGroupMember GetGroupsInfo", "groupID", groupID, "len", len(gs), "gs", gs)
if len(gs) > 0 {
v := gs[0]
count, err := g.db.GetGroupMemberCount(ctx, groupID)
if err != nil {
return err
}
if v.MemberCount != count {
v.MemberCount = count
if v.GroupType == constant.SuperGroupChatType {
if err := g.db.UpdateSuperGroup(ctx, v); err != nil {
//return err
log.ZError(ctx, "SyncGroupMember UpdateSuperGroup", err, "groupID", groupID, "info", v)
}
} else {
if err := g.db.UpdateGroup(ctx, v); err != nil {
log.ZError(ctx, "SyncGroupMember UpdateGroup", err, "groupID", groupID, "info", v)
}
}
data, err := json.Marshal(v)
if err != nil {
return err
}
log.ZInfo(ctx, "SyncGroupMember OnGroupInfoChanged", "groupID", groupID, "data", string(data))
g.listener().OnGroupInfoChanged(string(data))
}
}
//}
return nil
}
func (g *Group) SyncGroupMembers(ctx context.Context, groupID string, userIDs ...string) error {
return g.IncrSyncGroupAndMember(ctx, groupID)
//members, err := g.GetDesignatedGroupMembers(ctx, groupID, userIDs)
//if err != nil {
// return err
//}
//localData, err := g.db.GetGroupSomeMemberInfo(ctx, groupID, userIDs)
//if err != nil {
// return err
//}
//return g.syncGroupMembers(ctx, groupID, members, localData)
}
func (g *Group) SyncGroups(ctx context.Context, groupIDs ...string) error {
return g.IncrSyncJoinGroup(ctx)
//groups, err := g.getGroupsInfoFromSvr(ctx, groupIDs)
//if err != nil {
// return err
//}
//localData, err := g.db.GetGroups(ctx, groupIDs)
//if err != nil {
// return err
//}
//if err := g.groupSyncer.Sync(ctx, util.Batch(ServerGroupToLocalGroup, groups), localData, nil); err != nil {
// return err
//}
//return nil
}
func (g *Group) deleteGroup(ctx context.Context, groupID string) error {
return g.IncrSyncJoinGroup(ctx)
//groupInfo, err := g.db.GetGroupInfoByGroupID(ctx, groupID)
//if err != nil {
// return err
//}
//if err := g.db.DeleteGroup(ctx, groupID); err != nil {
// return err
//}
//g.listener().OnJoinedGroupDeleted(utils.StructToJsonString(groupInfo))
//return nil
}
// func (g *Group) SyncAllJoinedGroupsAndMembers(ctx context.Context) error {
// t := time.Now()
// defer func(start time.Time) {
//
// elapsed := time.Since(start).Milliseconds()
// log.ZDebug(ctx, "SyncAllJoinedGroupsAndMembers fn call end", "cost time", fmt.Sprintf("%d ms", elapsed))
//
// }(t)
// _, err := g.syncAllJoinedGroups(ctx)
// if err != nil {
// return err
// }
// groups, err := g.db.GetJoinedGroupListDB(ctx)
// if err != nil {
// return err
// }
// var wg sync.WaitGroup
// for _, group := range groups {
// wg.Add(1)
// go func(groupID string) {
// defer wg.Done()
// if err := g.SyncAllGroupMember(ctx, groupID); err != nil {
// log.ZError(ctx, "SyncGroupMember failed", err)
// }
// }(group.GroupID)
// }
// wg.Wait()
// return nil
// }
func (g *Group) SyncAllJoinedGroupsAndMembers(ctx context.Context) error {
t := time.Now()
defer func(start time.Time) {
elapsed := time.Since(start).Milliseconds()
log.ZDebug(ctx, "SyncAllJoinedGroupsAndMembers fn call end", "cost time", fmt.Sprintf("%d ms", elapsed))
}(t)
if err := g.IncrSyncJoinGroup(ctx); err != nil {
return err
}
return g.IncrSyncJoinGroupMember(ctx)
}
func (g *Group) syncAllJoinedGroups(ctx context.Context) ([]*sdkws.GroupInfo, error) {
groups, err := g.GetServerJoinGroup(ctx)
if err != nil {
return nil, err
}
localData, err := g.db.GetJoinedGroupListDB(ctx)
if err != nil {
return nil, err
}
if err := g.groupSyncer.Sync(ctx, datautil.Batch(ServerGroupToLocalGroup, groups), localData, nil); err != nil {
return nil, err
}
return groups, nil
}
func (g *Group) SyncAllSelfGroupApplication(ctx context.Context) error {
list, err := g.GetServerSelfGroupApplication(ctx)
if err != nil {
return err
}
localData, err := g.db.GetSendGroupApplication(ctx)
if err != nil {
return err
}
if err := g.groupRequestSyncer.Sync(ctx, datautil.Batch(ServerGroupRequestToLocalGroupRequest, list), localData, nil); err != nil {
return err
}
// todo
return nil
}
func (g *Group) SyncSelfGroupApplications(ctx context.Context, groupIDs ...string) error {
return g.SyncAllSelfGroupApplication(ctx)
}
func (g *Group) SyncAllAdminGroupApplication(ctx context.Context) error {
requests, err := g.GetServerAdminGroupApplicationList(ctx)
if err != nil {
return err
}
localData, err := g.db.GetAdminGroupApplication(ctx)
if err != nil {
return err
}
return g.groupAdminRequestSyncer.Sync(ctx, datautil.Batch(ServerGroupRequestToLocalAdminGroupRequest, requests), localData, nil)
}
func (g *Group) SyncAdminGroupApplications(ctx context.Context, groupIDs ...string) error {
return g.SyncAllAdminGroupApplication(ctx)
}
func (g *Group) GetServerJoinGroup(ctx context.Context) ([]*sdkws.GroupInfo, error) {
fn := func(resp *group.GetJoinedGroupListResp) []*sdkws.GroupInfo { return resp.Groups }
req := &group.GetJoinedGroupListReq{FromUserID: g.loginUserID, Pagination: &sdkws.RequestPagination{}}
return util.GetPageAll(ctx, constant.GetJoinedGroupListRouter, req, fn)
}
func (g *Group) GetServerAdminGroupApplicationList(ctx context.Context) ([]*sdkws.GroupRequest, error) {
fn := func(resp *group.GetGroupApplicationListResp) []*sdkws.GroupRequest { return resp.GroupRequests }
req := &group.GetGroupApplicationListReq{FromUserID: g.loginUserID, Pagination: &sdkws.RequestPagination{}}
return util.GetPageAll(ctx, constant.GetRecvGroupApplicationListRouter, req, fn)
}
func (g *Group) GetServerSelfGroupApplication(ctx context.Context) ([]*sdkws.GroupRequest, error) {
fn := func(resp *group.GetGroupApplicationListResp) []*sdkws.GroupRequest { return resp.GroupRequests }
req := &group.GetUserReqApplicationListReq{UserID: g.loginUserID, Pagination: &sdkws.RequestPagination{}}
return util.GetPageAll(ctx, constant.GetSendGroupApplicationListRouter, req, fn)
}
func (g *Group) GetServerGroupMembers(ctx context.Context, groupID string) ([]*sdkws.GroupMemberFullInfo, error) {
req := &group.GetGroupMemberListReq{GroupID: groupID, Pagination: &sdkws.RequestPagination{}}
fn := func(resp *group.GetGroupMemberListResp) []*sdkws.GroupMemberFullInfo { return resp.Members }
return util.GetPageAll(ctx, constant.GetGroupMemberListRouter, req, fn)
}
func (g *Group) GetDesignatedGroupMembers(ctx context.Context, groupID string, userID []string) ([]*sdkws.GroupMemberFullInfo, error) {
resp := &group.GetGroupMembersInfoResp{}
if err := util.ApiPost(ctx, constant.GetGroupMembersInfoRouter, &group.GetGroupMembersInfoReq{GroupID: groupID, UserIDs: userID}, resp); err != nil {
return nil, err
}
return resp.Members, nil
}
func (g *Group) GetGroupAbstractInfo(ctx context.Context, groupID string) (*group.GroupAbstractInfo, error) {
resp, err := util.CallApi[group.GetGroupAbstractInfoResp](ctx, constant.GetGroupAbstractInfoRouter, &group.GetGroupAbstractInfoReq{GroupIDs: []string{groupID}})
if err != nil {
return nil, err
}
if len(resp.GroupAbstractInfos) == 0 {
return nil, errors.New("group not found")
}
return resp.GroupAbstractInfos[0], nil
}

View File

@@ -0,0 +1,347 @@
package group
import (
"context"
"sync"
"gorm.io/gorm"
"github.com/openimsdk/openim-sdk-core/v3/internal/incrversion"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
constantpb "github.com/openimsdk/protocol/constant"
"github.com/openimsdk/protocol/group"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
)
type BatchIncrementalReq struct {
UserID string `json:"user_id"`
List []*group.GetIncrementalGroupMemberReq `json:"list"`
}
type BatchIncrementalResp struct {
List map[string]*group.GetIncrementalGroupMemberResp `json:"list"`
}
func (g *Group) getIncrementalGroupMemberBatch(ctx context.Context, groups []*group.GetIncrementalGroupMemberReq) (map[string]*group.GetIncrementalGroupMemberResp, error) {
resp, err := util.CallApi[BatchIncrementalResp](ctx, constant.GetIncrementalGroupMemberBatch, &BatchIncrementalReq{UserID: g.loginUserID, List: groups})
if err != nil {
return nil, err
}
return resp.List, nil
}
func (g *Group) groupAndMemberVersionTableName() string {
return "local_group_entities_version"
}
func (g *Group) groupTableName() string {
return model_struct.LocalGroup{}.TableName()
}
func (g *Group) IncrSyncJoinGroupMember(ctx context.Context) error {
groups, err := g.db.GetJoinedGroupListDB(ctx)
if err != nil {
return err
}
groupIDs := datautil.Slice(groups, func(e *model_struct.LocalGroup) string {
return e.GroupID
})
return g.IncrSyncGroupAndMember(ctx, groupIDs...)
}
func (g *Group) IncrSyncGroupAndMember(ctx context.Context, groupIDs ...string) error {
var wg sync.WaitGroup
if len(groupIDs) == 0 {
return nil
}
const maxSyncNum = constantpb.MaxSyncPullNumber
groupIDSet := datautil.SliceSet(groupIDs)
var groups []*group.GetIncrementalGroupMemberReq
if len(groupIDs) > maxSyncNum {
groups = make([]*group.GetIncrementalGroupMemberReq, 0, maxSyncNum)
} else {
groups = make([]*group.GetIncrementalGroupMemberReq, 0, len(groupIDs))
}
for {
if len(groupIDSet) == 0 {
return nil
}
for groupID := range groupIDSet {
if len(groups) == cap(groups) {
break
}
req := group.GetIncrementalGroupMemberReq{
GroupID: groupID,
}
lvs, err := g.db.GetVersionSync(ctx, g.groupAndMemberVersionTableName(), groupID)
if err == nil {
req.VersionID = lvs.VersionID
req.Version = lvs.Version
} else if errs.Unwrap(err) != gorm.ErrRecordNotFound {
return err
}
groups = append(groups, &req)
}
groupVersion, err := g.getIncrementalGroupMemberBatch(ctx, groups)
if err != nil {
return err
}
groups = groups[:0]
for groupID, resp := range groupVersion {
tempResp := resp
tempGroupID := groupID
wg.Add(1)
go func() error {
if err := g.syncGroupAndMember(ctx, tempGroupID, tempResp); err != nil {
return err
}
wg.Done()
return nil
}()
delete(groupIDSet, tempGroupID)
}
wg.Wait()
num := len(groupIDSet)
_ = num
}
}
func (g *Group) syncGroupAndMember(ctx context.Context, groupID string, resp *group.GetIncrementalGroupMemberResp) error {
groupMemberSyncer := incrversion.VersionSynchronizer[*model_struct.LocalGroupMember, *group.GetIncrementalGroupMemberResp]{
Ctx: ctx,
DB: g.db,
TableName: g.groupAndMemberVersionTableName(),
EntityID: groupID,
Key: func(localGroupMember *model_struct.LocalGroupMember) string {
return localGroupMember.UserID
},
Local: func() ([]*model_struct.LocalGroupMember, error) {
return g.db.GetGroupMemberListByGroupID(ctx, groupID)
},
ServerVersion: func() *group.GetIncrementalGroupMemberResp {
return resp
},
Full: func(resp *group.GetIncrementalGroupMemberResp) bool {
return resp.Full
},
Version: func(resp *group.GetIncrementalGroupMemberResp) (string, uint64) {
return resp.VersionID, resp.Version
},
Delete: func(resp *group.GetIncrementalGroupMemberResp) []string {
return resp.Delete
},
Update: func(resp *group.GetIncrementalGroupMemberResp) []*model_struct.LocalGroupMember {
return datautil.Batch(ServerGroupMemberToLocalGroupMember, resp.Update)
},
Insert: func(resp *group.GetIncrementalGroupMemberResp) []*model_struct.LocalGroupMember {
return datautil.Batch(ServerGroupMemberToLocalGroupMember, resp.Insert)
},
ExtraData: func(resp *group.GetIncrementalGroupMemberResp) any {
return resp.Group
},
ExtraDataProcessor: func(ctx context.Context, data any) error {
groupInfo, ok := data.(*sdkws.GroupInfo)
if !ok {
return errs.New("group info type error")
}
if groupInfo == nil {
return nil
}
local, err := g.db.GetJoinedGroupListDB(ctx)
if err != nil {
return err
}
log.ZDebug(ctx, "group info", "groupInfo", groupInfo)
changes := datautil.Batch(ServerGroupToLocalGroup, []*sdkws.GroupInfo{groupInfo})
kv := datautil.SliceToMapAny(local, func(e *model_struct.LocalGroup) (string, *model_struct.LocalGroup) {
return e.GroupID, e
})
for i, change := range changes {
key := change.GroupID
kv[key] = changes[i]
}
server := datautil.Values(kv)
return g.groupSyncer.Sync(ctx, server, local, nil)
},
Syncer: func(server, local []*model_struct.LocalGroupMember) error {
return g.groupMemberSyncer.Sync(ctx, server, local, nil)
},
FullSyncer: func(ctx context.Context) error {
return g.groupMemberSyncer.FullSync(ctx, groupID)
},
FullID: func(ctx context.Context) ([]string, error) {
resp, err := util.CallApi[group.GetFullGroupMemberUserIDsResp](ctx, constant.GetFullGroupMemberUserIDs, &group.GetFullGroupMemberUserIDsReq{
GroupID: groupID,
})
if err != nil {
return nil, err
}
return resp.UserIDs, nil
},
}
return groupMemberSyncer.Sync()
}
func (g *Group) onlineSyncGroupAndMember(ctx context.Context, groupID string, deleteGroupMembers, updateGroupMembers, insertGroupMembers []*sdkws.GroupMemberFullInfo,
updateGroup *sdkws.GroupInfo, version uint64, versionID string) error {
groupMemberSyncer := incrversion.VersionSynchronizer[*model_struct.LocalGroupMember, *group.GetIncrementalGroupMemberResp]{
Ctx: ctx,
DB: g.db,
TableName: g.groupAndMemberVersionTableName(),
EntityID: groupID,
Key: func(localGroupMember *model_struct.LocalGroupMember) string {
return localGroupMember.UserID
},
Local: func() ([]*model_struct.LocalGroupMember, error) {
return g.db.GetGroupMemberListByGroupID(ctx, groupID)
},
ServerVersion: func() *group.GetIncrementalGroupMemberResp {
return &group.GetIncrementalGroupMemberResp{
Version: version,
VersionID: versionID,
Full: false,
Delete: datautil.Slice(deleteGroupMembers, func(e *sdkws.GroupMemberFullInfo) string {
return e.UserID
}),
Insert: insertGroupMembers,
Update: updateGroupMembers,
Group: updateGroup,
}
},
Server: func(version *model_struct.LocalVersionSync) (*group.GetIncrementalGroupMemberResp, error) {
singleGroupReq := &group.GetIncrementalGroupMemberReq{
GroupID: groupID,
VersionID: version.VersionID,
Version: version.Version,
}
resp, err := util.CallApi[BatchIncrementalResp](ctx, constant.GetIncrementalGroupMemberBatch,
&BatchIncrementalReq{UserID: g.loginUserID, List: []*group.GetIncrementalGroupMemberReq{singleGroupReq}})
if err != nil {
return nil, err
}
if resp.List != nil {
if singleGroupResp, ok := resp.List[groupID]; ok {
return singleGroupResp, nil
}
}
return nil, errs.New("group member version record not found")
},
Full: func(resp *group.GetIncrementalGroupMemberResp) bool {
return resp.Full
},
Version: func(resp *group.GetIncrementalGroupMemberResp) (string, uint64) {
return resp.VersionID, resp.Version
},
Delete: func(resp *group.GetIncrementalGroupMemberResp) []string {
return resp.Delete
},
Update: func(resp *group.GetIncrementalGroupMemberResp) []*model_struct.LocalGroupMember {
return datautil.Batch(ServerGroupMemberToLocalGroupMember, resp.Update)
},
Insert: func(resp *group.GetIncrementalGroupMemberResp) []*model_struct.LocalGroupMember {
return datautil.Batch(ServerGroupMemberToLocalGroupMember, resp.Insert)
},
ExtraData: func(resp *group.GetIncrementalGroupMemberResp) any {
return resp.Group
},
ExtraDataProcessor: func(ctx context.Context, data any) error {
groupInfo, ok := data.(*sdkws.GroupInfo)
if !ok {
return errs.New("group info type error")
}
if groupInfo == nil {
return nil
}
local, err := g.db.GetJoinedGroupListDB(ctx)
if err != nil {
return err
}
log.ZDebug(ctx, "group info", "groupInfo", groupInfo)
changes := datautil.Batch(ServerGroupToLocalGroup, []*sdkws.GroupInfo{groupInfo})
kv := datautil.SliceToMapAny(local, func(e *model_struct.LocalGroup) (string, *model_struct.LocalGroup) {
return e.GroupID, e
})
for i, change := range changes {
key := change.GroupID
kv[key] = changes[i]
}
server := datautil.Values(kv)
return g.groupSyncer.Sync(ctx, server, local, nil)
},
Syncer: func(server, local []*model_struct.LocalGroupMember) error {
return g.groupMemberSyncer.Sync(ctx, server, local, nil)
},
FullSyncer: func(ctx context.Context) error {
return g.groupMemberSyncer.FullSync(ctx, groupID)
},
FullID: func(ctx context.Context) ([]string, error) {
resp, err := util.CallApi[group.GetFullGroupMemberUserIDsResp](ctx, constant.GetFullGroupMemberUserIDs, &group.GetFullGroupMemberUserIDsReq{
GroupID: groupID,
})
if err != nil {
return nil, err
}
return resp.UserIDs, nil
},
}
return groupMemberSyncer.CheckVersionSync()
}
func (g *Group) IncrSyncJoinGroup(ctx context.Context) error {
opt := incrversion.VersionSynchronizer[*model_struct.LocalGroup, *group.GetIncrementalJoinGroupResp]{
Ctx: ctx,
DB: g.db,
TableName: g.groupTableName(),
EntityID: g.loginUserID,
Key: func(LocalGroup *model_struct.LocalGroup) string {
return LocalGroup.GroupID
},
Local: func() ([]*model_struct.LocalGroup, error) {
return g.db.GetJoinedGroupListDB(ctx)
},
Server: func(version *model_struct.LocalVersionSync) (*group.GetIncrementalJoinGroupResp, error) {
return util.CallApi[group.GetIncrementalJoinGroupResp](ctx, constant.GetIncrementalJoinGroup, &group.GetIncrementalJoinGroupReq{
UserID: g.loginUserID,
Version: version.Version,
VersionID: version.VersionID,
})
},
Full: func(resp *group.GetIncrementalJoinGroupResp) bool {
return resp.Full
},
Version: func(resp *group.GetIncrementalJoinGroupResp) (string, uint64) {
return resp.VersionID, resp.Version
},
Delete: func(resp *group.GetIncrementalJoinGroupResp) []string {
return resp.Delete
},
Update: func(resp *group.GetIncrementalJoinGroupResp) []*model_struct.LocalGroup {
return datautil.Batch(ServerGroupToLocalGroup, resp.Update)
},
Insert: func(resp *group.GetIncrementalJoinGroupResp) []*model_struct.LocalGroup {
return datautil.Batch(ServerGroupToLocalGroup, resp.Insert)
},
Syncer: func(server, local []*model_struct.LocalGroup) error {
return g.groupSyncer.Sync(ctx, server, local, nil)
},
FullSyncer: func(ctx context.Context) error {
return g.groupSyncer.FullSync(ctx, g.loginUserID)
},
FullID: func(ctx context.Context) ([]string, error) {
resp, err := util.CallApi[group.GetFullJoinGroupIDsResp](ctx, constant.GetFullJoinedGroupIDs, &group.GetFullJoinGroupIDsReq{
UserID: g.loginUserID,
})
if err != nil {
return nil, err
}
return resp.GroupIDs, nil
},
}
return opt.Sync()
}

View File

@@ -0,0 +1 @@
package group

View File

@@ -0,0 +1,265 @@
package incrversion
import (
"context"
"reflect"
"sort"
"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/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
"gorm.io/gorm"
)
type VersionSynchronizer[V, R any] struct {
Ctx context.Context
DB db_interface.VersionSyncModel
TableName string
EntityID string
Key func(V) string
Local func() ([]V, error)
ServerVersion func() R
Server func(version *model_struct.LocalVersionSync) (R, error)
Full func(resp R) bool
Version func(resp R) (string, uint64)
Delete func(resp R) []string
Update func(resp R) []V
Insert func(resp R) []V
ExtraData func(resp R) any
ExtraDataProcessor func(ctx context.Context, data any) error
Syncer func(server, local []V) error
FullSyncer func(ctx context.Context) error
FullID func(ctx context.Context) ([]string, error)
}
func (o *VersionSynchronizer[V, R]) getVersionInfo() (*model_struct.LocalVersionSync, error) {
versionInfo, err := o.DB.GetVersionSync(o.Ctx, o.TableName, o.EntityID)
if err != nil && errs.Unwrap(err) != gorm.ErrRecordNotFound {
log.ZWarn(o.Ctx, "get version info", err)
return nil, err
}
return versionInfo, nil
}
func (o *VersionSynchronizer[V, R]) updateVersionInfo(lvs *model_struct.LocalVersionSync, resp R) error {
lvs.Table = o.TableName
lvs.EntityID = o.EntityID
lvs.VersionID, lvs.Version = o.Version(resp)
return o.DB.SetVersionSync(o.Ctx, lvs)
}
func judgeInterfaceIsNil(data any) bool {
return reflect.ValueOf(data).Kind() == reflect.Ptr && reflect.ValueOf(data).IsNil()
}
func (o *VersionSynchronizer[V, R]) Sync() error {
var lvs *model_struct.LocalVersionSync
var resp R
var extraData any
if o.ServerVersion == nil {
var err error
lvs, err = o.getVersionInfo()
if err != nil {
return err
}
resp, err = o.Server(lvs)
if err != nil {
return err
}
} else {
var err error
lvs, err = o.getVersionInfo()
if err != nil {
return err
}
resp = o.ServerVersion()
}
delIDs := o.Delete(resp)
changes := o.Update(resp)
insert := o.Insert(resp)
if o.ExtraData != nil {
temp := o.ExtraData(resp)
if !judgeInterfaceIsNil(temp) {
extraData = temp
}
}
if len(delIDs) == 0 && len(changes) == 0 && len(insert) == 0 && !o.Full(resp) && extraData == nil {
log.ZDebug(o.Ctx, "no data to sync", "table", o.TableName, "entityID", o.EntityID)
return nil
}
if o.Full(resp) {
err := o.FullSyncer(o.Ctx)
if err != nil {
return err
}
lvs.UIDList, err = o.FullID(o.Ctx)
if err != nil {
return err
}
} else {
if len(delIDs) > 0 {
lvs.UIDList = DeleteElements(lvs.UIDList, delIDs)
}
if len(insert) > 0 {
lvs.UIDList = append(lvs.UIDList, datautil.Slice(insert, o.Key)...)
}
local, err := o.Local()
if err != nil {
return err
}
kv := datautil.SliceToMapAny(local, func(v V) (string, V) {
return o.Key(v), v
})
changes = append(changes, insert...)
for i, change := range changes {
key := o.Key(change)
kv[key] = changes[i]
}
for _, id := range delIDs {
delete(kv, id)
}
server := datautil.Values(kv)
if err := o.Syncer(server, local); err != nil {
return err
}
if extraData != nil && o.ExtraDataProcessor != nil {
if err := o.ExtraDataProcessor(o.Ctx, extraData); err != nil {
return err
}
}
}
return o.updateVersionInfo(lvs, resp)
}
func (o *VersionSynchronizer[V, R]) CheckVersionSync() error {
lvs, err := o.getVersionInfo()
if err != nil {
return err
}
var extraData any
resp := o.ServerVersion()
delIDs := o.Delete(resp)
changes := o.Update(resp)
insert := o.Insert(resp)
versionID, version := o.Version(resp)
if o.ExtraData != nil {
temp := o.ExtraData(resp)
if !judgeInterfaceIsNil(temp) {
extraData = temp
}
}
if len(delIDs) == 0 && len(changes) == 0 && len(insert) == 0 && !o.Full(resp) && extraData == nil {
log.ZWarn(o.Ctx, "exception no data to sync", errs.New("notification no data"), "table", o.TableName, "entityID", o.EntityID)
return nil
}
log.ZDebug(o.Ctx, "check version sync", "table", o.TableName, "entityID", o.EntityID, "versionID", versionID, "localVersionID", lvs.VersionID, "version", version, "localVersion", lvs.Version)
/// If the version unique ID cannot correspond with the local version,
// it indicates that the data might have been tampered with or an exception has occurred.
//Trigger the complete client-server incremental synchronization.
if versionID != lvs.VersionID {
log.ZDebug(o.Ctx, "version id not match", errs.New("version id not match"), "versionID", versionID, "localVersionID", lvs.VersionID)
o.ServerVersion = nil
return o.Sync()
}
if lvs.Version+1 == version {
if len(delIDs) > 0 {
lvs.UIDList = DeleteElements(lvs.UIDList, delIDs)
}
if len(insert) > 0 {
lvs.UIDList = append(lvs.UIDList, datautil.Slice(insert, o.Key)...)
}
local, err := o.Local()
if err != nil {
return err
}
kv := datautil.SliceToMapAny(local, func(v V) (string, V) {
return o.Key(v), v
})
changes = append(changes, insert...)
for i, change := range changes {
key := o.Key(change)
kv[key] = changes[i]
}
for _, id := range delIDs {
delete(kv, id)
}
server := datautil.Values(kv)
if err := o.Syncer(server, local); err != nil {
return err
}
if extraData != nil && o.ExtraDataProcessor != nil {
if err := o.ExtraDataProcessor(o.Ctx, extraData); err != nil {
return err
}
}
return o.updateVersionInfo(lvs, resp)
} else if version <= lvs.Version {
log.ZWarn(o.Ctx, "version less than local version", errs.New("version less than local version"),
"table", o.TableName, "entityID", o.EntityID, "version", version, "localVersion", lvs.Version)
return nil
} else {
// If the version number has a gap with the local version number,
//it indicates that some pushed data might be missing.
//Trigger the complete client-server incremental synchronization.
o.ServerVersion = nil
return o.Sync()
}
}
// DeleteElements 删除切片中包含在另一个切片中的元素,并保持切片顺序
func DeleteElements[E comparable](es []E, toDelete []E) []E {
// 将要删除的元素存储在哈希集合中
deleteSet := make(map[E]struct{}, len(toDelete))
for _, e := range toDelete {
deleteSet[e] = struct{}{}
}
// 通过一个索引 j 来跟踪新的切片位置
j := 0
for _, e := range es {
if _, found := deleteSet[e]; !found {
es[j] = e
j++
}
}
return es[:j]
}
// DeleteElement 删除切片中的指定元素,并保持切片顺序
func DeleteElement[E comparable](es []E, element E) []E {
j := 0
for _, e := range es {
if e != element {
es[j] = e
j++
}
}
return es[:j]
}
// Slice Converts slice types in batches and sorts the resulting slice using a custom comparator
func Slice[E any, T any](es []E, fn func(e E) T, less func(a, b T) bool) []T {
// 转换切片
v := make([]T, len(es))
for i := 0; i < len(es); i++ {
v[i] = fn(es[i])
}
// 排序切片
sort.Slice(v, func(i, j int) bool {
return less(v[i], v[j])
})
return v
}

View File

@@ -0,0 +1,61 @@
// 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 interaction
import (
"bytes"
"compress/gzip"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"io"
)
type Compressor interface {
Compress(rawData []byte) ([]byte, error)
DeCompress(compressedData []byte) ([]byte, error)
}
type GzipCompressor struct {
compressProtocol string
}
func NewGzipCompressor() *GzipCompressor {
return &GzipCompressor{compressProtocol: "gzip"}
}
func (g *GzipCompressor) Compress(rawData []byte) ([]byte, error) {
gzipBuffer := bytes.Buffer{}
gz := gzip.NewWriter(&gzipBuffer)
if _, err := gz.Write(rawData); err != nil {
return nil, utils.Wrap(err, "")
}
if err := gz.Close(); err != nil {
return nil, utils.Wrap(err, "")
}
return gzipBuffer.Bytes(), nil
}
func (g *GzipCompressor) DeCompress(compressedData []byte) ([]byte, error) {
buff := bytes.NewBuffer(compressedData)
reader, err := gzip.NewReader(buff)
if err != nil {
return nil, utils.Wrap(err, "NewReader failed")
}
compressedData, err = io.ReadAll(reader)
if err != nil {
return nil, utils.Wrap(err, "ReadAll failed")
}
_ = reader.Close()
return compressedData, nil
}

View File

@@ -0,0 +1,39 @@
// 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 interaction
const (
WebSocket = iota
Tcp
)
const (
// MessageText is for UTF-8 encoded text messages like JSON.
MessageText = iota + 1
// MessageBinary is for binary messages like protobufs.
MessageBinary
// CloseMessage denotes a close control message. The optional message
// payload contains a numeric code and text. Use the FormatCloseMessage
// function to format a close message payload.
CloseMessage = 8
// PingMessage denotes a ping control message. The optional message payload
// is UTF-8 encoded text.
PingMessage = 9
// PongMessage denotes a pong control message. The optional message payload
// is UTF-8 encoded text.
PongMessage = 10
)

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 interaction
import (
"time"
"github.com/openimsdk/protocol/constant"
)
type ConnContext struct {
RemoteAddr string
}
func (c *ConnContext) Deadline() (deadline time.Time, ok bool) {
return
}
func (c *ConnContext) Done() <-chan struct{} {
return nil
}
func (c *ConnContext) Err() error {
return nil
}
func (c *ConnContext) Value(key any) any {
switch key {
case constant.RemoteAddr:
return c.RemoteAddr
default:
return ""
}
}
func newContext(remoteAddr string) *ConnContext {
return &ConnContext{
RemoteAddr: remoteAddr,
}
}

View File

@@ -0,0 +1,51 @@
// 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 interaction
import (
"bytes"
"encoding/gob"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
)
type Encoder interface {
Encode(data interface{}) ([]byte, error)
Decode(encodeData []byte, decodeData interface{}) error
}
type GobEncoder struct {
}
func NewGobEncoder() *GobEncoder {
return &GobEncoder{}
}
func (g *GobEncoder) Encode(data interface{}) ([]byte, error) {
buff := bytes.Buffer{}
enc := gob.NewEncoder(&buff)
err := enc.Encode(data)
if err != nil {
return nil, err
}
return buff.Bytes(), nil
}
func (g *GobEncoder) Decode(encodeData []byte, decodeData interface{}) error {
buff := bytes.NewBuffer(encodeData)
dec := gob.NewDecoder(buff)
err := dec.Decode(decodeData)
if err != nil {
return utils.Wrap(err, "")
}
return nil
}

View File

@@ -0,0 +1,579 @@
// 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 interaction
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/open_im_sdk_callback"
"github.com/openimsdk/openim-sdk-core/v3/pkg/ccontext"
"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"
"io"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/golang/protobuf/proto"
"github.com/gorilla/websocket"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 30 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 1024 * 1024
//Maximum number of reconnection attempts
maxReconnectAttempts = 300
)
const (
DefaultNotConnect = iota
Closed = iota + 1
Connecting
Connected
)
var (
newline = []byte{'\n'}
space = []byte{' '}
)
var (
ErrChanClosed = errors.New("send channel closed")
ErrConnClosed = errors.New("conn has closed")
ErrNotSupportMessageProtocol = errors.New("not support message protocol")
ErrClientClosed = errors.New("client actively close the connection")
ErrPanic = errors.New("panic error")
)
type LongConnMgr struct {
//conn status mutex
w sync.Mutex
connStatus int
// The long connection,can be set tcp or websocket.
conn LongConn
listener open_im_sdk_callback.OnConnListener
// Buffered channel of outbound messages.
send chan Message
pushMsgAndMaxSeqCh chan common.Cmd2Value
conversationCh chan common.Cmd2Value
loginMgrCh chan common.Cmd2Value
heartbeatCh chan common.Cmd2Value
closedErr error
ctx context.Context
IsCompression bool
Syncer *WsRespAsyn
encoder Encoder
compressor Compressor
reconnectStrategy ReconnectStrategy
mutex sync.Mutex
IsBackground bool
// write conn lock
connWrite *sync.Mutex
}
type Message struct {
Message GeneralWsReq
Resp chan *GeneralWsResp
}
func NewLongConnMgr(ctx context.Context, listener open_im_sdk_callback.OnConnListener, heartbeatCmdCh, pushMsgAndMaxSeqCh, loginMgrCh chan common.Cmd2Value) *LongConnMgr {
l := &LongConnMgr{listener: listener, pushMsgAndMaxSeqCh: pushMsgAndMaxSeqCh,
loginMgrCh: loginMgrCh, IsCompression: true,
Syncer: NewWsRespAsyn(), encoder: NewGobEncoder(), compressor: NewGzipCompressor(),
reconnectStrategy: NewExponentialRetry()}
l.send = make(chan Message, 10)
l.conn = NewWebSocket(WebSocket)
l.connWrite = new(sync.Mutex)
l.ctx = ctx
l.heartbeatCh = heartbeatCmdCh
return l
}
func (c *LongConnMgr) Run(ctx context.Context) {
//fmt.Println(mcontext.GetOperationID(ctx), "login run", string(debug.Stack()))
go c.readPump(ctx)
go c.writePump(ctx)
go c.heartbeat(ctx)
}
func (c *LongConnMgr) SendReqWaitResp(ctx context.Context, m proto.Message, reqIdentifier int, resp proto.Message) error {
data, err := proto.Marshal(m)
if err != nil {
return sdkerrs.ErrArgs
}
msg := Message{
Message: GeneralWsReq{
ReqIdentifier: reqIdentifier,
SendID: ccontext.Info(ctx).UserID(),
OperationID: ccontext.Info(ctx).OperationID(),
Data: data,
},
Resp: make(chan *GeneralWsResp, 1),
}
c.send <- msg
log.ZDebug(ctx, "send message to send channel success", "msg", m, "reqIdentifier", reqIdentifier)
select {
case <-ctx.Done():
return sdkerrs.ErrCtxDeadline
case v, ok := <-msg.Resp:
if !ok {
return errors.New("response channel closed")
}
if v.ErrCode != 0 {
return errs.NewCodeError(v.ErrCode, v.ErrMsg)
}
if err := proto.Unmarshal(v.Data, resp); err != nil {
return sdkerrs.ErrArgs
}
return nil
}
}
// readPump pumps messages from the websocket connection to the hub.
//
// The application runs readPump in a per-connection goroutine. The application
// ensures that there is at most one reader on a connection by executing all
// reads from this goroutine.
func (c *LongConnMgr) readPump(ctx context.Context) {
log.ZDebug(ctx, "readPump start", "goroutine ID:", getGoroutineID())
defer func() {
_ = c.close()
log.ZWarn(c.ctx, "readPump closed", c.closedErr)
}()
connNum := 0
//c.conn.SetPongHandler(function(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
ctx = ccontext.WithOperationID(ctx, utils.OperationIDGenerator())
needRecon, err := c.reConn(ctx, &connNum)
if !needRecon {
c.closedErr = err
return
}
if err != nil {
log.ZWarn(c.ctx, "reConn", err)
time.Sleep(c.reconnectStrategy.GetSleepInterval())
continue
}
c.conn.SetReadLimit(maxMessageSize)
_ = c.conn.SetReadDeadline(pongWait)
messageType, message, err := c.conn.ReadMessage()
if err != nil {
//if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
// log.Printf("error: %v", err)
//}
//break
//c.closedErr = err
log.ZError(c.ctx, "readMessage err", err, "goroutine ID:", getGoroutineID())
_ = c.close()
continue
}
switch messageType {
case MessageBinary:
err := c.handleMessage(message)
if err != nil {
c.closedErr = err
return
}
case MessageText:
c.closedErr = ErrNotSupportMessageProtocol
return
//case PingMessage:
// err := c.writePongMsg()
// log.ZError(c.ctx, "writePongMsg", err)
case CloseMessage:
c.closedErr = ErrClientClosed
return
default:
}
}
}
// writePump pumps messages from the hub to the websocket connection.
//
// A goroutine running writePump is started for each connection. The
// application ensures that there is at most one writer to a connection by
// executing all writes from this goroutine.
func (c *LongConnMgr) writePump(ctx context.Context) {
log.ZDebug(ctx, "writePump start", "goroutine ID:", getGoroutineID())
defer func() {
c.close()
close(c.send)
}()
for {
select {
case <-ctx.Done():
c.closedErr = ctx.Err()
return
case message, ok := <-c.send:
if !ok {
// The hub closed the channel.
_ = c.conn.SetWriteDeadline(writeWait)
err := c.conn.WriteMessage(websocket.CloseMessage, []byte{})
if err != nil {
log.ZError(c.ctx, "send close message error", err)
}
c.closedErr = ErrChanClosed
return
}
log.ZDebug(c.ctx, "writePump recv message", "reqIdentifier", message.Message.ReqIdentifier,
"operationID", message.Message.OperationID, "sendID", message.Message.SendID)
resp, err := c.sendAndWaitResp(&message.Message)
if err != nil {
resp = &GeneralWsResp{
ReqIdentifier: message.Message.ReqIdentifier,
OperationID: message.Message.OperationID,
Data: nil,
}
if code, ok := errs.Unwrap(err).(errs.CodeError); ok {
resp.ErrCode = code.Code()
resp.ErrMsg = code.Msg()
} else {
log.ZError(c.ctx, "writeBinaryMsgAndRetry failed", err, "wsReq", message.Message)
}
}
nErr := c.Syncer.notifyCh(message.Resp, resp, 1)
if nErr != nil {
log.ZError(c.ctx, "TriggerCmdNewMsgCome failed", nErr, "wsResp", resp)
}
}
}
}
func (c *LongConnMgr) heartbeat(ctx context.Context) {
log.ZDebug(ctx, "heartbeat start", "goroutine ID:", getGoroutineID())
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
log.ZWarn(c.ctx, "heartbeat closed", nil, "heartbeat", "heartbeat done sdk logout.....")
}()
for {
select {
case <-ctx.Done():
log.ZInfo(ctx, "heartbeat done sdk logout.....")
return
case <-c.heartbeatCh:
c.sendPingToServer(ctx)
case <-ticker.C:
c.sendPingToServer(ctx)
}
}
}
func getGoroutineID() int64 {
buf := make([]byte, 64)
buf = buf[:runtime.Stack(buf, false)]
idField := strings.Fields(strings.TrimPrefix(string(buf), "goroutine "))[0]
id, err := strconv.ParseInt(idField, 10, 64)
if err != nil {
panic(fmt.Sprintf("cannot get goroutine id: %v", err))
}
return id
}
func (c *LongConnMgr) sendPingToServer(ctx context.Context) {
if c.conn == nil {
return
}
var m sdkws.GetMaxSeqReq
m.UserID = ccontext.Info(ctx).UserID()
opID := utils.OperationIDGenerator()
sCtx := ccontext.WithOperationID(c.ctx, opID)
log.ZInfo(sCtx, "ping and getMaxSeq start", "goroutine ID:", getGoroutineID())
data, err := proto.Marshal(&m)
if err != nil {
log.ZError(sCtx, "proto.Marshal", err)
return
}
req := &GeneralWsReq{
ReqIdentifier: constant.GetNewestSeq,
SendID: m.UserID,
OperationID: opID,
Data: data,
}
resp, err := c.sendAndWaitResp(req)
if err != nil {
log.ZError(sCtx, "sendAndWaitResp", err)
_ = c.close()
time.Sleep(time.Second * 1)
return
} else {
if resp.ErrCode != 0 {
log.ZError(sCtx, "getMaxSeq failed", nil, "errCode:", resp.ErrCode, "errMsg:", resp.ErrMsg)
}
var wsSeqResp sdkws.GetMaxSeqResp
err = proto.Unmarshal(resp.Data, &wsSeqResp)
if err != nil {
log.ZError(sCtx, "proto.Unmarshal", err)
}
var cmd sdk_struct.CmdMaxSeqToMsgSync
cmd.ConversationMaxSeqOnSvr = wsSeqResp.MaxSeqs
err := common.TriggerCmdMaxSeq(sCtx, &cmd, c.pushMsgAndMaxSeqCh)
if err != nil {
log.ZError(sCtx, "TriggerCmdMaxSeq failed", err)
}
}
}
func (c *LongConnMgr) sendAndWaitResp(msg *GeneralWsReq) (*GeneralWsResp, error) {
tempChan, err := c.writeBinaryMsgAndRetry(msg)
defer c.Syncer.DelCh(msg.MsgIncr)
if err != nil {
return nil, err
} else {
select {
case resp := <-tempChan:
return resp, nil
case <-time.After(time.Second * 5):
return nil, sdkerrs.ErrNetworkTimeOut
}
}
}
func (c *LongConnMgr) writeBinaryMsgAndRetry(msg *GeneralWsReq) (chan *GeneralWsResp, error) {
msgIncr, tempChan := c.Syncer.AddCh(msg.SendID)
msg.MsgIncr = msgIncr
if c.GetConnectionStatus() != Connected && msg.ReqIdentifier == constant.GetNewestSeq {
return tempChan, sdkerrs.ErrNetwork.WrapMsg("connection closed,conning...")
}
for i := 0; i < maxReconnectAttempts; i++ {
err := c.writeBinaryMsg(*msg)
if err != nil {
log.ZError(c.ctx, "send binary message error", err, "message", msg)
c.closedErr = err
_ = c.close()
time.Sleep(time.Second * 1)
continue
} else {
return tempChan, nil
}
}
return nil, sdkerrs.ErrNetwork.WrapMsg("send binary message error")
}
func (c *LongConnMgr) writeBinaryMsg(req GeneralWsReq) error {
c.connWrite.Lock()
defer c.connWrite.Unlock()
encodeBuf, err := c.encoder.Encode(req)
if err != nil {
return err
}
if c.GetConnectionStatus() != Connected {
return sdkerrs.ErrNetwork.WrapMsg("connection closed,re conning...")
}
_ = c.conn.SetWriteDeadline(writeWait)
if c.IsCompression {
resultBuf, compressErr := c.compressor.Compress(encodeBuf)
if compressErr != nil {
return compressErr
}
return c.conn.WriteMessage(MessageBinary, resultBuf)
} else {
return c.conn.WriteMessage(MessageBinary, encodeBuf)
}
}
func (c *LongConnMgr) close() error {
c.w.Lock()
defer c.w.Unlock()
if c.connStatus == Closed || c.connStatus == Connecting || c.connStatus == DefaultNotConnect {
return nil
}
c.connStatus = Closed
log.ZWarn(c.ctx, "conn closed", c.closedErr)
return c.conn.Close()
}
func (c *LongConnMgr) handleMessage(message []byte) error {
if c.IsCompression {
var decompressErr error
message, decompressErr = c.compressor.DeCompress(message)
if decompressErr != nil {
log.ZError(c.ctx, "DeCompress failed", decompressErr, message)
return sdkerrs.ErrMsgDeCompression
}
}
var wsResp GeneralWsResp
err := c.encoder.Decode(message, &wsResp)
if err != nil {
log.ZError(c.ctx, "decodeBinaryWs err", err, "message", message)
return sdkerrs.ErrMsgDecodeBinaryWs
}
ctx := context.WithValue(c.ctx, "operationID", wsResp.OperationID)
log.ZInfo(ctx, "recv msg", "errCode", wsResp.ErrCode, "errMsg", wsResp.ErrMsg,
"reqIdentifier", wsResp.ReqIdentifier)
switch wsResp.ReqIdentifier {
case constant.PushMsg:
if err = c.doPushMsg(ctx, wsResp); err != nil {
log.ZError(ctx, "doWSPushMsg failed", err, "wsResp", wsResp)
}
case constant.LogoutMsg:
if err := c.Syncer.NotifyResp(ctx, wsResp); err != nil {
log.ZError(ctx, "notifyResp failed", err, "wsResp", wsResp)
}
return sdkerrs.ErrLoginOut
case constant.KickOnlineMsg:
log.ZDebug(ctx, "client kicked offline")
c.listener.OnKickedOffline()
_ = common.TriggerCmdLogOut(ctx, c.loginMgrCh)
return errors.New("client kicked offline")
case constant.GetNewestSeq:
fallthrough
case constant.PullMsgBySeqList:
fallthrough
case constant.SendMsg:
fallthrough
case constant.SendSignalMsg:
fallthrough
case constant.SetBackgroundStatus:
if err := c.Syncer.NotifyResp(ctx, wsResp); err != nil {
log.ZError(ctx, "notifyResp failed", err, "reqIdentifier", wsResp.ReqIdentifier, "errCode",
wsResp.ErrCode, "errMsg", wsResp.ErrMsg, "msgIncr", wsResp.MsgIncr, "operationID", wsResp.OperationID)
}
default:
// log.Error(wsResp.OperationID, "type failed, ", wsResp.ReqIdentifier)
return sdkerrs.ErrMsgBinaryTypeNotSupport
}
return nil
}
func (c *LongConnMgr) IsConnected() bool {
c.w.Lock()
defer c.w.Unlock()
if c.connStatus == Connected {
return true
}
return false
}
func (c *LongConnMgr) GetConnectionStatus() int {
c.w.Lock()
defer c.w.Unlock()
return c.connStatus
}
func (c *LongConnMgr) SetConnectionStatus(status int) {
c.w.Lock()
defer c.w.Unlock()
c.connStatus = status
}
func (c *LongConnMgr) reConn(ctx context.Context, num *int) (needRecon bool, err error) {
if c.IsConnected() {
return true, nil
}
c.connWrite.Lock()
defer c.connWrite.Unlock()
log.ZDebug(ctx, "conn start")
c.listener.OnConnecting()
c.SetConnectionStatus(Connecting)
url := fmt.Sprintf("%s?sendID=%s&token=%s&platformID=%d&operationID=%s&isBackground=%t",
ccontext.Info(ctx).WsAddr(), ccontext.Info(ctx).UserID(), ccontext.Info(ctx).Token(),
ccontext.Info(ctx).PlatformID(), ccontext.Info(ctx).OperationID(), c.GetBackground())
if c.IsCompression {
url += fmt.Sprintf("&compression=%s", "gzip")
}
resp, err := c.conn.Dial(url, nil)
if err != nil {
c.SetConnectionStatus(Closed)
if resp != nil {
body, err := io.ReadAll(resp.Body)
if err != nil {
return true, err
}
log.ZInfo(ctx, "reConn resp", "body", string(body))
var apiResp struct {
ErrCode int `json:"errCode"`
ErrMsg string `json:"errMsg"`
ErrDlt string `json:"errDlt"`
}
if err := json.Unmarshal(body, &apiResp); err != nil {
return true, err
}
err = errs.NewCodeError(apiResp.ErrCode, apiResp.ErrMsg).WithDetail(apiResp.ErrDlt).Wrap()
ccontext.GetApiErrCodeCallback(ctx).OnError(ctx, err)
switch apiResp.ErrCode {
case
errs.TokenExpiredError,
errs.TokenMalformedError,
errs.TokenNotValidYetError,
errs.TokenUnknownError:
return false, err
default:
return true, err
}
}
c.listener.OnConnectFailed(sdkerrs.NetworkError, err.Error())
return true, err
}
c.listener.OnConnectSuccess()
c.ctx = newContext(c.conn.LocalAddr())
c.ctx = context.WithValue(ctx, "ConnContext", c.ctx)
c.SetConnectionStatus(Connected)
*num++
log.ZInfo(c.ctx, "long conn establish success", "localAddr", c.conn.LocalAddr(), "connNum", *num)
c.reconnectStrategy.Reset()
_ = common.TriggerCmdConnected(ctx, c.pushMsgAndMaxSeqCh)
return true, nil
}
func (c *LongConnMgr) doPushMsg(ctx context.Context, wsResp GeneralWsResp) error {
var msg sdkws.PushMessages
err := proto.Unmarshal(wsResp.Data, &msg)
if err != nil {
return err
}
return common.TriggerCmdPushMsg(ctx, &msg, c.pushMsgAndMaxSeqCh)
}
func (c *LongConnMgr) Close(ctx context.Context) {
if c.GetConnectionStatus() == Connected {
log.ZInfo(ctx, "network change conn close")
c.closedErr = errors.New("closed by client network change")
_ = c.close()
} else {
log.ZInfo(ctx, "conn already closed")
}
}
func (c *LongConnMgr) GetBackground() bool {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.IsBackground
}
func (c *LongConnMgr) SetBackground(isBackground bool) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.IsBackground = isBackground
}

View File

@@ -0,0 +1,44 @@
// 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 interaction
import (
"net/http"
"time"
)
type PongHandler func(string) error
type LongConn interface {
//Close this connection
Close() error
// WriteMessage Write message to connection,messageType means data type,can be set binary(2) and text(1).
WriteMessage(messageType int, message []byte) error
// ReadMessage Read message from connection.
ReadMessage() (int, []byte, error)
// SetReadDeadline sets the read deadline on the underlying network connection,
//after a read has timed out, will return an error.
SetReadDeadline(timeout time.Duration) error
// SetWriteDeadline sets to write deadline when send message,when read has timed out,will return error.
SetWriteDeadline(timeout time.Duration) error
// Dial Try to dial a connection,url must set auth args,header can control compress data
Dial(urlStr string, requestHeader http.Header) (*http.Response, error)
// IsNil Whether the connection of the current long connection is nil
IsNil() bool
// SetReadLimit sets the maximum size for a message read from the peer.bytes
SetReadLimit(limit int64)
SetPongHandler(handler PongHandler)
// LocalAddr returns the local network address.
LocalAddr() string
}

View File

@@ -0,0 +1,387 @@
// 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 interaction
import (
"context"
"strings"
"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/sdk_struct"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
const (
connectPullNums = 1
defaultPullNums = 10
SplitPullMsgNum = 100
)
// The callback synchronization starts. The reconnection ends
type MsgSyncer struct {
loginUserID string // login user ID
longConnMgr *LongConnMgr // long connection manager
PushMsgAndMaxSeqCh chan common.Cmd2Value // channel for receiving push messages and the maximum SEQ number
conversationCh chan common.Cmd2Value // storage and session triggering
syncedMaxSeqs map[string]int64 // map of the maximum synced SEQ numbers for all group IDs
db db_interface.DataBase // data store
syncTimes int // times of sync
ctx context.Context // context
reinstalled bool //true if the app was uninstalled and reinstalled
}
// NewMsgSyncer creates a new instance of the message synchronizer.
func NewMsgSyncer(ctx context.Context, conversationCh, PushMsgAndMaxSeqCh chan common.Cmd2Value,
loginUserID string, longConnMgr *LongConnMgr, db db_interface.DataBase, syncTimes int) (*MsgSyncer, error) {
m := &MsgSyncer{
loginUserID: loginUserID,
longConnMgr: longConnMgr,
PushMsgAndMaxSeqCh: PushMsgAndMaxSeqCh,
conversationCh: conversationCh,
ctx: ctx,
syncedMaxSeqs: make(map[string]int64),
db: db,
syncTimes: syncTimes,
}
if err := m.loadSeq(ctx); err != nil {
log.ZError(ctx, "loadSeq err", err)
return nil, err
}
return m, nil
}
// seq The db reads the data to the memory,set syncedMaxSeqs
func (m *MsgSyncer) loadSeq(ctx context.Context) error {
conversationIDList, err := m.db.GetAllConversationIDList(ctx)
if err != nil {
log.ZError(ctx, "get conversation id list failed", err)
return err
}
if len(conversationIDList) == 0 {
m.reinstalled = true
}
//TODO With a large number of sessions, this could potentially cause blocking and needs optimization.
for _, v := range conversationIDList {
maxSyncedSeq, err := m.db.GetConversationNormalMsgSeq(ctx, v)
if err != nil {
log.ZError(ctx, "get group normal seq failed", err, "conversationID", v)
} else {
m.syncedMaxSeqs[v] = maxSyncedSeq
}
}
notificationSeqs, err := m.db.GetNotificationAllSeqs(ctx)
if err != nil {
log.ZError(ctx, "get notification seq failed", err)
return err
}
for _, notificationSeq := range notificationSeqs {
m.syncedMaxSeqs[notificationSeq.ConversationID] = notificationSeq.Seq
}
log.ZDebug(ctx, "loadSeq", "syncedMaxSeqs", m.syncedMaxSeqs)
return nil
}
// DoListener Listen to the message pipe of the message synchronizer
// and process received and pushed messages
func (m *MsgSyncer) DoListener(ctx context.Context) {
for {
select {
case cmd := <-m.PushMsgAndMaxSeqCh:
m.handlePushMsgAndEvent(cmd)
case <-ctx.Done():
log.ZInfo(m.ctx, "msg syncer done, sdk logout.....")
return
}
}
}
// get seqs need sync interval
func (m *MsgSyncer) getSeqsNeedSync(syncedMaxSeq, maxSeq int64) []int64 {
var seqs []int64
for i := syncedMaxSeq + 1; i <= maxSeq; i++ {
seqs = append(seqs, i)
}
return seqs
}
// recv msg from
func (m *MsgSyncer) handlePushMsgAndEvent(cmd common.Cmd2Value) {
switch cmd.Cmd {
case constant.CmdConnSuccesss:
log.ZInfo(cmd.Ctx, "recv long conn mgr connected", "cmd", cmd.Cmd, "value", cmd.Value)
m.doConnected(cmd.Ctx)
case constant.CmdMaxSeq:
log.ZInfo(cmd.Ctx, "recv max seqs from long conn mgr, start sync msgs", "cmd", cmd.Cmd, "value", cmd.Value)
m.compareSeqsAndBatchSync(cmd.Ctx, cmd.Value.(*sdk_struct.CmdMaxSeqToMsgSync).ConversationMaxSeqOnSvr, defaultPullNums)
case constant.CmdPushMsg:
m.doPushMsg(cmd.Ctx, cmd.Value.(*sdkws.PushMessages))
}
}
func (m *MsgSyncer) compareSeqsAndBatchSync(ctx context.Context, maxSeqToSync map[string]int64, pullNums int64) {
needSyncSeqMap := make(map[string][2]int64)
//when app reinstalled do not pull notifications messages.
if m.reinstalled {
notificationsSeqMap := make(map[string]int64)
messagesSeqMap := make(map[string]int64)
for conversationID, seq := range maxSeqToSync {
if IsNotification(conversationID) {
notificationsSeqMap[conversationID] = seq
} else {
messagesSeqMap[conversationID] = seq
}
}
for conversationID, seq := range notificationsSeqMap {
err := m.db.SetNotificationSeq(ctx, conversationID, seq)
if err != nil {
log.ZWarn(ctx, "SetNotificationSeq err", err, "conversationID", conversationID, "seq", seq)
continue
} else {
m.syncedMaxSeqs[conversationID] = seq
}
}
for conversationID, maxSeq := range messagesSeqMap {
if syncedMaxSeq, ok := m.syncedMaxSeqs[conversationID]; ok {
if maxSeq > syncedMaxSeq {
needSyncSeqMap[conversationID] = [2]int64{syncedMaxSeq + 1, maxSeq}
}
} else {
needSyncSeqMap[conversationID] = [2]int64{0, maxSeq}
}
}
m.reinstalled = false
} else {
for conversationID, maxSeq := range maxSeqToSync {
if syncedMaxSeq, ok := m.syncedMaxSeqs[conversationID]; ok {
if maxSeq > syncedMaxSeq {
needSyncSeqMap[conversationID] = [2]int64{syncedMaxSeq + 1, maxSeq}
}
} else {
needSyncSeqMap[conversationID] = [2]int64{0, maxSeq}
}
}
}
_ = m.syncAndTriggerMsgs(m.ctx, needSyncSeqMap, pullNums)
}
func (m *MsgSyncer) doPushMsg(ctx context.Context, push *sdkws.PushMessages) {
log.ZDebug(ctx, "push msgs", "push", push, "syncedMaxSeqs", m.syncedMaxSeqs)
m.pushTriggerAndSync(ctx, push.Msgs, m.triggerConversation)
m.pushTriggerAndSync(ctx, push.NotificationMsgs, m.triggerNotification)
}
func (m *MsgSyncer) pushTriggerAndSync(ctx context.Context, pullMsgs map[string]*sdkws.PullMsgs, triggerFunc func(ctx context.Context, msgs map[string]*sdkws.PullMsgs) error) {
if len(pullMsgs) == 0 {
return
}
needSyncSeqMap := make(map[string][2]int64)
var lastSeq int64
var storageMsgs []*sdkws.MsgData
for conversationID, msgs := range pullMsgs {
for _, msg := range msgs.Msgs {
if msg.Seq == 0 {
_ = triggerFunc(ctx, map[string]*sdkws.PullMsgs{conversationID: {Msgs: []*sdkws.MsgData{msg}}})
continue
}
lastSeq = msg.Seq
storageMsgs = append(storageMsgs, msg)
}
if lastSeq == m.syncedMaxSeqs[conversationID]+int64(len(storageMsgs)) && lastSeq != 0 {
log.ZDebug(ctx, "trigger msgs", "msgs", storageMsgs)
_ = triggerFunc(ctx, map[string]*sdkws.PullMsgs{conversationID: {Msgs: storageMsgs}})
m.syncedMaxSeqs[conversationID] = lastSeq
} else if lastSeq != 0 && lastSeq > m.syncedMaxSeqs[conversationID] {
//must pull message when message type is notification
needSyncSeqMap[conversationID] = [2]int64{m.syncedMaxSeqs[conversationID] + 1, lastSeq}
}
}
m.syncAndTriggerMsgs(ctx, needSyncSeqMap, defaultPullNums)
}
// Called after successful reconnection to synchronize the latest message
func (m *MsgSyncer) doConnected(ctx context.Context) {
common.TriggerCmdNotification(m.ctx, sdk_struct.CmdNewMsgComeToConversation{SyncFlag: constant.MsgSyncBegin}, m.conversationCh)
var resp sdkws.GetMaxSeqResp
if err := m.longConnMgr.SendReqWaitResp(m.ctx, &sdkws.GetMaxSeqReq{UserID: m.loginUserID}, constant.GetNewestSeq, &resp); err != nil {
log.ZError(m.ctx, "get max seq error", err)
common.TriggerCmdNotification(m.ctx, sdk_struct.CmdNewMsgComeToConversation{SyncFlag: constant.MsgSyncFailed}, m.conversationCh)
return
} else {
log.ZDebug(m.ctx, "get max seq success", "resp", resp)
}
m.compareSeqsAndBatchSync(ctx, resp.MaxSeqs, connectPullNums)
common.TriggerCmdNotification(m.ctx, sdk_struct.CmdNewMsgComeToConversation{SyncFlag: constant.MsgSyncEnd}, m.conversationCh)
}
func IsNotification(conversationID string) bool {
return strings.HasPrefix(conversationID, "n_")
}
// Fragment synchronization message, seq refresh after successful trigger
func (m *MsgSyncer) syncAndTriggerMsgs(ctx context.Context, seqMap map[string][2]int64, syncMsgNum int64) error {
if len(seqMap) > 0 {
log.ZDebug(ctx, "current sync seqMap", "seqMap", seqMap)
tempSeqMap := make(map[string][2]int64, 50)
msgNum := 0
for k, v := range seqMap {
oneConversationSyncNum := v[1] - v[0] + 1
if (oneConversationSyncNum/SplitPullMsgNum) > 1 && IsNotification(k) {
nSeqMap := make(map[string][2]int64, 1)
count := int(oneConversationSyncNum / SplitPullMsgNum)
startSeq := v[0]
var end int64
for i := 0; i <= count; i++ {
if i == count {
nSeqMap[k] = [2]int64{startSeq, v[1]}
} else {
end = startSeq + int64(SplitPullMsgNum)
if end > v[1] {
end = v[1]
i = count
}
nSeqMap[k] = [2]int64{startSeq, end}
}
resp, err := m.pullMsgBySeqRange(ctx, nSeqMap, syncMsgNum)
if err != nil {
log.ZError(ctx, "syncMsgFromSvr err", err, "nSeqMap", nSeqMap)
return err
}
_ = m.triggerConversation(ctx, resp.Msgs)
_ = m.triggerNotification(ctx, resp.NotificationMsgs)
for conversationID, seqs := range nSeqMap {
m.syncedMaxSeqs[conversationID] = seqs[1]
}
startSeq = end + 1
}
continue
}
tempSeqMap[k] = v
if oneConversationSyncNum > 0 {
msgNum += int(oneConversationSyncNum)
}
if msgNum >= SplitPullMsgNum {
resp, err := m.pullMsgBySeqRange(ctx, tempSeqMap, syncMsgNum)
if err != nil {
log.ZError(ctx, "syncMsgFromSvr err", err, "tempSeqMap", tempSeqMap)
return err
}
_ = m.triggerConversation(ctx, resp.Msgs)
_ = m.triggerNotification(ctx, resp.NotificationMsgs)
for conversationID, seqs := range tempSeqMap {
m.syncedMaxSeqs[conversationID] = seqs[1]
}
tempSeqMap = make(map[string][2]int64, 50)
msgNum = 0
}
}
resp, err := m.pullMsgBySeqRange(ctx, tempSeqMap, syncMsgNum)
if err != nil {
log.ZError(ctx, "syncMsgFromSvr err", err, "seqMap", seqMap)
return err
}
_ = m.triggerConversation(ctx, resp.Msgs)
_ = m.triggerNotification(ctx, resp.NotificationMsgs)
for conversationID, seqs := range seqMap {
m.syncedMaxSeqs[conversationID] = seqs[1]
}
} else {
log.ZDebug(ctx, "noting conversation to sync", "syncMsgNum", syncMsgNum)
}
return nil
}
func (m *MsgSyncer) splitSeqs(split int, seqsNeedSync []int64) (splitSeqs [][]int64) {
if len(seqsNeedSync) <= split {
splitSeqs = append(splitSeqs, seqsNeedSync)
return
}
for i := 0; i < len(seqsNeedSync); i += split {
end := i + split
if end > len(seqsNeedSync) {
end = len(seqsNeedSync)
}
splitSeqs = append(splitSeqs, seqsNeedSync[i:end])
}
return
}
func (m *MsgSyncer) pullMsgBySeqRange(ctx context.Context, seqMap map[string][2]int64, syncMsgNum int64) (resp *sdkws.PullMessageBySeqsResp, err error) {
log.ZDebug(ctx, "pullMsgBySeqRange", "seqMap", seqMap, "syncMsgNum", syncMsgNum)
req := sdkws.PullMessageBySeqsReq{UserID: m.loginUserID}
for conversationID, seqs := range seqMap {
req.SeqRanges = append(req.SeqRanges, &sdkws.SeqRange{
ConversationID: conversationID,
Begin: seqs[0],
End: seqs[1],
Num: syncMsgNum,
})
}
resp = &sdkws.PullMessageBySeqsResp{}
if err := m.longConnMgr.SendReqWaitResp(ctx, &req, constant.PullMsgBySeqList, resp); err != nil {
return nil, err
}
return resp, nil
}
// synchronizes messages by SEQs.
func (m *MsgSyncer) syncMsgBySeqs(ctx context.Context, conversationID string, seqsNeedSync []int64) (allMsgs []*sdkws.MsgData, err error) {
pullMsgReq := sdkws.PullMessageBySeqsReq{}
pullMsgReq.UserID = m.loginUserID
split := constant.SplitPullMsgNum
seqsList := m.splitSeqs(split, seqsNeedSync)
for i := 0; i < len(seqsList); {
var pullMsgResp sdkws.PullMessageBySeqsResp
err := m.longConnMgr.SendReqWaitResp(ctx, &pullMsgReq, constant.PullMsgBySeqList, &pullMsgResp)
if err != nil {
log.ZError(ctx, "syncMsgFromSvrSplit err", err, "pullMsgReq", pullMsgReq)
continue
}
i++
allMsgs = append(allMsgs, pullMsgResp.Msgs[conversationID].Msgs...)
}
return allMsgs, nil
}
// triggers a conversation with a new message.
func (m *MsgSyncer) triggerConversation(ctx context.Context, msgs map[string]*sdkws.PullMsgs) error {
if len(msgs) >= 0 {
err := common.TriggerCmdNewMsgCome(ctx, sdk_struct.CmdNewMsgComeToConversation{Msgs: msgs}, m.conversationCh)
if err != nil {
log.ZError(ctx, "triggerCmdNewMsgCome err", err, "msgs", msgs)
}
log.ZDebug(ctx, "triggerConversation", "msgs", msgs)
return err
}
return nil
}
func (m *MsgSyncer) triggerNotification(ctx context.Context, msgs map[string]*sdkws.PullMsgs) error {
if len(msgs) >= 0 {
err := common.TriggerCmdNotification(ctx, sdk_struct.CmdNewMsgComeToConversation{Msgs: msgs}, m.conversationCh)
if err != nil {
log.ZError(ctx, "triggerCmdNewMsgCome err", err, "msgs", msgs)
}
return err
}
return nil
}

View File

@@ -0,0 +1,32 @@
package interaction
import (
"time"
)
type ReconnectStrategy interface {
GetSleepInterval() time.Duration
Reset()
}
type ExponentialRetry struct {
attempts []int
index int
}
func NewExponentialRetry() *ExponentialRetry {
return &ExponentialRetry{
attempts: []int{1, 2, 4, 8, 16},
index: -1,
}
}
func (rs *ExponentialRetry) GetSleepInterval() time.Duration {
rs.index++
interval := rs.index % len(rs.attempts)
return time.Second * time.Duration(rs.attempts[interval])
}
func (rs *ExponentialRetry) Reset() {
rs.index = -1
}

View File

@@ -0,0 +1,87 @@
// 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.
//go:build !js
package interaction
import (
"net/http"
"time"
"github.com/gorilla/websocket"
)
type Default struct {
ConnType int
conn *websocket.Conn
isSetConf bool
}
func (d *Default) SetReadDeadline(timeout time.Duration) error {
return d.conn.SetReadDeadline(time.Now().Add(timeout))
}
func (d *Default) SetWriteDeadline(timeout time.Duration) error {
return d.conn.SetWriteDeadline(time.Now().Add(timeout))
}
func (d *Default) SetReadLimit(limit int64) {
if !d.isSetConf {
d.conn.SetReadLimit(limit)
}
}
func (d *Default) SetPongHandler(handler PongHandler) {
if !d.isSetConf {
d.conn.SetPongHandler(handler)
d.isSetConf = true
}
}
func (d *Default) LocalAddr() string {
return d.conn.LocalAddr().String()
}
func NewWebSocket(connType int) *Default {
return &Default{ConnType: connType}
}
func (d *Default) Close() error {
return d.conn.Close()
}
func (d *Default) WriteMessage(messageType int, message []byte) error {
return d.conn.WriteMessage(messageType, message)
}
func (d *Default) ReadMessage() (int, []byte, error) {
return d.conn.ReadMessage()
}
func (d *Default) Dial(urlStr string, requestHeader http.Header) (*http.Response, error) {
conn, httpResp, err := websocket.DefaultDialer.Dial(urlStr, requestHeader)
if err == nil {
d.conn = conn
}
return httpResp, err
}
func (d *Default) IsNil() bool {
if d.conn != nil {
return false
}
return true
}

View File

@@ -0,0 +1,140 @@
// 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.
//go:build js && wasm
// +build js,wasm
package interaction
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/openimsdk/tools/log"
"io"
"net/http"
"net/url"
"nhooyr.io/websocket"
"time"
)
type JSWebSocket struct {
ConnType int
conn *websocket.Conn
sendConn *websocket.Conn
}
func (w *JSWebSocket) SetReadDeadline(timeout time.Duration) error {
return nil
}
func (w *JSWebSocket) SetWriteDeadline(timeout time.Duration) error {
return nil
}
func (w *JSWebSocket) SetReadLimit(limit int64) {
w.conn.SetReadLimit(limit)
}
func (w *JSWebSocket) SetPongHandler(handler PongHandler) {
}
func (w *JSWebSocket) LocalAddr() string {
return ""
}
func NewWebSocket(connType int) *JSWebSocket {
return &JSWebSocket{ConnType: connType}
}
func (w *JSWebSocket) Close() error {
return w.conn.Close(websocket.StatusGoingAway, "Actively close the conn have old conn")
}
func (w *JSWebSocket) WriteMessage(messageType int, message []byte) error {
return w.conn.Write(context.Background(), websocket.MessageType(messageType), message)
}
func (w *JSWebSocket) ReadMessage() (int, []byte, error) {
messageType, b, err := w.conn.Read(context.Background())
return int(messageType), b, err
}
func (w *JSWebSocket) dial(ctx context.Context, urlStr string) (*websocket.Conn, *http.Response, error) {
u, err := url.Parse(urlStr)
if err != nil {
return nil, nil, err
}
query := u.Query()
query.Set("isMsgResp", "true")
u.RawQuery = query.Encode()
conn, httpResp, err := websocket.Dial(ctx, u.String(), nil)
if err != nil {
return nil, nil, err
}
if httpResp == nil {
httpResp = &http.Response{
StatusCode: http.StatusSwitchingProtocols,
}
}
_, data, err := conn.Read(ctx)
if err != nil {
_ = conn.CloseNow()
return nil, nil, fmt.Errorf("read response error %w", err)
}
var apiResp struct {
ErrCode int `json:"errCode"`
ErrMsg string `json:"errMsg"`
ErrDlt string `json:"errDlt"`
}
if err := json.Unmarshal(data, &apiResp); err != nil {
return nil, nil, fmt.Errorf("unmarshal response error %w", err)
}
if apiResp.ErrCode == 0 {
return conn, httpResp, nil
}
log.ZDebug(ctx, "ws msg read resp", "data", string(data))
httpResp.Body = io.NopCloser(bytes.NewReader(data))
return conn, httpResp, fmt.Errorf("read response error %d %s %s",
apiResp.ErrCode, apiResp.ErrMsg, apiResp.ErrDlt)
}
func (w *JSWebSocket) Dial(urlStr string, _ http.Header) (*http.Response, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
conn, httpResp, err := w.dial(ctx, urlStr)
if err == nil {
w.conn = conn
}
return httpResp, err
}
//func (w *JSWebSocket) Dial(urlStr string, _ http.Header) (*http.Response, error) {
// ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
// defer cancel()
// conn, httpResp, err := websocket.Dial(ctx, urlStr, nil)
// if err == nil {
// w.conn = conn
// }
// return httpResp, err
//}
func (w *JSWebSocket) IsNil() bool {
if w.conn != nil {
return false
}
return true
}

View File

@@ -0,0 +1,168 @@
// 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 interaction
import (
"context"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"sync"
"time"
"github.com/openimsdk/tools/log"
)
type GeneralWsResp struct {
ReqIdentifier int `json:"reqIdentifier"`
ErrCode int `json:"errCode"`
ErrMsg string `json:"errMsg"`
MsgIncr string `json:"msgIncr"`
OperationID string `json:"operationID"`
Data []byte `json:"data"`
}
type GeneralWsReq struct {
ReqIdentifier int `json:"reqIdentifier"`
Token string `json:"token"`
SendID string `json:"sendID"`
OperationID string `json:"operationID"`
MsgIncr string `json:"msgIncr"`
Data []byte `json:"data"`
}
type WsRespAsyn struct {
wsNotification map[string]chan *GeneralWsResp
wsMutex sync.RWMutex
}
func NewWsRespAsyn() *WsRespAsyn {
return &WsRespAsyn{wsNotification: make(map[string]chan *GeneralWsResp, 10)}
}
func GenMsgIncr(userID string) string {
return userID + "_" + utils.OperationIDGenerator()
}
func (u *WsRespAsyn) AddCh(userID string) (string, chan *GeneralWsResp) {
u.wsMutex.Lock()
defer u.wsMutex.Unlock()
msgIncr := GenMsgIncr(userID)
ch := make(chan *GeneralWsResp, 1)
_, ok := u.wsNotification[msgIncr]
if ok {
}
u.wsNotification[msgIncr] = ch
return msgIncr, ch
}
func (u *WsRespAsyn) AddChByIncr(msgIncr string) chan *GeneralWsResp {
u.wsMutex.Lock()
defer u.wsMutex.Unlock()
ch := make(chan *GeneralWsResp, 1)
_, ok := u.wsNotification[msgIncr]
if ok {
log.ZError(context.Background(), "Repeat failed", nil, msgIncr)
}
u.wsNotification[msgIncr] = ch
return ch
}
func (u *WsRespAsyn) GetCh(msgIncr string) chan *GeneralWsResp {
ch, ok := u.wsNotification[msgIncr]
if ok {
return ch
}
return nil
}
func (u *WsRespAsyn) DelCh(msgIncr string) {
u.wsMutex.Lock()
defer u.wsMutex.Unlock()
ch, ok := u.wsNotification[msgIncr]
if ok {
close(ch)
delete(u.wsNotification, msgIncr)
}
}
func (u *WsRespAsyn) notifyCh(ch chan *GeneralWsResp, value *GeneralWsResp, timeout int64) error {
var flag = 0
select {
case ch <- value:
flag = 1
case <-time.After(time.Second * time.Duration(timeout)):
flag = 2
}
if flag == 1 {
return nil
} else {
return errors.New("send cmd timeout")
}
}
// write a unit test for this function
func (u *WsRespAsyn) NotifyResp(ctx context.Context, wsResp GeneralWsResp) error {
u.wsMutex.Lock()
defer u.wsMutex.Unlock()
ch := u.GetCh(wsResp.MsgIncr)
if ch == nil {
return utils.Wrap(errors.New("no ch"), "GetCh failed "+wsResp.MsgIncr)
}
for {
err := u.notifyCh(ch, &wsResp, 1)
if err != nil {
log.ZWarn(ctx, "TriggerCmdNewMsgCome failed ", err, "ch", ch, "wsResp", wsResp)
continue
}
return nil
}
}
func (u *WsRespAsyn) WaitResp(ctx context.Context, ch chan *GeneralWsResp, timeout int) (*GeneralWsResp, error) {
select {
case r, ok := <-ch:
if !ok { //ch has been closed
//log.Debug(operationID, "ws network has been changed ")
return nil, nil
}
//log.Debug(operationID, "ws ch recvMsg success, code ", r.ErrCode)
if r.ErrCode != 0 {
//log.Error(operationID, "ws ch recvMsg failed, code, err msg: ", r.ErrCode, r.ErrMsg)
//switch r.ErrCode {
//case int(constant.ErrInBlackList.ErrCode):
// return nil, &constant.ErrInBlackList
//case int(constant.ErrNotFriend.ErrCode):
// return nil, &constant.ErrNotFriend
//}
//return nil, errors.New(utils.IntToString(r.ErrCode) + ":" + r.ErrMsg)
} else {
return r, nil
}
case <-time.After(time.Second * time.Duration(timeout)):
//log.Error(operationID, "ws ch recvMsg err, timeout")
//if w.conn.IsNil() {
// return nil, errors.New("ws ch recvMsg err, timeout,conn is nil")
//}
//if w.conn.CheckSendConnDiffNow() {
// return nil, constant.WsRecvConnDiff
//} else {
// return nil, constant.WsRecvConnSame
//}
}
return nil, nil
}

View File

@@ -0,0 +1,85 @@
package third
import (
"context"
"errors"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/internal/file"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/ccontext"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/protocol/third"
"io"
"math/rand"
"os"
"path/filepath"
"strings"
"time"
)
func (c *Third) UploadLogs(ctx context.Context, progress Progress) error {
logFilePath := c.LogFilePath
entrys, err := os.ReadDir(logFilePath)
if err != nil {
return err
}
files := make([]string, 0, len(entrys))
for _, entry := range entrys {
if (!entry.IsDir()) && (!strings.HasSuffix(entry.Name(), ".zip")) && checkLogPath(entry.Name()) {
files = append(files, filepath.Join(logFilePath, entry.Name()))
}
}
if len(files) == 0 {
return errors.New("not found log file")
}
zippath := filepath.Join(logFilePath, fmt.Sprintf("%d_%d.zip", time.Now().UnixMilli(), rand.Uint32()))
defer os.Remove(zippath)
if err := zipFiles(zippath, files); err != nil {
return err
}
reqUpload := &file.UploadFileReq{Filepath: zippath, Name: fmt.Sprintf("sdk_log_%s_%s", c.loginUserID, filepath.Base(zippath)), Cause: "sdklog", ContentType: "application/zip"}
resp, err := c.fileUploader.UploadFile(ctx, reqUpload, &progressConvert{ctx: ctx, p: progress})
if err != nil {
return err
}
ccontext.Info(ctx)
reqLog := &third.UploadLogsReq{
Platform: c.platformID,
SystemType: c.systemType,
Version: c.version,
FileURLs: []*third.FileURL{{Filename: zippath, URL: resp.URL}},
}
_, err = util.CallApi[third.UploadLogsResp](ctx, constant.UploadLogsRouter, reqLog)
return err
}
func checkLogPath(logPath string) bool {
if len(logPath) < len("open-im-sdk-core.yyyy-mm-dd") {
return false
}
logTime := logPath[len(logPath)-len(".yyyy-mm-dd"):]
if _, err := time.Parse(".2006-01-02", logTime); err != nil {
return false
}
if !strings.HasPrefix(logPath, "open-im-sdk-core.") {
return false
}
return true
}
func (c *Third) fileCopy(src, dst string) error {
_ = os.RemoveAll(dst)
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0777)
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
return err
}

View File

@@ -0,0 +1,74 @@
package third
import (
"fmt"
"os"
"path/filepath"
"testing"
)
func TestLogMatch(t *testing.T) {
filenames := []string{
"log1.txt",
"log2.log",
"log3.log.txt",
"log4.log.2022-01-01",
"log5.log.2022-01-01.txt",
"log20230918.log",
"OpenIM.CronTask.log.all.2023-09-18", "OpenIM.log.all.2023-09-18",
}
expected := []string{
"OpenIM.CronTask.log.all.2023-09-18", "OpenIM.log.all.2023-09-18",
}
var actual []string
for _, filename := range filenames {
if checkLogPath(filename) {
actual = append(actual, filename)
}
}
if len(actual) != len(expected) {
t.Errorf("Expected %d matches, but got %d", len(expected), len(actual))
}
for i := range expected {
if actual[i] != expected[i] {
t.Errorf("Expected match %d to be %q, but got %q", i, expected[i], actual[i])
}
}
}
func TestName(t *testing.T) {
dir := `C:\Users\openIM\Desktop\testlog`
dirs, err := os.ReadDir(dir)
if err != nil {
panic(err)
}
for _, entry := range dirs {
if !entry.IsDir() {
info, err := entry.Info()
if err != nil {
panic(err)
}
fmt.Println(entry.Name(), info.Size(), info.ModTime())
}
}
if true {
return
}
files := []string{
//filepath.Join(dir, "open-im-sdk-core.2023-10-13"),
filepath.Join(dir, "open-im-sdk-core.2023-11-15"),
//filepath.Join(dir, "open-im-sdk-core.2023-11-17"),
}
if err := zipFiles(filepath.Join(dir, "test1.zip"), files); err != nil {
t.Error(err)
}
}

View File

@@ -0,0 +1,37 @@
package third
import (
"context"
)
type Progress interface {
OnProgress(current int64, size int64)
}
type progressConvert struct {
ctx context.Context
p Progress
}
func (p *progressConvert) Open(size int64) {
p.p.OnProgress(0, size)
}
func (p *progressConvert) PartSize(partSize int64, num int) {}
func (p *progressConvert) HashPartProgress(index int, size int64, partHash string) {}
func (p *progressConvert) HashPartComplete(partsHash string, fileHash string) {}
func (p *progressConvert) UploadID(uploadID string) {}
func (p *progressConvert) UploadPartComplete(index int, partSize int64, partHash string) {}
func (p *progressConvert) UploadComplete(fileSize int64, streamSize int64, storageSize int64) {
//log.ZDebug(p.ctx, "upload log progress", "fileSize", fileSize, "current", streamSize)
p.p.OnProgress(streamSize, fileSize)
}
func (p *progressConvert) Complete(size int64, url string, typ int) {
p.p.OnProgress(size, size)
}

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 third
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/protocol/third"
"github.com/openimsdk/openim-sdk-core/v3/internal/file"
)
type Third struct {
platformID int32
loginUserID string
version string
systemType string
LogFilePath string
fileUploader *file.File
}
func NewThird(platformID int32, loginUserID, version, systemType, LogFilePath string, fileUploader *file.File) *Third {
return &Third{platformID: platformID, loginUserID: loginUserID, version: version, systemType: systemType, LogFilePath: LogFilePath, fileUploader: fileUploader}
}
func (c *Third) UpdateFcmToken(ctx context.Context, fcmToken string, expireTime int64) error {
req := third.FcmUpdateTokenReq{
PlatformID: c.platformID,
FcmToken: fcmToken,
Account: c.loginUserID,
ExpireTime: expireTime}
_, err := util.CallApi[third.FcmUpdateTokenResp](ctx, constant.FcmUpdateTokenRouter, &req)
return err
}
func (c *Third) SetAppBadge(ctx context.Context, appUnreadCount int32) error {
req := third.SetAppBadgeReq{
UserID: c.loginUserID,
AppUnreadCount: appUnreadCount,
}
_, err := util.CallApi[third.SetAppBadgeResp](ctx, constant.SetAppBadgeRouter, &req)
return err
}

View File

@@ -0,0 +1,75 @@
package third
import (
"archive/zip"
"io"
"os"
"path/filepath"
)
func (c *Third) addFileToZip(zipWriter *zip.Writer, filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = filepath.Base(filename)
header.Method = zip.Deflate
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
_, err = io.Copy(writer, io.LimitReader(file, info.Size()))
return err
}
func zipFiles(zipPath string, files []string) error {
zipFile, err := os.Create(zipPath)
if err != nil {
return err
}
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
addFileToZip := func(fp string) error {
file, err := os.Open(fp)
if err != nil {
return err
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = filepath.Base(file.Name())
header.Method = zip.Deflate
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
_, err = io.Copy(writer, io.LimitReader(file, info.Size()))
return err
}
for _, file := range files {
err := addFileToZip(file)
if err != nil {
return err
}
}
if err := zipWriter.Flush(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,43 @@
// 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 user
import (
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/protocol/user"
"github.com/openimsdk/protocol/sdkws"
)
func ServerUserToLocalUser(user *sdkws.UserInfo) *model_struct.LocalUser {
return &model_struct.LocalUser{
UserID: user.UserID,
Nickname: user.Nickname,
FaceURL: user.FaceURL,
CreateTime: user.CreateTime,
Ex: user.Ex,
//AppMangerLevel: user.AppMangerLevel,
GlobalRecvMsgOpt: user.GlobalRecvMsgOpt,
//AttachedInfo: user.AttachedInfo,
}
}
func ServerCommandToLocalCommand(data *user.AllCommandInfoResp) *model_struct.LocalUserCommand {
return &model_struct.LocalUserCommand{
Type: data.Type,
CreateTime: data.CreateTime,
Uuid: data.Uuid,
Value: data.Value,
}
}

View File

@@ -0,0 +1,89 @@
// 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 user
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
pbUser "github.com/openimsdk/protocol/user"
userPb "github.com/openimsdk/protocol/user"
"github.com/openimsdk/protocol/sdkws"
)
func (u *User) GetUsersInfo(ctx context.Context, userIDs []string) ([]*model_struct.LocalUser, error) {
return u.GetUsersInfoFromSvr(ctx, userIDs)
}
func (u *User) GetSelfUserInfo(ctx context.Context) (*model_struct.LocalUser, error) {
return u.getSelfUserInfo(ctx)
}
// Deprecated: user SetSelfInfoEx instead
func (u *User) SetSelfInfo(ctx context.Context, userInfo *sdkws.UserInfo) error {
return u.updateSelfUserInfo(ctx, userInfo)
}
func (u *User) SetSelfInfoEx(ctx context.Context, userInfo *sdkws.UserInfoWithEx) error {
return u.updateSelfUserInfoEx(ctx, userInfo)
}
func (u *User) SetGlobalRecvMessageOpt(ctx context.Context, opt int) error {
if err := util.ApiPost(ctx, constant.SetGlobalRecvMessageOptRouter,
&pbUser.SetGlobalRecvMessageOptReq{UserID: u.loginUserID, GlobalRecvMsgOpt: int32(opt)}, nil); err != nil {
return err
}
u.SyncLoginUserInfo(ctx)
return nil
}
func (u *User) UpdateMsgSenderInfo(ctx context.Context, nickname, faceURL string) (err error) {
if nickname != "" {
if err = u.DataBase.UpdateMsgSenderNickname(ctx, u.loginUserID, nickname, constant.SingleChatType); err != nil {
return err
}
}
if faceURL != "" {
if err = u.DataBase.UpdateMsgSenderFaceURL(ctx, u.loginUserID, faceURL, constant.SingleChatType); err != nil {
return err
}
}
return nil
}
func (u *User) SubscribeUsersStatus(ctx context.Context, userIDs []string) ([]*userPb.OnlineStatus, error) {
userStatus, err := u.subscribeUsersStatus(ctx, userIDs)
if err != nil {
return nil, err
}
u.OnlineStatusCache.DeleteAll()
u.OnlineStatusCache.StoreAll(func(value *userPb.OnlineStatus) string {
return value.UserID
}, userStatus)
return userStatus, nil
}
func (u *User) UnsubscribeUsersStatus(ctx context.Context, userIDs []string) error {
u.OnlineStatusCache.DeleteAll()
return u.unsubscribeUsersStatus(ctx, userIDs)
}
func (u *User) GetSubscribeUsersStatus(ctx context.Context) ([]*userPb.OnlineStatus, error) {
return u.getSubscribeUsersStatus(ctx)
}
func (u *User) GetUserStatus(ctx context.Context, userIDs []string) ([]*userPb.OnlineStatus, error) {
return u.getUserStatus(ctx, userIDs)
}

View File

@@ -0,0 +1,97 @@
// 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 user
import (
"context"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"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"
userPb "github.com/openimsdk/protocol/user"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"gorm.io/gorm"
)
func (u *User) SyncLoginUserInfo(ctx context.Context) error {
remoteUser, err := u.GetSingleUserFromSvr(ctx, u.loginUserID)
if err != nil {
return err
}
localUser, err := u.GetLoginUser(ctx, u.loginUserID)
if err != nil && errs.Unwrap(err) != gorm.ErrRecordNotFound {
log.ZError(ctx, "SyncLoginUserInfo", err)
}
var localUsers []*model_struct.LocalUser
if err == nil {
localUsers = []*model_struct.LocalUser{localUser}
}
log.ZDebug(ctx, "SyncLoginUserInfo", "remoteUser", remoteUser, "localUser", localUser)
return u.userSyncer.Sync(ctx, []*model_struct.LocalUser{remoteUser}, localUsers, nil)
}
func (u *User) SyncUserStatus(ctx context.Context, fromUserID string, status int32, platformID int32) {
userOnlineStatus := userPb.OnlineStatus{
UserID: fromUserID,
Status: status,
PlatformIDs: []int32{platformID},
}
if v, ok := u.OnlineStatusCache.Load(fromUserID); ok {
if status == constant.Online {
v.PlatformIDs = utils.RemoveRepeatedElementsInList(append(v.PlatformIDs, platformID))
u.OnlineStatusCache.Store(fromUserID, v)
} else {
v.PlatformIDs = utils.RemoveOneInList(v.PlatformIDs, platformID)
if len(v.PlatformIDs) == 0 {
v.Status = constant.Offline
v.PlatformIDs = []int32{}
u.OnlineStatusCache.Delete(fromUserID)
}
}
u.listener().OnUserStatusChanged(utils.StructToJsonString(v))
} else {
if status == constant.Online {
u.OnlineStatusCache.Store(fromUserID, &userOnlineStatus)
u.listener().OnUserStatusChanged(utils.StructToJsonString(userOnlineStatus))
} else {
log.ZWarn(ctx, "exception", errors.New("user not exist"), "fromUserID", fromUserID,
"status", status, "platformID", platformID)
}
}
}
type CommandInfoResponse struct {
CommandResp []*userPb.AllCommandInfoResp `json:"CommandResp"`
}
func (u *User) SyncAllCommand(ctx context.Context) error {
var serverData CommandInfoResponse
err := util.ApiPost(ctx, constant.ProcessUserCommandGetAll, userPb.ProcessUserCommandGetAllReq{
UserID: u.loginUserID,
}, &serverData)
if err != nil {
return err
}
localData, err := u.DataBase.ProcessUserCommandGetAll(ctx)
if err != nil {
return err
}
log.ZDebug(ctx, "sync command", "data from server", serverData, "data from local", localData)
return u.commandSyncer.Sync(ctx, datautil.Batch(ServerCommandToLocalCommand, serverData.CommandResp), localData, nil)
}

View File

@@ -0,0 +1,427 @@
// 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 user
import (
"context"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/internal/cache"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"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/sdkerrs"
"github.com/openimsdk/openim-sdk-core/v3/pkg/syncer"
authPb "github.com/openimsdk/protocol/auth"
"github.com/openimsdk/protocol/sdkws"
userPb "github.com/openimsdk/protocol/user"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
"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/utils"
PbConstant "github.com/openimsdk/protocol/constant"
)
type BasicInfo struct {
Nickname string
FaceURL string
}
// User is a struct that represents a user in the system.
type User struct {
db_interface.DataBase
loginUserID string
listener func() open_im_sdk_callback.OnUserListener
userSyncer *syncer.Syncer[*model_struct.LocalUser, syncer.NoResp, string]
commandSyncer *syncer.Syncer[*model_struct.LocalUserCommand, syncer.NoResp, string]
conversationCh chan common.Cmd2Value
UserBasicCache *cache.Cache[string, *BasicInfo]
OnlineStatusCache *cache.Cache[string, *userPb.OnlineStatus]
}
// SetListener sets the user's listener.
func (u *User) SetListener(listener func() open_im_sdk_callback.OnUserListener) {
u.listener = listener
}
// NewUser creates a new User object.
func NewUser(dataBase db_interface.DataBase, loginUserID string, conversationCh chan common.Cmd2Value) *User {
user := &User{DataBase: dataBase, loginUserID: loginUserID, conversationCh: conversationCh}
user.initSyncer()
user.UserBasicCache = cache.NewCache[string, *BasicInfo]()
user.OnlineStatusCache = cache.NewCache[string, *userPb.OnlineStatus]()
return user
}
func (u *User) initSyncer() {
u.userSyncer = syncer.New[*model_struct.LocalUser, syncer.NoResp, string](
func(ctx context.Context, value *model_struct.LocalUser) error {
return u.InsertLoginUser(ctx, value)
},
func(ctx context.Context, value *model_struct.LocalUser) error {
return fmt.Errorf("not support delete user %s", value.UserID)
},
func(ctx context.Context, serverUser, localUser *model_struct.LocalUser) error {
return u.DataBase.UpdateLoginUser(context.Background(), serverUser)
},
func(user *model_struct.LocalUser) string {
return user.UserID
},
nil,
func(ctx context.Context, state int, server, local *model_struct.LocalUser) error {
switch state {
case syncer.Update:
u.listener().OnSelfInfoUpdated(utils.StructToJsonString(server))
if server.Nickname != local.Nickname || server.FaceURL != local.FaceURL {
_ = common.TriggerCmdUpdateMessage(ctx, common.UpdateMessageNode{Action: constant.UpdateMsgFaceUrlAndNickName,
Args: common.UpdateMessageInfo{SessionType: constant.SingleChatType, UserID: server.UserID, FaceURL: server.FaceURL, Nickname: server.Nickname}}, u.conversationCh)
}
}
return nil
},
)
u.commandSyncer = syncer.New[*model_struct.LocalUserCommand, syncer.NoResp, string](
func(ctx context.Context, command *model_struct.LocalUserCommand) error {
// Logic to insert a command
return u.DataBase.ProcessUserCommandAdd(ctx, command)
},
func(ctx context.Context, command *model_struct.LocalUserCommand) error {
// Logic to delete a command
return u.DataBase.ProcessUserCommandDelete(ctx, command)
},
func(ctx context.Context, serverCommand *model_struct.LocalUserCommand, localCommand *model_struct.LocalUserCommand) error {
// Logic to update a command
if serverCommand == nil || localCommand == nil {
return fmt.Errorf("nil command reference")
}
return u.DataBase.ProcessUserCommandUpdate(ctx, serverCommand)
},
func(command *model_struct.LocalUserCommand) string {
// Return a unique identifier for the command
if command == nil {
return ""
}
return command.Uuid
},
func(a *model_struct.LocalUserCommand, b *model_struct.LocalUserCommand) bool {
// Compare two commands to check if they are equal
if a == nil || b == nil {
return false
}
return a.Uuid == b.Uuid && a.Type == b.Type && a.Value == b.Value
},
func(ctx context.Context, state int, serverCommand *model_struct.LocalUserCommand, localCommand *model_struct.LocalUserCommand) error {
if u.listener == nil {
return nil
}
switch state {
case syncer.Delete:
u.listener().OnUserCommandDelete(utils.StructToJsonString(serverCommand))
case syncer.Update:
u.listener().OnUserCommandUpdate(utils.StructToJsonString(serverCommand))
case syncer.Insert:
u.listener().OnUserCommandAdd(utils.StructToJsonString(serverCommand))
}
return nil
},
)
}
//func (u *User) equal(a, b *model_struct.LocalUser) bool {
// if a.CreateTime != b.CreateTime {
// log.ZDebug(context.Background(), "user equal", "a", a.CreateTime, "b", b.CreateTime)
// }
// if a.UserID != b.UserID {
// log.ZDebug(context.Background(), "user equal", "a", a.UserID, "b", b.UserID)
// }
// if a.Ex != b.Ex {
// log.ZDebug(context.Background(), "user equal", "a", a.Ex, "b", b.Ex)
// }
//
// if a.Nickname != b.Nickname {
// log.ZDebug(context.Background(), "user equal", "a", a.Nickname, "b", b.Nickname)
// }
// if a.FaceURL != b.FaceURL {
// log.ZDebug(context.Background(), "user equal", "a", a.FaceURL, "b", b.FaceURL)
// }
// if a.AttachedInfo != b.AttachedInfo {
// log.ZDebug(context.Background(), "user equal", "a", a.AttachedInfo, "b", b.AttachedInfo)
// }
// if a.GlobalRecvMsgOpt != b.GlobalRecvMsgOpt {
// log.ZDebug(context.Background(), "user equal", "a", a.GlobalRecvMsgOpt, "b", b.GlobalRecvMsgOpt)
// }
// if a.AppMangerLevel != b.AppMangerLevel {
// log.ZDebug(context.Background(), "user equal", "a", a.AppMangerLevel, "b", b.AppMangerLevel)
// }
// return a.UserID == b.UserID && a.Nickname == b.Nickname && a.FaceURL == b.FaceURL &&
// a.CreateTime == b.CreateTime && a.AttachedInfo == b.AttachedInfo &&
// a.Ex == b.Ex && a.GlobalRecvMsgOpt == b.GlobalRecvMsgOpt && a.AppMangerLevel == b.AppMangerLevel
//}
// DoNotification handles incoming notifications for the user.
func (u *User) DoNotification(ctx context.Context, msg *sdkws.MsgData) {
log.ZDebug(ctx, "user notification", "msg", *msg)
go func() {
switch msg.ContentType {
case constant.UserInfoUpdatedNotification:
u.userInfoUpdatedNotification(ctx, msg)
case constant.UserStatusChangeNotification:
u.userStatusChangeNotification(ctx, msg)
case constant.UserCommandAddNotification:
u.userCommandAddNotification(ctx, msg)
case constant.UserCommandDeleteNotification:
u.userCommandDeleteNotification(ctx, msg)
case constant.UserCommandUpdateNotification:
u.userCommandUpdateNotification(ctx, msg)
default:
// log.Error(operationID, "type failed ", msg.ClientMsgID, msg.ServerMsgID, msg.ContentType)
}
}()
}
// userInfoUpdatedNotification handles notifications about updated user information.
func (u *User) userInfoUpdatedNotification(ctx context.Context, msg *sdkws.MsgData) {
log.ZDebug(ctx, "userInfoUpdatedNotification", "msg", *msg)
tips := sdkws.UserInfoUpdatedTips{}
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
log.ZError(ctx, "comm.UnmarshalTips failed", err, "msg", msg.Content)
return
}
if tips.UserID == u.loginUserID {
u.SyncLoginUserInfo(ctx)
} else {
log.ZDebug(ctx, "detail.UserID != u.loginUserID, do nothing", "detail.UserID", tips.UserID, "u.loginUserID", u.loginUserID)
}
}
// userStatusChangeNotification get subscriber status change callback
func (u *User) userStatusChangeNotification(ctx context.Context, msg *sdkws.MsgData) {
log.ZDebug(ctx, "userStatusChangeNotification", "msg", *msg)
tips := sdkws.UserStatusChangeTips{}
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
log.ZError(ctx, "comm.UnmarshalTips failed", err, "msg", msg.Content)
return
}
if tips.FromUserID == u.loginUserID {
log.ZDebug(ctx, "self terminal login", "tips", tips)
return
}
u.SyncUserStatus(ctx, tips.FromUserID, tips.Status, tips.PlatformID)
}
// userCommandAddNotification handle notification when user add favorite
func (u *User) userCommandAddNotification(ctx context.Context, msg *sdkws.MsgData) {
log.ZDebug(ctx, "userCommandAddNotification", "msg", *msg)
tip := sdkws.UserCommandAddTips{}
if tip.ToUserID == u.loginUserID {
u.SyncAllCommand(ctx)
} else {
log.ZDebug(ctx, "ToUserID != u.loginUserID, do nothing", "detail.UserID", tip.ToUserID, "u.loginUserID", u.loginUserID)
}
}
// userCommandDeleteNotification handle notification when user delete favorite
func (u *User) userCommandDeleteNotification(ctx context.Context, msg *sdkws.MsgData) {
log.ZDebug(ctx, "userCommandAddNotification", "msg", *msg)
tip := sdkws.UserCommandDeleteTips{}
if tip.ToUserID == u.loginUserID {
u.SyncAllCommand(ctx)
} else {
log.ZDebug(ctx, "ToUserID != u.loginUserID, do nothing", "detail.UserID", tip.ToUserID, "u.loginUserID", u.loginUserID)
}
}
// userCommandUpdateNotification handle notification when user update favorite
func (u *User) userCommandUpdateNotification(ctx context.Context, msg *sdkws.MsgData) {
log.ZDebug(ctx, "userCommandAddNotification", "msg", *msg)
tip := sdkws.UserCommandUpdateTips{}
if tip.ToUserID == u.loginUserID {
u.SyncAllCommand(ctx)
} else {
log.ZDebug(ctx, "ToUserID != u.loginUserID, do nothing", "detail.UserID", tip.ToUserID, "u.loginUserID", u.loginUserID)
}
}
// GetUsersInfoFromSvr retrieves user information from the server.
func (u *User) GetUsersInfoFromSvr(ctx context.Context, userIDs []string) ([]*model_struct.LocalUser, error) {
resp, err := util.CallApi[userPb.GetDesignateUsersResp](ctx, constant.GetUsersInfoRouter, userPb.GetDesignateUsersReq{UserIDs: userIDs})
if err != nil {
return nil, sdkerrs.WrapMsg(err, "GetUsersInfoFromSvr failed")
}
return datautil.Batch(ServerUserToLocalUser, resp.UsersInfo), nil
}
// GetSingleUserFromSvr retrieves user information from the server.
func (u *User) GetSingleUserFromSvr(ctx context.Context, userID string) (*model_struct.LocalUser, error) {
users, err := u.GetUsersInfoFromSvr(ctx, []string{userID})
if err != nil {
return nil, err
}
if len(users) > 0 {
return users[0], nil
}
return nil, sdkerrs.ErrUserIDNotFound.WrapMsg(fmt.Sprintf("getSelfUserInfo failed, userID: %s not exist", userID))
}
// getSelfUserInfo retrieves the user's information.
func (u *User) getSelfUserInfo(ctx context.Context) (*model_struct.LocalUser, error) {
userInfo, errLocal := u.GetLoginUser(ctx, u.loginUserID)
if errLocal != nil {
srvUserInfo, errServer := u.GetServerUserInfo(ctx, []string{u.loginUserID})
if errServer != nil {
return nil, errServer
}
if len(srvUserInfo) == 0 {
return nil, sdkerrs.ErrUserIDNotFound
}
userInfo = ServerUserToLocalUser(srvUserInfo[0])
_ = u.InsertLoginUser(ctx, userInfo)
}
return userInfo, nil
}
// updateSelfUserInfo updates the user's information.
func (u *User) updateSelfUserInfo(ctx context.Context, userInfo *sdkws.UserInfo) error {
userInfo.UserID = u.loginUserID
if err := util.ApiPost(ctx, constant.UpdateSelfUserInfoRouter, userPb.UpdateUserInfoReq{UserInfo: userInfo}, nil); err != nil {
return err
}
_ = u.SyncLoginUserInfo(ctx)
return nil
}
// updateSelfUserInfoEx updates the user's information with Ex field.
func (u *User) updateSelfUserInfoEx(ctx context.Context, userInfo *sdkws.UserInfoWithEx) error {
userInfo.UserID = u.loginUserID
if err := util.ApiPost(ctx, constant.UpdateSelfUserInfoExRouter, userPb.UpdateUserInfoExReq{UserInfo: userInfo}, nil); err != nil {
return err
}
_ = u.SyncLoginUserInfo(ctx)
return nil
}
// CRUD user command
func (u *User) ProcessUserCommandAdd(ctx context.Context, userCommand *userPb.ProcessUserCommandAddReq) error {
if err := util.ApiPost(ctx, constant.ProcessUserCommandAdd, userPb.ProcessUserCommandAddReq{UserID: u.loginUserID, Type: userCommand.Type, Uuid: userCommand.Uuid, Value: userCommand.Value}, nil); err != nil {
return err
}
return u.SyncAllCommand(ctx)
}
// ProcessUserCommandDelete delete user's choice
func (u *User) ProcessUserCommandDelete(ctx context.Context, userCommand *userPb.ProcessUserCommandDeleteReq) error {
if err := util.ApiPost(ctx, constant.ProcessUserCommandDelete, userPb.ProcessUserCommandDeleteReq{UserID: u.loginUserID,
Type: userCommand.Type, Uuid: userCommand.Uuid}, nil); err != nil {
return err
}
return u.SyncAllCommand(ctx)
}
// ProcessUserCommandUpdate update user's choice
func (u *User) ProcessUserCommandUpdate(ctx context.Context, userCommand *userPb.ProcessUserCommandUpdateReq) error {
if err := util.ApiPost(ctx, constant.ProcessUserCommandUpdate, userPb.ProcessUserCommandUpdateReq{UserID: u.loginUserID,
Type: userCommand.Type, Uuid: userCommand.Uuid, Value: userCommand.Value}, nil); err != nil {
return err
}
return u.SyncAllCommand(ctx)
}
// ProcessUserCommandGet get user's choice
func (u *User) ProcessUserCommandGetAll(ctx context.Context) ([]*userPb.CommandInfoResp, error) {
localCommands, err := u.DataBase.ProcessUserCommandGetAll(ctx)
if err != nil {
return nil, err // Handle the error appropriately
}
var result []*userPb.CommandInfoResp
for _, localCommand := range localCommands {
result = append(result, &userPb.CommandInfoResp{
Type: localCommand.Type,
CreateTime: localCommand.CreateTime,
Uuid: localCommand.Uuid,
Value: localCommand.Value,
})
}
return result, nil
}
// ParseTokenFromSvr parses a token from the server.
func (u *User) ParseTokenFromSvr(ctx context.Context) (int64, error) {
resp, err := util.CallApi[authPb.ParseTokenResp](ctx, constant.ParseTokenRouter, authPb.ParseTokenReq{})
return resp.ExpireTimeSeconds, err
}
// GetServerUserInfo retrieves user information from the server.
func (u *User) GetServerUserInfo(ctx context.Context, userIDs []string) ([]*sdkws.UserInfo, error) {
resp, err := util.CallApi[userPb.GetDesignateUsersResp](ctx, constant.GetUsersInfoRouter, &userPb.GetDesignateUsersReq{UserIDs: userIDs})
if err != nil {
return nil, err
}
return resp.UsersInfo, nil
}
// subscribeUsersStatus Presence status of subscribed users.
func (u *User) subscribeUsersStatus(ctx context.Context, userIDs []string) ([]*userPb.OnlineStatus, error) {
resp, err := util.CallApi[userPb.SubscribeOrCancelUsersStatusResp](ctx, constant.SubscribeUsersStatusRouter, &userPb.SubscribeOrCancelUsersStatusReq{
UserID: u.loginUserID,
UserIDs: userIDs,
Genre: PbConstant.SubscriberUser,
})
if err != nil {
return nil, err
}
return resp.StatusList, nil
}
// unsubscribeUsersStatus Unsubscribe a user's presence.
func (u *User) unsubscribeUsersStatus(ctx context.Context, userIDs []string) error {
_, err := util.CallApi[userPb.SubscribeOrCancelUsersStatusResp](ctx, constant.SubscribeUsersStatusRouter, &userPb.SubscribeOrCancelUsersStatusReq{
UserID: u.loginUserID,
UserIDs: userIDs,
Genre: PbConstant.Unsubscribe,
})
if err != nil {
return err
}
return nil
}
// getSubscribeUsersStatus Get the online status of subscribers.
func (u *User) getSubscribeUsersStatus(ctx context.Context) ([]*userPb.OnlineStatus, error) {
resp, err := util.CallApi[userPb.GetSubscribeUsersStatusResp](ctx, constant.GetSubscribeUsersStatusRouter, &userPb.GetSubscribeUsersStatusReq{
UserID: u.loginUserID,
})
if err != nil {
return nil, err
}
return resp.StatusList, nil
}
// getUserStatus Get the online status of users.
func (u *User) getUserStatus(ctx context.Context, userIDs []string) ([]*userPb.OnlineStatus, error) {
resp, err := util.CallApi[userPb.GetUserStatusResp](ctx, constant.GetUserStatusRouter, &userPb.GetUserStatusReq{
UserID: u.loginUserID,
UserIDs: userIDs,
})
if err != nil {
return nil, err
}
return resp.StatusList, nil
}

View File

@@ -0,0 +1,28 @@
// 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 util
//func NoticeChange[T any](fn func(data string)) func(ctx context.Context, state int, value T) error {
// return func(ctx context.Context, state int, value T) error {
// if state != syncer.Unchanged {
// data, err := json.Marshal(value)
// if err != nil {
// return err
// }
// fn(string(data))
// }
// return nil
// }
//}

View File

@@ -0,0 +1,239 @@
// 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 util
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/pkg/ccontext"
"github.com/openimsdk/openim-sdk-core/v3/pkg/page"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/tools/errs"
"io"
"net/http"
"time"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
// apiClient is a global HTTP client with a timeout of one minute.
var apiClient = &http.Client{
Timeout: time.Second * 30,
}
// ApiResponse represents the standard structure of an API response.
type ApiResponse struct {
ErrCode int `json:"errCode"`
ErrMsg string `json:"errMsg"`
ErrDlt string `json:"errDlt"`
Data json.RawMessage `json:"data"`
}
// ApiPost performs an HTTP POST request to a specified API endpoint.
// It serializes the request object, sends it to the API, and unmarshals the response into the resp object.
// It handles logging, error wrapping, and operation ID validation.
// Context (ctx) is used for passing metadata and control information.
// api: the API endpoint to which the request is sent.
// req: the request object to be sent to the API.
// resp: a pointer to the response object where the API response will be unmarshalled.
// Returns an error if the request fails at any stage.
func ApiPost(ctx context.Context, api string, req, resp any) (err error) {
// Extract operationID from context and validate.
operationID, _ := ctx.Value("operationID").(string)
if operationID == "" {
err := sdkerrs.ErrArgs.WrapMsg("call api operationID is empty")
log.ZError(ctx, "ApiRequest", err, "type", "ctx not set operationID")
return err
}
// Deferred function to log the result of the API call.
defer func(start time.Time) {
elapsed := time.Since(start).Milliseconds()
if err == nil {
log.ZDebug(ctx, "CallApi", "api", api, "state", "success", "cost time", fmt.Sprintf("%dms", elapsed))
} else {
log.ZError(ctx, "CallApi", err, "api", api, "state", "failed", "cost time", fmt.Sprintf("%dms", elapsed))
}
}(time.Now())
// Serialize the request object to JSON.
reqBody, err := json.Marshal(req)
if err != nil {
log.ZError(ctx, "ApiRequest", err, "type", "json.Marshal(req) failed")
return sdkerrs.ErrSdkInternal.WrapMsg("json.Marshal(req) failed " + err.Error())
}
// Construct the full API URL and create a new HTTP request with context.
ctxInfo := ccontext.Info(ctx)
reqUrl := ctxInfo.ApiAddr() + api
request, err := http.NewRequestWithContext(ctx, http.MethodPost, reqUrl, bytes.NewReader(reqBody))
if err != nil {
log.ZError(ctx, "ApiRequest", err, "type", "http.NewRequestWithContext failed")
return sdkerrs.ErrSdkInternal.WrapMsg("sdk http.NewRequestWithContext failed " + err.Error())
}
// Set headers for the request.
log.ZDebug(ctx, "ApiRequest", "url", reqUrl, "body", string(reqBody))
request.ContentLength = int64(len(reqBody))
request.Header.Set("Content-Type", "application/json")
request.Header.Set("operationID", operationID)
request.Header.Set("token", ctxInfo.Token())
// Send the request and receive the response.
response, err := apiClient.Do(request)
if err != nil {
log.ZError(ctx, "ApiRequest", err, "type", "network error")
return sdkerrs.ErrNetwork.WrapMsg("ApiPost http.Client.Do failed " + err.Error())
}
// Ensure the response body is closed after processing.
defer response.Body.Close()
// Read the response body.
respBody, err := io.ReadAll(response.Body)
if err != nil {
log.ZError(ctx, "ApiResponse", err, "type", "read body", "status", response.Status)
return sdkerrs.ErrSdkInternal.WrapMsg("io.ReadAll(ApiResponse) failed " + err.Error())
}
// Log the response for debugging purposes.
log.ZDebug(ctx, "ApiResponse", "url", reqUrl, "status", response.Status, "body", string(respBody))
// Unmarshal the response body into the ApiResponse structure.
var baseApi ApiResponse
if err := json.Unmarshal(respBody, &baseApi); err != nil {
log.ZError(ctx, "ApiResponse", err, "type", "api code parse")
return sdkerrs.ErrSdkInternal.WrapMsg(fmt.Sprintf("api %s json.Unmarshal(%q, %T) failed %s", api, string(respBody), &baseApi, err.Error()))
}
// Check if the API returned an error code and handle it.
if baseApi.ErrCode != 0 {
err := sdkerrs.New(baseApi.ErrCode, baseApi.ErrMsg, baseApi.ErrDlt)
ccontext.GetApiErrCodeCallback(ctx).OnError(ctx, err)
log.ZError(ctx, "ApiResponse", err, "type", "api code error", "msg", baseApi.ErrMsg, "dlt", baseApi.ErrDlt)
return err
}
// If no data is received, or it's null, return with no error.
if resp == nil || len(baseApi.Data) == 0 || string(baseApi.Data) == "null" {
return nil
}
// Unmarshal the actual data part of the response into the provided response object.
if err := json.Unmarshal(baseApi.Data, resp); err != nil {
log.ZError(ctx, "ApiResponse", err, "type", "api data parse", "data", string(baseApi.Data), "bind", fmt.Sprintf("%T", resp))
return sdkerrs.ErrSdkInternal.WrapMsg(fmt.Sprintf("json.Unmarshal(%q, %T) failed %s", string(baseApi.Data), resp, err.Error()))
}
return nil
}
// CallApi wraps ApiPost to make an API call and unmarshal the response into a new instance of type T.
func CallApi[T any](ctx context.Context, api string, req any) (*T, error) {
var resp T
if err := ApiPost(ctx, api, req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// GetPageAll handles pagination for API requests. It iterates over pages of data until all data is retrieved.
// A is the request type with pagination support, B is the response type, and C is the type of data to be returned.
// The function fn processes each page of response data to extract a slice of C.
func GetPageAll[A interface {
GetPagination() *sdkws.RequestPagination
}, B, C any](ctx context.Context, api string, req A, fn func(resp *B) []C) ([]C, error) {
if req.GetPagination().ShowNumber <= 0 {
req.GetPagination().ShowNumber = 50
}
var res []C
for i := int32(0); ; i++ {
req.GetPagination().PageNumber = i + 1
memberResp, err := CallApi[B](ctx, api, req)
if err != nil {
return nil, err
}
list := fn(memberResp)
res = append(res, list...)
if len(list) < int(req.GetPagination().ShowNumber) {
break
}
}
return res, nil
}
func GetPageAllWithMaxNum[A interface {
GetPagination() *sdkws.RequestPagination
}, B, C any](ctx context.Context, api string, req A, fn func(resp *B) []C, maxItems int) ([]C, error) {
if req.GetPagination().ShowNumber <= 0 {
req.GetPagination().ShowNumber = 50
}
var res []C
totalFetched := 0
for i := int32(0); ; i++ {
req.GetPagination().PageNumber = i + 1
memberResp, err := CallApi[B](ctx, api, req)
if err != nil {
return nil, err
}
list := fn(memberResp)
res = append(res, list...)
totalFetched += len(list)
if len(list) < int(req.GetPagination().ShowNumber) || (maxItems > 0 && totalFetched >= maxItems) {
break
}
}
if maxItems > 0 && len(res) > maxItems {
res = res[:maxItems]
}
return res, nil
}
func FetchAndInsertPagedData[RESP, L any](ctx context.Context, api string, req page.PageReq, fn func(resp *RESP) []L, batchInsertFn func(ctx context.Context, items []L) error,
insertFn func(ctx context.Context, item L) error, maxItems int) error {
if req.GetPagination().ShowNumber <= 0 {
req.GetPagination().ShowNumber = 50
}
var errSingle error
var errList []error
totalFetched := 0
for i := int32(0); ; i++ {
req.GetPagination().PageNumber = i + 1
memberResp, err := CallApi[RESP](ctx, api, req)
if err != nil {
return err
}
list := fn(memberResp)
if err := batchInsertFn(ctx, list); err != nil {
for _, item := range list {
errSingle = insertFn(ctx, item)
if errSingle != nil {
errList = append(errList, errs.New(errSingle.Error(), "item", item))
}
}
}
totalFetched += len(list)
if len(list) < int(req.GetPagination().ShowNumber) || (maxItems > 0 && totalFetched >= maxItems) {
break
}
}
if len(errList) > 0 {
return errs.WrapMsg(errList[0], "batch insert failed due to data exception")
}
return nil
}

View File

@@ -0,0 +1,40 @@
// 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 workMoments
//import (
// "context"
// "open_im_sdk/open_im_sdk_callback"
// "open_im_sdk/pkg/db/model_struct"
//)
//
//funcation (w *WorkMoments) SetListener(callback open_im_sdk_callback.OnWorkMomentsListener) {
// if callback == nil {
// return
// }
// w.listener = callback
//}
//
//funcation (w *WorkMoments) GetWorkMomentsUnReadCount(ctx context.Context) (model_struct.LocalWorkMomentsNotificationUnreadCount, error) {
// return w.getWorkMomentsNotificationUnReadCount(ctx)
//}
//
//funcation (w *WorkMoments) GetWorkMomentsNotification(ctx context.Context, offset, count int) ([]*model_struct.WorkMomentNotificationMsg, error) {
// return w.getWorkMomentsNotification(ctx, offset, count)
//}
//
//funcation (w *WorkMoments) ClearWorkMomentsNotification(ctx context.Context) error {
// return w.clearWorkMomentsNotification(ctx)
//}

View File

@@ -0,0 +1,78 @@
// 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 workMoments
//
//import (
// "context"
// "open_im_sdk/open_im_sdk_callback"
// "open_im_sdk/pkg/db/db_interface"
// "open_im_sdk/pkg/db/model_struct"
// "open_im_sdk/pkg/utils"
//
// "github.com/openimsdk/tools/log"
//)
//
//type WorkMoments struct {
// listener open_im_sdk_callback.OnWorkMomentsListener
// loginUserID string
// db db_interface.DataBase
//}
//
//funcation NewWorkMoments(loginUserID string, db db_interface.DataBase) *WorkMoments {
// return &WorkMoments{loginUserID: loginUserID, db: db}
//}
//
//funcation (w *WorkMoments) DoNotification(ctx context.Context, jsonDetail string) {
// if w.listener == nil {
// return
// }
// if err := w.db.InsertWorkMomentsNotification(ctx, jsonDetail); err != nil {
// log.ZError(ctx, "InsertWorkMomentsNotification failed", err, "jsonDetail", jsonDetail)
// return
// }
// if err := w.db.IncrWorkMomentsNotificationUnreadCount(ctx); err != nil {
// log.ZError(ctx, "IncrWorkMomentsNotificationUnreadCount failed", err)
// return
// }
// w.listener.OnRecvNewNotification()
//}
//
//funcation (w *WorkMoments) getWorkMomentsNotification(ctx context.Context, offset, count int) ([]*model_struct.WorkMomentNotificationMsg, error) {
// if err := w.db.MarkAllWorkMomentsNotificationAsRead(ctx); err != nil {
// return nil, err
// }
// workMomentsNotifications, err := w.db.GetWorkMomentsNotification(ctx, offset, count)
// if err != nil {
// return nil, err
// }
// msgs := make([]*model_struct.WorkMomentNotificationMsg, len(workMomentsNotifications))
// for i, v := range workMomentsNotifications {
// workMomentNotificationMsg := model_struct.WorkMomentNotificationMsg{}
// if err := utils.JsonStringToStruct(v.JsonDetail, &workMomentNotificationMsg); err != nil {
// log.ZError(ctx, "invalid data", err, "jsonDetail", v.JsonDetail)
// continue
// }
// msgs[i] = &workMomentNotificationMsg
// }
// return msgs, nil
//}
//
//funcation (w *WorkMoments) clearWorkMomentsNotification(ctx context.Context) error {
// return w.db.ClearWorkMomentsNotification(ctx)
//}
//
//funcation (w *WorkMoments) getWorkMomentsNotificationUnReadCount(ctx context.Context) (model_struct.LocalWorkMomentsNotificationUnreadCount, error) {
// return w.db.GetWorkMomentsUnReadCount(ctx)
//}