You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
217 lines
6.7 KiB
217 lines
6.7 KiB
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
|
|
}
|
|
|