// 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 open_im_sdk import ( "context" "encoding/json" "errors" "fmt" "github.com/openimsdk/openim-sdk-core/v3/internal/business" conv "github.com/openimsdk/openim-sdk-core/v3/internal/conversation_msg" "github.com/openimsdk/openim-sdk-core/v3/internal/file" "github.com/openimsdk/openim-sdk-core/v3/internal/friend" "github.com/openimsdk/openim-sdk-core/v3/internal/full" "github.com/openimsdk/openim-sdk-core/v3/internal/group" "github.com/openimsdk/openim-sdk-core/v3/internal/interaction" "github.com/openimsdk/openim-sdk-core/v3/internal/third" "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/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/db" "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/utils" "github.com/openimsdk/openim-sdk-core/v3/sdk_struct" "github.com/openimsdk/protocol/push" "github.com/openimsdk/protocol/sdkws" "github.com/openimsdk/tools/log" "strings" "sync" "time" "unsafe" ) const ( LogoutStatus = iota + 1 Logging Logged ) const ( LogoutTips = "js sdk socket close" ) var ( // UserForSDK Client-independent user class UserForSDK *LoginMgr ) // CheckResourceLoad checks the SDK is resource load status. func CheckResourceLoad(uSDK *LoginMgr, funcName string) error { if uSDK == nil { return utils.Wrap(errors.New("CheckResourceLoad failed uSDK == nil "), "") } if funcName == "" { return nil } parts := strings.Split(funcName, ".") if parts[len(parts)-1] == "Login-fm" { return nil } if uSDK.Friend() == nil || uSDK.User() == nil || uSDK.Group() == nil || uSDK.Conversation() == nil || uSDK.Full() == nil { return utils.Wrap(errors.New("CheckResourceLoad failed, resource nil "), "") } return nil } type LoginMgr struct { friend *friend.Friend group *group.Group conversation *conv.Conversation user *user.User file *file.File business *business.Business full *full.Full db db_interface.DataBase longConnMgr *interaction.LongConnMgr msgSyncer *interaction.MsgSyncer third *third.Third token string loginUserID string connListener open_im_sdk_callback.OnConnListener justOnceFlag bool w sync.Mutex loginStatus int groupListener open_im_sdk_callback.OnGroupListener friendListener open_im_sdk_callback.OnFriendshipListener conversationListener open_im_sdk_callback.OnConversationListener advancedMsgListener open_im_sdk_callback.OnAdvancedMsgListener batchMsgListener open_im_sdk_callback.OnBatchMsgListener userListener open_im_sdk_callback.OnUserListener signalingListener open_im_sdk_callback.OnSignalingListener businessListener open_im_sdk_callback.OnCustomBusinessListener msgKvListener open_im_sdk_callback.OnMessageKvInfoListener conversationCh chan common.Cmd2Value cmdWsCh chan common.Cmd2Value heartbeatCmdCh chan common.Cmd2Value pushMsgAndMaxSeqCh chan common.Cmd2Value loginMgrCh chan common.Cmd2Value ctx context.Context cancel context.CancelFunc info *ccontext.GlobalConfig id2MinSeq map[string]int64 } func (u *LoginMgr) GroupListener() open_im_sdk_callback.OnGroupListener { return u.groupListener } func (u *LoginMgr) FriendListener() open_im_sdk_callback.OnFriendshipListener { return u.friendListener } func (u *LoginMgr) ConversationListener() open_im_sdk_callback.OnConversationListener { return u.conversationListener } func (u *LoginMgr) AdvancedMsgListener() open_im_sdk_callback.OnAdvancedMsgListener { return u.advancedMsgListener } func (u *LoginMgr) BatchMsgListener() open_im_sdk_callback.OnBatchMsgListener { return u.batchMsgListener } func (u *LoginMgr) UserListener() open_im_sdk_callback.OnUserListener { return u.userListener } func (u *LoginMgr) SignalingListener() open_im_sdk_callback.OnSignalingListener { return u.signalingListener } func (u *LoginMgr) BusinessListener() open_im_sdk_callback.OnCustomBusinessListener { return u.businessListener } func (u *LoginMgr) MsgKvListener() open_im_sdk_callback.OnMessageKvInfoListener { return u.msgKvListener } func (u *LoginMgr) BaseCtx() context.Context { return u.ctx } func (u *LoginMgr) Exit() { u.cancel() } func (u *LoginMgr) GetToken() string { return u.token } func (u *LoginMgr) Third() *third.Third { return u.third } func (u *LoginMgr) ImConfig() sdk_struct.IMConfig { return sdk_struct.IMConfig{ PlatformID: u.info.PlatformID, ApiAddr: u.info.ApiAddr, WsAddr: u.info.WsAddr, DataDir: u.info.DataDir, LogLevel: u.info.LogLevel, IsExternalExtensions: u.info.IsExternalExtensions, } } func (u *LoginMgr) Conversation() *conv.Conversation { return u.conversation } func (u *LoginMgr) User() *user.User { return u.user } func (u *LoginMgr) File() *file.File { return u.file } func (u *LoginMgr) Full() *full.Full { return u.full } func (u *LoginMgr) Group() *group.Group { return u.group } func (u *LoginMgr) Friend() *friend.Friend { return u.friend } func (u *LoginMgr) SetConversationListener(conversationListener open_im_sdk_callback.OnConversationListener) { u.conversationListener = conversationListener } func (u *LoginMgr) SetAdvancedMsgListener(advancedMsgListener open_im_sdk_callback.OnAdvancedMsgListener) { u.advancedMsgListener = advancedMsgListener } func (u *LoginMgr) SetMessageKvInfoListener(messageKvInfoListener open_im_sdk_callback.OnMessageKvInfoListener) { u.msgKvListener = messageKvInfoListener } func (u *LoginMgr) SetBatchMsgListener(batchMsgListener open_im_sdk_callback.OnBatchMsgListener) { u.batchMsgListener = batchMsgListener } func (u *LoginMgr) SetFriendListener(friendListener open_im_sdk_callback.OnFriendshipListener) { u.friendListener = friendListener } func (u *LoginMgr) SetGroupListener(groupListener open_im_sdk_callback.OnGroupListener) { u.groupListener = groupListener } func (u *LoginMgr) SetUserListener(userListener open_im_sdk_callback.OnUserListener) { u.userListener = userListener } func (u *LoginMgr) SetCustomBusinessListener(listener open_im_sdk_callback.OnCustomBusinessListener) { u.businessListener = listener } func (u *LoginMgr) GetLoginUserID() string { return u.loginUserID } func (u *LoginMgr) logoutListener(ctx context.Context) { for { select { case <-u.loginMgrCh: log.ZDebug(ctx, "logoutListener exit") err := u.logout(ctx, true) if err != nil { log.ZError(ctx, "logout error", err) } case <-ctx.Done(): log.ZInfo(ctx, "logoutListener done sdk logout.....") return } } } func NewLoginMgr() *LoginMgr { return &LoginMgr{ info: &ccontext.GlobalConfig{}, // 分配内存空间 } } func (u *LoginMgr) getLoginStatus(_ context.Context) int { u.w.Lock() defer u.w.Unlock() return u.loginStatus } func (u *LoginMgr) setLoginStatus(status int) { u.w.Lock() defer u.w.Unlock() u.loginStatus = status } func (u *LoginMgr) checkSendingMessage(ctx context.Context) { sendingMessages, err := u.db.GetAllSendingMessages(ctx) if err != nil { log.ZError(ctx, "GetAllSendingMessages failed", err) } for _, message := range sendingMessages { if err := u.handlerSendingMsg(ctx, message); err != nil { log.ZError(ctx, "handlerSendingMsg failed", err, "message", message) } if err := u.db.DeleteSendingMessage(ctx, message.ConversationID, message.ClientMsgID); err != nil { log.ZError(ctx, "DeleteSendingMessage failed", err, "conversationID", message.ConversationID, "clientMsgID", message.ClientMsgID) } } } func (u *LoginMgr) handlerSendingMsg(ctx context.Context, sendingMsg *model_struct.LocalSendingMessages) error { tableMessage, err := u.db.GetMessage(ctx, sendingMsg.ConversationID, sendingMsg.ClientMsgID) if err != nil { return err } if tableMessage.Status != constant.MsgStatusSending { return nil } err = u.db.UpdateMessage(ctx, sendingMsg.ConversationID, &model_struct.LocalChatLog{ClientMsgID: sendingMsg.ClientMsgID, Status: constant.MsgStatusSendFailed}) if err != nil { return err } conversation, err := u.db.GetConversation(ctx, sendingMsg.ConversationID) if err != nil { return err } latestMsg := &sdk_struct.MsgStruct{} if err := json.Unmarshal([]byte(conversation.LatestMsg), &latestMsg); err != nil { return err } if latestMsg.ClientMsgID == sendingMsg.ClientMsgID { latestMsg.Status = constant.MsgStatusSendFailed conversation.LatestMsg = utils.StructToJsonString(latestMsg) return u.db.UpdateConversation(ctx, conversation) } return nil } func (u *LoginMgr) login(ctx context.Context, userID, token string) error { if u.getLoginStatus(ctx) == Logged { return sdkerrs.ErrLoginRepeat } u.setLoginStatus(Logging) u.info.UserID = userID u.info.Token = token log.ZDebug(ctx, "login start... ", "userID", userID, "token", token) t1 := time.Now() u.token = token u.loginUserID = userID var err error u.db, err = db.NewDataBase(ctx, userID, u.info.DataDir, int(u.info.LogLevel)) if err != nil { return sdkerrs.ErrSdkInternal.WrapMsg("init database " + err.Error()) } u.checkSendingMessage(ctx) log.ZDebug(ctx, "NewDataBase ok", "userID", userID, "dataDir", u.info.DataDir, "login cost time", time.Since(t1)) u.user = user.NewUser(u.db, u.loginUserID, u.conversationCh) u.file = file.NewFile(u.db, u.loginUserID) u.friend = friend.NewFriend(u.loginUserID, u.db, u.user, u.conversationCh) u.group = group.NewGroup(u.loginUserID, u.db, u.conversationCh) u.full = full.NewFull(u.user, u.friend, u.group, u.conversationCh, u.db) u.business = business.NewBusiness(u.db) u.third = third.NewThird(u.info.PlatformID, u.loginUserID, constant.SdkVersion, u.info.SystemType, u.info.LogFilePath, u.file) log.ZDebug(ctx, "forcedSynchronization success...", "login cost time: ", time.Since(t1)) u.msgSyncer, _ = interaction.NewMsgSyncer(ctx, u.conversationCh, u.pushMsgAndMaxSeqCh, u.loginUserID, u.longConnMgr, u.db, 0) u.conversation = conv.NewConversation(ctx, u.longConnMgr, u.db, u.conversationCh, u.friend, u.group, u.user, u.business, u.full, u.file) u.setListener(ctx) u.run(ctx) u.setLoginStatus(Logged) log.ZDebug(ctx, "login success...", "login cost time: ", time.Since(t1)) return nil } func (u *LoginMgr) setListener(ctx context.Context) { setListener(ctx, &u.userListener, u.UserListener, u.user.SetListener, newEmptyUserListener) setListener(ctx, &u.friendListener, u.FriendListener, u.friend.SetListener, newEmptyFriendshipListener) setListener(ctx, &u.groupListener, u.GroupListener, u.group.SetGroupListener, newEmptyGroupListener) setListener(ctx, &u.conversationListener, u.ConversationListener, u.conversation.SetConversationListener, newEmptyConversationListener) setListener(ctx, &u.advancedMsgListener, u.AdvancedMsgListener, u.conversation.SetMsgListener, newEmptyAdvancedMsgListener) setListener(ctx, &u.batchMsgListener, u.BatchMsgListener, u.conversation.SetBatchMsgListener, nil) setListener(ctx, &u.businessListener, u.BusinessListener, u.business.SetListener, newEmptyCustomBusinessListener) } func setListener[T any](ctx context.Context, listener *T, getter func() T, setFunc func(listener func() T), newFunc func(context.Context) T) { if *(*unsafe.Pointer)(unsafe.Pointer(listener)) == nil && newFunc != nil { *listener = newFunc(ctx) } setFunc(getter) } func (u *LoginMgr) run(ctx context.Context) { u.longConnMgr.Run(ctx) go u.msgSyncer.DoListener(ctx) go common.DoListener(u.conversation, u.ctx) go u.logoutListener(ctx) } func (u *LoginMgr) InitSDK(config sdk_struct.IMConfig, listener open_im_sdk_callback.OnConnListener) bool { if listener == nil { return false } u.info = &ccontext.GlobalConfig{} u.info.IMConfig = config u.connListener = listener u.initResources() return true } func (u *LoginMgr) Context() context.Context { return u.ctx } func (u *LoginMgr) initResources() { ctx := ccontext.WithInfo(context.Background(), u.info) u.ctx, u.cancel = context.WithCancel(ctx) u.conversationCh = make(chan common.Cmd2Value, 1000) u.heartbeatCmdCh = make(chan common.Cmd2Value, 10) u.pushMsgAndMaxSeqCh = make(chan common.Cmd2Value, 1000) u.loginMgrCh = make(chan common.Cmd2Value, 1) u.longConnMgr = interaction.NewLongConnMgr(u.ctx, u.connListener, u.heartbeatCmdCh, u.pushMsgAndMaxSeqCh, u.loginMgrCh) u.ctx = ccontext.WithApiErrCode(u.ctx, &apiErrCallback{loginMgrCh: u.loginMgrCh, listener: u.connListener}) u.setLoginStatus(LogoutStatus) } func (u *LoginMgr) UnInitSDK() { if u.getLoginStatus(context.Background()) == Logged { fmt.Println("sdk not logout, please logout first") return } u.info = nil u.setLoginStatus(0) } // token error recycle recourse, kicked not recycle func (u *LoginMgr) logout(ctx context.Context, isTokenValid bool) error { if ccontext.Info(ctx).OperationID() == LogoutTips { isTokenValid = true } if !isTokenValid { ctx, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() err := u.longConnMgr.SendReqWaitResp(ctx, &push.DelUserPushTokenReq{UserID: u.info.UserID, PlatformID: u.info.PlatformID}, constant.LogoutMsg, &push.DelUserPushTokenResp{}) if err != nil { log.ZWarn(ctx, "TriggerCmdLogout server recycle resources failed...", err) } else { log.ZDebug(ctx, "TriggerCmdLogout server recycle resources success...") } } u.Exit() err := u.db.Close(u.ctx) if err != nil { log.ZWarn(ctx, "TriggerCmdLogout db recycle resources failed...", err) } // user object must be rest when user logout u.initResources() log.ZDebug(ctx, "TriggerCmdLogout client success...", "isTokenValid", isTokenValid) return nil } func (u *LoginMgr) setAppBackgroundStatus(ctx context.Context, isBackground bool) error { if u.longConnMgr.GetConnectionStatus() == 0 { u.longConnMgr.SetBackground(isBackground) return nil } var resp sdkws.SetAppBackgroundStatusResp err := u.longConnMgr.SendReqWaitResp(ctx, &sdkws.SetAppBackgroundStatusReq{UserID: u.loginUserID, IsBackground: isBackground}, constant.SetBackgroundStatus, &resp) if err != nil { return err } else { u.longConnMgr.SetBackground(isBackground) if isBackground == false { _ = common.TriggerCmdWakeUp(u.heartbeatCmdCh) } return nil } }