feat: incr sync version.
This commit is contained in:
49
go/chao-sdk-core/internal/business/business.go
Normal file
49
go/chao-sdk-core/internal/business/business.go
Normal 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)
|
||||
}
|
||||
23
go/chao-sdk-core/internal/business/open_im_sdk_business.go
Normal file
23
go/chao-sdk-core/internal/business/open_im_sdk_business.go
Normal 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
72
go/chao-sdk-core/internal/cache/cahe.go
vendored
Normal 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
|
||||
}
|
||||
33
go/chao-sdk-core/internal/common/common.go
Normal file
33
go/chao-sdk-core/internal/common/common.go
Normal 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
|
||||
}
|
||||
33
go/chao-sdk-core/internal/common/object_storage.go
Normal file
33
go/chao-sdk-core/internal/common/object_storage.go
Normal 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)
|
||||
}
|
||||
1178
go/chao-sdk-core/internal/conversation_msg/conversation.go
Normal file
1178
go/chao-sdk-core/internal/conversation_msg/conversation.go
Normal file
File diff suppressed because it is too large
Load Diff
1064
go/chao-sdk-core/internal/conversation_msg/conversation_msg.go
Normal file
1064
go/chao-sdk-core/internal/conversation_msg/conversation_msg.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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})
|
||||
|
||||
}
|
||||
57
go/chao-sdk-core/internal/conversation_msg/convert.go
Normal file
57
go/chao-sdk-core/internal/conversation_msg/convert.go
Normal 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,
|
||||
}
|
||||
}
|
||||
492
go/chao-sdk-core/internal/conversation_msg/create_message.go
Normal file
492
go/chao-sdk-core/internal/conversation_msg/create_message.go
Normal 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
|
||||
}
|
||||
245
go/chao-sdk-core/internal/conversation_msg/delete.go
Normal file
245
go/chao-sdk-core/internal/conversation_msg/delete.go
Normal 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}})
|
||||
}
|
||||
217
go/chao-sdk-core/internal/conversation_msg/entering.go
Normal file
217
go/chao-sdk-core/internal/conversation_msg/entering.go
Normal 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
|
||||
}
|
||||
32
go/chao-sdk-core/internal/conversation_msg/image.go
Normal file
32
go/chao-sdk-core/internal/conversation_msg/image.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
347
go/chao-sdk-core/internal/conversation_msg/message_check.go
Normal file
347
go/chao-sdk-core/internal/conversation_msg/message_check.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
go/chao-sdk-core/internal/conversation_msg/message_controller.go
Normal file
124
go/chao-sdk-core/internal/conversation_msg/message_controller.go
Normal 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
|
||||
}
|
||||
102
go/chao-sdk-core/internal/conversation_msg/progress.go
Normal file
102
go/chao-sdk-core/internal/conversation_msg/progress.go
Normal 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)
|
||||
}
|
||||
}
|
||||
281
go/chao-sdk-core/internal/conversation_msg/read_drawing.go
Normal file
281
go/chao-sdk-core/internal/conversation_msg/read_drawing.go
Normal 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)
|
||||
}
|
||||
}
|
||||
203
go/chao-sdk-core/internal/conversation_msg/revoke.go
Normal file
203
go/chao-sdk-core/internal/conversation_msg/revoke.go
Normal 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
|
||||
}
|
||||
1180
go/chao-sdk-core/internal/conversation_msg/sdk.go
Normal file
1180
go/chao-sdk-core/internal/conversation_msg/sdk.go
Normal file
File diff suppressed because it is too large
Load Diff
148
go/chao-sdk-core/internal/conversation_msg/sync.go
Normal file
148
go/chao-sdk-core/internal/conversation_msg/sync.go
Normal 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
|
||||
}
|
||||
89
go/chao-sdk-core/internal/file/bitmap.go
Normal file
89
go/chao-sdk-core/internal/file/bitmap.go
Normal 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
|
||||
}
|
||||
62
go/chao-sdk-core/internal/file/cb.go
Normal file
62
go/chao-sdk-core/internal/file/cb.go
Normal 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)
|
||||
}
|
||||
24
go/chao-sdk-core/internal/file/file.go
Normal file
24
go/chao-sdk-core/internal/file/file.go
Normal 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
|
||||
}
|
||||
73
go/chao-sdk-core/internal/file/file_default.go
Normal file
73
go/chao-sdk-core/internal/file/file_default.go
Normal 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()
|
||||
}
|
||||
156
go/chao-sdk-core/internal/file/file_js.go
Normal file
156
go/chao-sdk-core/internal/file/file_js.go
Normal 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
|
||||
}
|
||||
40
go/chao-sdk-core/internal/file/file_test.go
Normal file
40
go/chao-sdk-core/internal/file/file_test.go
Normal 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)
|
||||
|
||||
}
|
||||
43
go/chao-sdk-core/internal/file/md5.go
Normal file
43
go/chao-sdk-core/internal/file/md5.go
Normal 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))
|
||||
}
|
||||
44
go/chao-sdk-core/internal/file/progress.go
Normal file
44
go/chao-sdk-core/internal/file/progress.go
Normal 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
|
||||
}
|
||||
576
go/chao-sdk-core/internal/file/upload.go
Normal file
576
go/chao-sdk-core/internal/file/upload.go
Normal 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
|
||||
}
|
||||
71
go/chao-sdk-core/internal/friend/conversion.go
Normal file
71
go/chao-sdk-core/internal/friend/conversion.go
Normal 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,
|
||||
}
|
||||
}
|
||||
199
go/chao-sdk-core/internal/friend/friend.go
Normal file
199
go/chao-sdk-core/internal/friend/friend.go
Normal 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
|
||||
}
|
||||
33
go/chao-sdk-core/internal/friend/hash.go
Normal file
33
go/chao-sdk-core/internal/friend/hash.go
Normal 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[:])
|
||||
}
|
||||
136
go/chao-sdk-core/internal/friend/notification.go
Normal file
136
go/chao-sdk-core/internal/friend/notification.go
Normal 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
|
||||
}
|
||||
344
go/chao-sdk-core/internal/friend/sdk.go
Normal file
344
go/chao-sdk-core/internal/friend/sdk.go
Normal 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
|
||||
}
|
||||
180
go/chao-sdk-core/internal/friend/sync.go
Normal file
180
go/chao-sdk-core/internal/friend/sync.go
Normal 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
|
||||
}
|
||||
72
go/chao-sdk-core/internal/friend/sync2.go
Normal file
72
go/chao-sdk-core/internal/friend/sync2.go
Normal 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()
|
||||
}
|
||||
13
go/chao-sdk-core/internal/friend/sync2_test.go
Normal file
13
go/chao-sdk-core/internal/friend/sync2_test.go
Normal 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])
|
||||
}
|
||||
62
go/chao-sdk-core/internal/full/full.go
Normal file
62
go/chao-sdk-core/internal/full/full.go
Normal 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
|
||||
}
|
||||
29
go/chao-sdk-core/internal/full/full_model.go
Normal file
29
go/chao-sdk-core/internal/full/full_model.go
Normal 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...)
|
||||
}
|
||||
199
go/chao-sdk-core/internal/full/open_im_sdk_full.go
Normal file
199
go/chao-sdk-core/internal/full/open_im_sdk_full.go
Normal 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
|
||||
}
|
||||
97
go/chao-sdk-core/internal/group/conversion.go
Normal file
97
go/chao-sdk-core/internal/group/conversion.go
Normal 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),
|
||||
}
|
||||
}
|
||||
343
go/chao-sdk-core/internal/group/group.go
Normal file
343
go/chao-sdk-core/internal/group/group.go
Normal 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
|
||||
}
|
||||
245
go/chao-sdk-core/internal/group/notification.go
Normal file
245
go/chao-sdk-core/internal/group/notification.go
Normal 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()
|
||||
}
|
||||
}
|
||||
383
go/chao-sdk-core/internal/group/sdk.go
Normal file
383
go/chao-sdk-core/internal/group/sdk.go
Normal 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,
|
||||
}
|
||||
}
|
||||
310
go/chao-sdk-core/internal/group/sync.go
Normal file
310
go/chao-sdk-core/internal/group/sync.go
Normal 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
|
||||
}
|
||||
347
go/chao-sdk-core/internal/group/sync2.go
Normal file
347
go/chao-sdk-core/internal/group/sync2.go
Normal 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()
|
||||
}
|
||||
1
go/chao-sdk-core/internal/group/sync2_test.go
Normal file
1
go/chao-sdk-core/internal/group/sync2_test.go
Normal file
@@ -0,0 +1 @@
|
||||
package group
|
||||
265
go/chao-sdk-core/internal/incrversion/option.go
Normal file
265
go/chao-sdk-core/internal/incrversion/option.go
Normal 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
|
||||
}
|
||||
61
go/chao-sdk-core/internal/interaction/compressor.go
Normal file
61
go/chao-sdk-core/internal/interaction/compressor.go
Normal 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
|
||||
}
|
||||
39
go/chao-sdk-core/internal/interaction/constant.go
Normal file
39
go/chao-sdk-core/internal/interaction/constant.go
Normal 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
|
||||
)
|
||||
52
go/chao-sdk-core/internal/interaction/context.go
Normal file
52
go/chao-sdk-core/internal/interaction/context.go
Normal 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,
|
||||
}
|
||||
}
|
||||
51
go/chao-sdk-core/internal/interaction/encoder.go
Normal file
51
go/chao-sdk-core/internal/interaction/encoder.go
Normal 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
|
||||
}
|
||||
579
go/chao-sdk-core/internal/interaction/long_conn_mgr.go
Normal file
579
go/chao-sdk-core/internal/interaction/long_conn_mgr.go
Normal 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
|
||||
}
|
||||
44
go/chao-sdk-core/internal/interaction/long_connection.go
Normal file
44
go/chao-sdk-core/internal/interaction/long_connection.go
Normal 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
|
||||
}
|
||||
387
go/chao-sdk-core/internal/interaction/msg_sync.go
Normal file
387
go/chao-sdk-core/internal/interaction/msg_sync.go
Normal 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
|
||||
|
||||
}
|
||||
32
go/chao-sdk-core/internal/interaction/reconnect.go
Normal file
32
go/chao-sdk-core/internal/interaction/reconnect.go
Normal 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
|
||||
}
|
||||
87
go/chao-sdk-core/internal/interaction/ws_default.go
Normal file
87
go/chao-sdk-core/internal/interaction/ws_default.go
Normal 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
|
||||
}
|
||||
140
go/chao-sdk-core/internal/interaction/ws_js.go
Normal file
140
go/chao-sdk-core/internal/interaction/ws_js.go
Normal 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
|
||||
}
|
||||
168
go/chao-sdk-core/internal/interaction/ws_resp_asyn.go
Normal file
168
go/chao-sdk-core/internal/interaction/ws_resp_asyn.go
Normal 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
|
||||
}
|
||||
85
go/chao-sdk-core/internal/third/log.go
Normal file
85
go/chao-sdk-core/internal/third/log.go
Normal 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
|
||||
}
|
||||
74
go/chao-sdk-core/internal/third/log_test.go
Normal file
74
go/chao-sdk-core/internal/third/log_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
37
go/chao-sdk-core/internal/third/progress.go
Normal file
37
go/chao-sdk-core/internal/third/progress.go
Normal 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)
|
||||
}
|
||||
57
go/chao-sdk-core/internal/third/third.go
Normal file
57
go/chao-sdk-core/internal/third/third.go
Normal 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
|
||||
}
|
||||
75
go/chao-sdk-core/internal/third/zip.go
Normal file
75
go/chao-sdk-core/internal/third/zip.go
Normal 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
|
||||
}
|
||||
43
go/chao-sdk-core/internal/user/convert.go
Normal file
43
go/chao-sdk-core/internal/user/convert.go
Normal 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,
|
||||
}
|
||||
}
|
||||
89
go/chao-sdk-core/internal/user/sdk.go
Normal file
89
go/chao-sdk-core/internal/user/sdk.go
Normal 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)
|
||||
}
|
||||
97
go/chao-sdk-core/internal/user/sync.go
Normal file
97
go/chao-sdk-core/internal/user/sync.go
Normal 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)
|
||||
}
|
||||
427
go/chao-sdk-core/internal/user/user.go
Normal file
427
go/chao-sdk-core/internal/user/user.go
Normal 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
|
||||
}
|
||||
28
go/chao-sdk-core/internal/util/notice.go
Normal file
28
go/chao-sdk-core/internal/util/notice.go
Normal 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
|
||||
// }
|
||||
//}
|
||||
239
go/chao-sdk-core/internal/util/post.go
Normal file
239
go/chao-sdk-core/internal/util/post.go
Normal 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
|
||||
}
|
||||
40
go/chao-sdk-core/internal/work_moments/sdk.go
Normal file
40
go/chao-sdk-core/internal/work_moments/sdk.go
Normal 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)
|
||||
//}
|
||||
78
go/chao-sdk-core/internal/work_moments/work_moments.go
Normal file
78
go/chao-sdk-core/internal/work_moments/work_moments.go
Normal 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)
|
||||
//}
|
||||
Reference in New Issue
Block a user