feat: add redis cache and fanout support
This commit is contained in:
+2
-2
@@ -86,7 +86,7 @@ type videoCall struct {
|
||||
// Call participants (session sid -> callPartyData).
|
||||
parties map[string]callPartyData
|
||||
// Call message seq ID.
|
||||
seq int
|
||||
seq int64
|
||||
// Call message content.
|
||||
content any
|
||||
// Call message content mime type.
|
||||
@@ -300,7 +300,7 @@ func (call *videoCall) messageHead(head map[string]any, newState string, duratio
|
||||
head = map[string]any{}
|
||||
}
|
||||
|
||||
head["replace"] = ":" + strconv.Itoa(call.seq)
|
||||
head["replace"] = ":" + strconv.FormatInt(call.seq, 10)
|
||||
head["webrtc"] = newState
|
||||
|
||||
if duration > 0 {
|
||||
|
||||
+23
-23
@@ -27,9 +27,9 @@ type MsgGetOpts struct {
|
||||
// Return results modified since this timespamp.
|
||||
IfModifiedSince *time.Time `json:"ims,omitempty"`
|
||||
// Load messages/ranges with IDs equal or greater than this (inclusive or closed).
|
||||
SinceId int `json:"since,omitempty"`
|
||||
SinceId int64 `json:"since,omitempty"`
|
||||
// Load messages/ranges with IDs lower than this (exclusive or open).
|
||||
BeforeId int `json:"before,omitempty"`
|
||||
BeforeId int64 `json:"before,omitempty"`
|
||||
// Limit the number of messages loaded.
|
||||
Limit int `json:"limit,omitempty"`
|
||||
// Fetch messages with IDs in these ranges.
|
||||
@@ -100,8 +100,8 @@ type MsgSetQuery struct {
|
||||
// MsgRange is either an individual ID (HiId=0) or a randge of IDs, low end inclusive (closed),
|
||||
// high-end exclusive (open): [LowId .. HiId), e.g. 1..5 -> 1, 2, 3, 4.
|
||||
type MsgRange struct {
|
||||
LowId int `json:"low,omitempty"`
|
||||
HiId int `json:"hi,omitempty"`
|
||||
LowId int64 `json:"low,omitempty"`
|
||||
HiId int64 `json:"hi,omitempty"`
|
||||
}
|
||||
|
||||
/****************************************************************
|
||||
@@ -321,7 +321,7 @@ type MsgClientNote struct {
|
||||
// what is being reported: "recv" - message received, "read" - message read, "kp" - typing notification
|
||||
What string `json:"what"`
|
||||
// Server-issued message ID being reported
|
||||
SeqId int `json:"seq,omitempty"`
|
||||
SeqId int64 `json:"seq,omitempty"`
|
||||
// Client's count of unread messages to report back to the server. Used in push notifications on iOS.
|
||||
Unread int `json:"unread,omitempty"`
|
||||
// Call event.
|
||||
@@ -453,9 +453,9 @@ type MsgTopicDesc struct {
|
||||
// Actual access mode
|
||||
Acs *MsgAccessMode `json:"acs,omitempty"`
|
||||
// Max message ID
|
||||
SeqId int `json:"seq,omitempty"`
|
||||
ReadSeqId int `json:"read,omitempty"`
|
||||
RecvSeqId int `json:"recv,omitempty"`
|
||||
SeqId int64 `json:"seq,omitempty"`
|
||||
ReadSeqId int64 `json:"read,omitempty"`
|
||||
RecvSeqId int64 `json:"recv,omitempty"`
|
||||
// Id of the last delete operation as seen by the requesting user
|
||||
DelId int `json:"clear,omitempty"`
|
||||
SubCnt int `json:"subcnt,omitempty"`
|
||||
@@ -475,13 +475,13 @@ func (src *MsgTopicDesc) describe() string {
|
||||
s += " acs={" + src.Acs.describe() + "}"
|
||||
}
|
||||
if src.SeqId != 0 {
|
||||
s += " seq=" + strconv.Itoa(src.SeqId)
|
||||
s += " seq=" + strconv.FormatInt(src.SeqId, 10)
|
||||
}
|
||||
if src.ReadSeqId != 0 {
|
||||
s += " read=" + strconv.Itoa(src.ReadSeqId)
|
||||
s += " read=" + strconv.FormatInt(src.ReadSeqId, 10)
|
||||
}
|
||||
if src.RecvSeqId != 0 {
|
||||
s += " recv=" + strconv.Itoa(src.RecvSeqId)
|
||||
s += " recv=" + strconv.FormatInt(src.RecvSeqId, 10)
|
||||
}
|
||||
if src.DelId != 0 {
|
||||
s += " clear=" + strconv.Itoa(src.DelId)
|
||||
@@ -517,9 +517,9 @@ type MsgTopicSub struct {
|
||||
// Acs.Mode = want & given. The field is not a pointer because at least one value is always assigned.
|
||||
Acs MsgAccessMode `json:"acs,omitempty"`
|
||||
// ID of the message reported by the given user as read
|
||||
ReadSeqId int `json:"read,omitempty"`
|
||||
ReadSeqId int64 `json:"read,omitempty"`
|
||||
// ID of the message reported by the given user as received
|
||||
RecvSeqId int `json:"recv,omitempty"`
|
||||
RecvSeqId int64 `json:"recv,omitempty"`
|
||||
// Topic's public data
|
||||
Public any `json:"public,omitempty"`
|
||||
// Topic's trusted public data
|
||||
@@ -540,7 +540,7 @@ type MsgTopicSub struct {
|
||||
// Timestamp of the last message in the topic.
|
||||
TouchedAt *time.Time `json:"touched,omitempty"`
|
||||
// ID of the last {data} message in a topic
|
||||
SeqId int `json:"seq,omitempty"`
|
||||
SeqId int64 `json:"seq,omitempty"`
|
||||
// Id of the latest Delete operation
|
||||
DelId int `json:"clear,omitempty"`
|
||||
// Number of subscribers, group topics only.
|
||||
@@ -555,13 +555,13 @@ func (src *MsgTopicSub) describe() string {
|
||||
s := src.Topic + ":" + src.User + " online=" + strconv.FormatBool(src.Online) + " acs=" + src.Acs.describe()
|
||||
|
||||
if src.SeqId != 0 {
|
||||
s += " seq=" + strconv.Itoa(src.SeqId)
|
||||
s += " seq=" + strconv.FormatInt(src.SeqId, 10)
|
||||
}
|
||||
if src.ReadSeqId != 0 {
|
||||
s += " read=" + strconv.Itoa(src.ReadSeqId)
|
||||
s += " read=" + strconv.FormatInt(src.ReadSeqId, 10)
|
||||
}
|
||||
if src.RecvSeqId != 0 {
|
||||
s += " recv=" + strconv.Itoa(src.RecvSeqId)
|
||||
s += " recv=" + strconv.FormatInt(src.RecvSeqId, 10)
|
||||
}
|
||||
if src.DelId != 0 {
|
||||
s += " clear=" + strconv.Itoa(src.DelId)
|
||||
@@ -621,7 +621,7 @@ type MsgServerData struct {
|
||||
From string `json:"from,omitempty"`
|
||||
Timestamp time.Time `json:"ts"`
|
||||
DeletedAt *time.Time `json:"deleted,omitempty"`
|
||||
SeqId int `json:"seq"`
|
||||
SeqId int64 `json:"seq"`
|
||||
Head map[string]any `json:"head,omitempty"`
|
||||
Content any `json:"content"`
|
||||
}
|
||||
@@ -636,7 +636,7 @@ func (src *MsgServerData) copy() *MsgServerData {
|
||||
}
|
||||
|
||||
func (src *MsgServerData) describe() string {
|
||||
s := src.Topic + " from=" + src.From + " seq=" + strconv.Itoa(src.SeqId)
|
||||
s := src.Topic + " from=" + src.From + " seq=" + strconv.FormatInt(src.SeqId, 10)
|
||||
if src.DeletedAt != nil {
|
||||
s += " deleted"
|
||||
} else {
|
||||
@@ -654,7 +654,7 @@ type MsgServerPres struct {
|
||||
Src string `json:"src,omitempty"`
|
||||
What string `json:"what"`
|
||||
UserAgent string `json:"ua,omitempty"`
|
||||
SeqId int `json:"seq,omitempty"`
|
||||
SeqId int64 `json:"seq,omitempty"`
|
||||
DelId int `json:"clear,omitempty"`
|
||||
DelSeq []MsgRange `json:"delseq,omitempty"`
|
||||
AcsTarget string `json:"tgt,omitempty"`
|
||||
@@ -706,7 +706,7 @@ func (src *MsgServerPres) describe() string {
|
||||
s += " ua=" + src.UserAgent
|
||||
}
|
||||
if src.SeqId != 0 {
|
||||
s += " seq=" + strconv.Itoa(src.SeqId)
|
||||
s += " seq=" + strconv.FormatInt(src.SeqId, 10)
|
||||
}
|
||||
if src.DelId != 0 {
|
||||
s += " clear=" + strconv.Itoa(src.DelId)
|
||||
@@ -799,7 +799,7 @@ type MsgServerInfo struct {
|
||||
// The event being reported: "rcpt" - message received, "read" - message read, "kp" - typing notification, "call" - video call.
|
||||
What string `json:"what"`
|
||||
// Server-issued message ID being reported.
|
||||
SeqId int `json:"seq,omitempty"`
|
||||
SeqId int64 `json:"seq,omitempty"`
|
||||
// Call event.
|
||||
Event string `json:"event,omitempty"`
|
||||
// Arbitrary json payload (used by video calls).
|
||||
@@ -829,7 +829,7 @@ func (src *MsgServerInfo) describe() string {
|
||||
}
|
||||
s += " what=" + src.What + " from=" + src.From
|
||||
if src.SeqId > 0 {
|
||||
s += " seq=" + strconv.Itoa(src.SeqId)
|
||||
s += " seq=" + strconv.FormatInt(src.SeqId, 10)
|
||||
}
|
||||
if len(src.Payload) > 0 {
|
||||
s += " payload=<..." + strconv.Itoa(len(src.Payload)) + " bytes ...>"
|
||||
|
||||
+12
-5
@@ -326,13 +326,20 @@ type TenantBusinessAdapter interface {
|
||||
|
||||
TenantMessageSave(t.TenantID, *t.Message) error
|
||||
TenantMessageGetByClientID(t.TenantID, t.Uid, string) (*t.Message, error)
|
||||
TenantMessageGetOne(t.TenantID, string, int) (*t.Message, error)
|
||||
TenantMessageRecall(t.TenantID, string, int, t.Uid, int, time.Time) (*t.Message, error)
|
||||
TenantMessageEdit(t.TenantID, string, int, t.Uid, int, t.KVMap, any, string, time.Time) (*t.Message, error)
|
||||
TenantMessageGetOne(t.TenantID, string, int64) (*t.Message, error)
|
||||
TenantMessageRecall(t.TenantID, string, int64, t.Uid, int, time.Time) (*t.Message, error)
|
||||
TenantMessageEdit(t.TenantID, string, int64, t.Uid, int, t.KVMap, any, string, time.Time) (*t.Message, error)
|
||||
TenantMessageGetAll(t.TenantID, string, t.Uid, *t.QueryOpt) ([]t.Message, error)
|
||||
TenantMessageDeleteList(t.TenantID, string, *t.DelMessage) error
|
||||
TenantMessageGetDeleted(t.TenantID, string, t.Uid, *t.QueryOpt) ([]t.DelMessage, error)
|
||||
TenantMessageSearch(t.TenantID, string, t.Uid, string, string, string, int) ([]t.Message, string, error)
|
||||
TenantGroupMessageSave(t.TenantID, *t.Message) error
|
||||
TenantGroupMessageGetByClientID(t.TenantID, string, t.Uid, string) (*t.Message, error)
|
||||
TenantGroupMessageGetOne(t.TenantID, string, int64) (*t.Message, error)
|
||||
TenantGroupMessageRecall(t.TenantID, string, int64, t.Uid, int, time.Time) (*t.Message, error)
|
||||
TenantGroupMessageEdit(t.TenantID, string, int64, t.Uid, int, t.KVMap, any, string, time.Time) (*t.Message, error)
|
||||
TenantGroupMessageGetAll(t.TenantID, string, t.Uid, *t.QueryOpt) ([]t.Message, error)
|
||||
TenantGroupMessageDeleteList(t.TenantID, string, *t.DelMessage) error
|
||||
|
||||
TenantConversationSettingsGet(t.TenantID, t.Uid, string) (*t.ConversationSettings, error)
|
||||
TenantConversationSettingsUpdate(t.TenantID, t.Uid, string, t.ConversationSettingsPatch) (*t.ConversationSettings, error)
|
||||
@@ -340,8 +347,8 @@ type TenantBusinessAdapter interface {
|
||||
TenantTopicDraftPut(t.TenantID, t.Uid, string, string, []byte, int) (*t.TopicDraft, error)
|
||||
|
||||
TenantCallCreate(t.TenantID, *t.CallRecord) error
|
||||
TenantCallAccept(t.TenantID, string, int, time.Time) error
|
||||
TenantCallFinish(t.TenantID, string, int, string, int64, time.Time) error
|
||||
TenantCallAccept(t.TenantID, string, int64, time.Time) error
|
||||
TenantCallFinish(t.TenantID, string, int64, string, int64, time.Time) error
|
||||
TenantCallList(t.TenantID, t.Uid, string, string, int) ([]t.CallRecord, string, error)
|
||||
|
||||
TenantGroupSettingsCreate(t.TenantID, *t.GroupSettings) (*t.GroupSettings, error)
|
||||
|
||||
@@ -126,7 +126,7 @@ func TestRangesToSql(t *testing.T) {
|
||||
ranges := []types.Range{{Low: 5, Hi: 0}}
|
||||
sql, args := RangesToSql(ranges)
|
||||
expectedSql := "IN (?)"
|
||||
expectedArgs := []any{5}
|
||||
expectedArgs := []any{int64(5)}
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func TestRangesToSql(t *testing.T) {
|
||||
ranges = []types.Range{{Low: 5, Hi: 8}}
|
||||
sql, args = RangesToSql(ranges)
|
||||
expectedSql = "BETWEEN ? AND ?"
|
||||
expectedArgs = []any{5, 7} // Hi-1 for BETWEEN
|
||||
expectedArgs = []any{int64(5), int64(7)} // Hi-1 for BETWEEN
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func TestRangesToSql(t *testing.T) {
|
||||
ranges = []types.Range{{Low: 1, Hi: 3}, {Low: 5, Hi: 0}, {Low: 8, Hi: 10}}
|
||||
sql, args = RangesToSql(ranges)
|
||||
expectedSql = "IN (?,?,?,?,?)"
|
||||
expectedArgs = []any{1, 2, 5, 8, 9}
|
||||
expectedArgs = []any{int64(1), int64(2), int64(5), int64(8), int64(9)}
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
|
||||
@@ -754,7 +754,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
usebt TINYINT DEFAULT 0,
|
||||
owner BIGINT NOT NULL DEFAULT 0,
|
||||
access JSON,
|
||||
seqid INT NOT NULL DEFAULT 0,
|
||||
seqid BIGINT NOT NULL DEFAULT 0,
|
||||
delid INT DEFAULT 0,
|
||||
subcnt INT DEFAULT 0,
|
||||
public JSON,
|
||||
@@ -799,8 +799,8 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
userid BIGINT NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
delid INT DEFAULT 0,
|
||||
recvseqid INT DEFAULT 0,
|
||||
readseqid INT DEFAULT 0,
|
||||
recvseqid BIGINT DEFAULT 0,
|
||||
readseqid BIGINT DEFAULT 0,
|
||||
modewant CHAR(8),
|
||||
modegiven CHAR(8),
|
||||
private JSON,
|
||||
@@ -822,7 +822,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
deletedat DATETIME(3),
|
||||
delid INT DEFAULT 0,
|
||||
seqid INT NOT NULL,
|
||||
seqid BIGINT NOT NULL,
|
||||
topic CHAR(25) NOT NULL,` +
|
||||
"`from` BIGINT NOT NULL," +
|
||||
`client_msg_id VARCHAR(64),
|
||||
@@ -845,7 +845,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seqid INT NOT NULL,
|
||||
seqid BIGINT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
actor BIGINT NOT NULL,
|
||||
head JSON,
|
||||
@@ -1050,7 +1050,7 @@ func createIMCallTables(tx *sql.Tx) error {
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
call_id VARCHAR(64) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seq_id INT NOT NULL,
|
||||
seq_id BIGINT NOT NULL,
|
||||
caller_id BIGINT NOT NULL,
|
||||
callee_id BIGINT NOT NULL,
|
||||
call_type VARCHAR(16) NOT NULL,
|
||||
@@ -1403,7 +1403,7 @@ func createIMUserProductTables(tx *sql.Tx) error {
|
||||
target_type VARCHAR(16) NOT NULL,
|
||||
target_user_id BIGINT NOT NULL DEFAULT 0,
|
||||
target_topic VARCHAR(32) NOT NULL DEFAULT '',
|
||||
target_seq INT NOT NULL DEFAULT 0,
|
||||
target_seq BIGINT NOT NULL DEFAULT 0,
|
||||
reason_code VARCHAR(64) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
evidence_file_ids JSON,
|
||||
@@ -1481,7 +1481,7 @@ func upgradeIMMessageActions(a *adapter) error {
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seqid INT NOT NULL,
|
||||
seqid BIGINT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
actor BIGINT NOT NULL,
|
||||
head JSON,
|
||||
@@ -1554,7 +1554,7 @@ func createIMRelationTables(exec sqlExecutor) error {
|
||||
user_id BIGINT NOT NULL,
|
||||
item_type VARCHAR(16) NOT NULL,
|
||||
source_topic CHAR(25) NOT NULL,
|
||||
source_seq INT NOT NULL,
|
||||
source_seq BIGINT NOT NULL,
|
||||
snapshot JSON,
|
||||
attachment_refs JSON,
|
||||
availability VARCHAR(16) NOT NULL DEFAULT 'available',
|
||||
|
||||
@@ -23,6 +23,26 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error {
|
||||
INDEX idx_im_group_settings_owner(owner_user_id, group_status),
|
||||
INDEX idx_im_group_settings_status_updated(group_status, updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_group_messages (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT, topic VARCHAR(64) NOT NULL, group_id BIGINT NOT NULL,
|
||||
seqid BIGINT NOT NULL, from_user BIGINT NOT NULL, client_msg_id VARCHAR(64),
|
||||
msg_type VARCHAR(32) NOT NULL DEFAULT 'text', status VARCHAR(32) NOT NULL DEFAULT 'normal',
|
||||
content JSON, head JSON, mentioned_user_ids JSON, mention_all TINYINT(1) NOT NULL DEFAULT 0,
|
||||
reply_seqid BIGINT, root_seqid BIGINT, file_count INT NOT NULL DEFAULT 0,
|
||||
content_size INT NOT NULL DEFAULT 0, has_risk TINYINT(1) NOT NULL DEFAULT 0,
|
||||
audit_status VARCHAR(32) NOT NULL DEFAULT 'pass', version INT NOT NULL DEFAULT 1,
|
||||
recalled_at DATETIME(3), recalled_by BIGINT, deleted_at DATETIME(3), expires_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL, updated_at DATETIME(3) NOT NULL,
|
||||
PRIMARY KEY(id), UNIQUE INDEX uk_im_group_messages_group_seq(group_id, seqid),
|
||||
UNIQUE INDEX uk_im_group_messages_topic_seq(topic, seqid),
|
||||
UNIQUE INDEX uk_im_group_messages_topic_sender_client_mid(topic, from_user, client_msg_id),
|
||||
INDEX idx_im_group_messages_group_created(group_id, created_at),
|
||||
INDEX idx_im_group_messages_group_status_seq(group_id, status, seqid),
|
||||
INDEX idx_im_group_messages_topic_created(topic, created_at),
|
||||
INDEX idx_im_group_messages_topic_from_seq(topic, from_user, seqid),
|
||||
INDEX idx_im_group_messages_audit(audit_status, created_at),
|
||||
INDEX idx_im_group_messages_expires(expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_group_members (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT, topic VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL,
|
||||
role VARCHAR(16) NOT NULL DEFAULT 'member', member_status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
@@ -105,7 +125,7 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_group_files (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT, group_file_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL,
|
||||
file_id VARCHAR(64) NOT NULL, source_topic VARCHAR(32) NOT NULL DEFAULT '', source_seq INT NOT NULL DEFAULT 0,
|
||||
file_id VARCHAR(64) NOT NULL, source_topic VARCHAR(32) NOT NULL DEFAULT '', source_seq BIGINT NOT NULL DEFAULT 0,
|
||||
uploader_id BIGINT NOT NULL, file_name VARCHAR(255) NOT NULL DEFAULT '', mime_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
file_type VARCHAR(32) NOT NULL DEFAULT 'file', file_size BIGINT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active', deleted_by BIGINT, deleted_at DATETIME(3),
|
||||
|
||||
@@ -92,6 +92,56 @@ func finalizeTenantMessage(id t.TenantID, msg t.Message, recalledBy sql.NullInt6
|
||||
return msg
|
||||
}
|
||||
|
||||
func normalizeGroupMessage(id t.TenantID, msg t.Message, recalledBy sql.NullInt64) t.Message {
|
||||
msg.TenantID = id
|
||||
msg.From = common.EncodeUidString(msg.From).String()
|
||||
if recalledBy.Valid {
|
||||
msg.RecalledBy = common.EncodeUidString(strconv.FormatInt(recalledBy.Int64, 10)).String()
|
||||
}
|
||||
msg.Content = common.FromJSON(msg.Content)
|
||||
return msg
|
||||
}
|
||||
|
||||
func messageHeadType(head t.KVMap) string {
|
||||
if head == nil {
|
||||
return "text"
|
||||
}
|
||||
for _, key := range []string{"x-im-type", "type"} {
|
||||
if value, ok := head[key].(string); ok {
|
||||
value = strings.TrimSpace(value)
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return "text"
|
||||
}
|
||||
|
||||
func messageHeadJSON(head t.KVMap, key string) []byte {
|
||||
if head == nil {
|
||||
return nil
|
||||
}
|
||||
if value, ok := head[key]; ok {
|
||||
return common.ToJSON(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func messageHeadBool(head t.KVMap, key string) bool {
|
||||
if head == nil {
|
||||
return false
|
||||
}
|
||||
switch value := head[key].(type) {
|
||||
case bool:
|
||||
return value
|
||||
case string:
|
||||
parsed, _ := strconv.ParseBool(value)
|
||||
return parsed
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func stampTenantFile(id t.TenantID, file *t.FileDef) *t.FileDef {
|
||||
if file != nil {
|
||||
file.TenantID = id
|
||||
@@ -1105,6 +1155,63 @@ func tenantMessageDeleteList(tx *sqlx.Tx, tenantID t.TenantID, topic string, toD
|
||||
return nil
|
||||
}
|
||||
|
||||
func groupMessageDeleteList(tx *sqlx.Tx, topic string, toDel *t.DelMessage) error {
|
||||
var err error
|
||||
if toDel == nil {
|
||||
if _, err = tx.Exec("DELETE FROM im_dellog WHERE topic=?", topic); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec("DELETE FROM im_group_messages WHERE topic=?", topic)
|
||||
return err
|
||||
}
|
||||
delRanges := toDel.SeqIdRanges
|
||||
if toDel.DeletedFor == "" {
|
||||
where := "m.topic=?"
|
||||
args := []any{topic}
|
||||
if len(delRanges) > 0 {
|
||||
rSQL, rArgs := common.RangesToSql(delRanges)
|
||||
where += " AND m.seqid " + rSQL
|
||||
args = append(args, rArgs...)
|
||||
}
|
||||
where += " AND m.deleted_at IS NULL"
|
||||
if newerThan := toDel.GetNewerThan(); newerThan != nil {
|
||||
where += " AND m.created_at>?"
|
||||
args = append(args, newerThan)
|
||||
}
|
||||
var seqIDs []int
|
||||
if err = tx.Select(&seqIDs, "SELECT seqid FROM im_group_messages AS m WHERE "+where, args...); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(seqIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Ints(seqIDs)
|
||||
delRanges = t.SliceToRanges(seqIDs)
|
||||
rSQL, rArgs := common.RangesToSql(delRanges)
|
||||
where = "m.topic=? AND m.seqid " + rSQL
|
||||
args = append([]any{topic}, rArgs...)
|
||||
if _, err = tx.Exec("UPDATE im_group_messages AS m SET m.updated_at=?,m.deleted_at=?,m.status='deleted',m.from_user=0,m.head=NULL,m.content=NULL,m.content_size=0 WHERE "+
|
||||
where, append([]any{t.TimeNow(), t.TimeNow()}, args...)...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
stmt, err := tx.Prepare("INSERT INTO im_dellog(topic,deletedfor,delid,low,hi) VALUES(?,?,?,?,?)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
forUser := common.DecodeUidString(toDel.DeletedFor)
|
||||
for _, rng := range delRanges {
|
||||
if rng.Hi == 0 {
|
||||
rng.Hi = rng.Low + 1
|
||||
}
|
||||
if _, err = stmt.Exec(topic, forUser, toDel.DelId, rng.Low, rng.Hi); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantTopicDelete(id t.TenantID, topic string, isChan, hard bool) error {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil || topic == "" {
|
||||
@@ -1138,7 +1245,12 @@ func (a *adapter) TenantTopicDelete(id t.TenantID, topic string, isChan, hard bo
|
||||
if _, err = tx.Exec(tx.Rebind(q), args...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = tenantMessageDeleteList(tx, id, topic, nil); err != nil {
|
||||
if t.GetTopicCat(topic) == t.TopicCatGrp {
|
||||
err = groupMessageDeleteList(tx, topic, nil)
|
||||
} else {
|
||||
err = tenantMessageDeleteList(tx, id, topic, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec("DELETE FROM im_topictags WHERE tenant_id=? AND topic=?", id, topic); err != nil {
|
||||
@@ -1652,6 +1764,42 @@ func (a *adapter) TenantMessageSave(id t.TenantID, msg *t.Message) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupMessageSave(id t.TenantID, msg *t.Message) error {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil || msg == nil || msg.TenantID != id || msg.Topic == "" || msg.SeqId <= 0 {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantGroupMessageSave(id, msg)
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
groupID, err := a.groupIDByTopic(ctx, msg.Topic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientMsgID := sql.NullString{String: msg.ClientMsgID, Valid: msg.ClientMsgID != ""}
|
||||
contentJSON := common.ToJSON(msg.Content)
|
||||
res, err := a.db.ExecContext(ctx,
|
||||
`INSERT INTO im_group_messages
|
||||
(topic,group_id,seqid,from_user,client_msg_id,msg_type,status,content,head,mentioned_user_ids,mention_all,
|
||||
file_count,content_size,version,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
msg.Topic, groupID, msg.SeqId, decodeUID(t.ParseUid(msg.From)), clientMsgID, messageHeadType(msg.Head), "normal",
|
||||
contentJSON, msg.Head, messageHeadJSON(msg.Head, "mentioned_user_ids"), messageHeadBool(msg.Head, "mention_all"),
|
||||
0, len(contentJSON), messageVersion(msg.Version), msg.CreatedAt, msg.UpdatedAt)
|
||||
if err == nil {
|
||||
lastID, _ := res.LastInsertId()
|
||||
msg.SetUid(t.Uid(lastID))
|
||||
}
|
||||
if isDupe(err) {
|
||||
return t.ErrDuplicate
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageGetByClientID(id t.TenantID, from t.Uid, clientMsgID string) (*t.Message, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
@@ -1687,7 +1835,40 @@ func (a *adapter) TenantMessageGetByClientID(id t.TenantID, from t.Uid, clientMs
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageGetOne(id t.TenantID, topic string, seq int) (*t.Message, error) {
|
||||
func (a *adapter) TenantGroupMessageGetByClientID(id t.TenantID, topic string, from t.Uid, clientMsgID string) (*t.Message, error) {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantGroupMessageGetByClientID(id, topic, from, clientMsgID)
|
||||
}
|
||||
if topic == "" || from.IsZero() || clientMsgID == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
row := struct {
|
||||
t.Message
|
||||
RecalledBy sql.NullInt64 `db:"recalled_by"`
|
||||
}{}
|
||||
err = a.db.QueryRowxContext(ctx,
|
||||
"SELECT id,created_at AS createdat,updated_at AS updatedat,deleted_at AS deletedat,0 AS delid,seqid,topic,from_user AS `from`,client_msg_id,version,recalled_at,recalled_by,head,content "+
|
||||
"FROM im_group_messages WHERE topic=? AND from_user=? AND client_msg_id=?",
|
||||
topic, decodeUID(from), clientMsgID).StructScan(&row)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg := normalizeGroupMessage(id, row.Message, row.RecalledBy)
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageGetOne(id t.TenantID, topic string, seq int64) (*t.Message, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
@@ -1711,7 +1892,29 @@ func (a *adapter) TenantMessageGetOne(id t.TenantID, topic string, seq int) (*t.
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageRecall(id t.TenantID, topic string, seq int, actor t.Uid, expectedVersion int, recalledAt time.Time) (*t.Message, error) {
|
||||
func (a *adapter) TenantGroupMessageGetOne(id t.TenantID, topic string, seq int64) (*t.Message, error) {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantGroupMessageGetOne(id, topic, seq)
|
||||
}
|
||||
if topic == "" || seq <= 0 {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
msg, err := tenantGroupMessageGetOne(a.db, ctx, id, topic, seq, "")
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageRecall(id t.TenantID, topic string, seq int64, actor t.Uid, expectedVersion int, recalledAt time.Time) (*t.Message, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
@@ -1763,7 +1966,57 @@ func (a *adapter) TenantMessageRecall(id t.TenantID, topic string, seq int, acto
|
||||
return a.TenantMessageGetOne(id, topic, seq)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageEdit(id t.TenantID, topic string, seq int, actor t.Uid, expectedVersion int, head t.KVMap, content any, reason string, editedAt time.Time) (*t.Message, error) {
|
||||
func (a *adapter) TenantGroupMessageRecall(id t.TenantID, topic string, seq int64, actor t.Uid, expectedVersion int, recalledAt time.Time) (*t.Message, error) {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantGroupMessageRecall(id, topic, seq, actor, expectedVersion, recalledAt)
|
||||
}
|
||||
if topic == "" || seq <= 0 || actor.IsZero() || expectedVersion <= 0 {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
tx, err := a.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
msg, err := tenantGroupMessageGetOne(tx, ctx, id, topic, seq, " FOR UPDATE")
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if msg.Version != expectedVersion || msg.RecalledAt != nil || msg.DeletedAt != nil {
|
||||
return nil, t.ErrDuplicate
|
||||
}
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE im_group_messages SET updated_at=?,version=version+1,status='recalled',recalled_at=?,recalled_by=?,head=NULL,content=NULL WHERE topic=? AND seqid=? AND version=?",
|
||||
recalledAt, recalledAt, decodeUID(actor), topic, seq, expectedVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = insertBusinessEventTx(tx, t.StableEventID("message", topic, seq, expectedVersion+1, "recalled"),
|
||||
"topic", topic, actor, "message.recalled", map[string]any{"topic": topic, "seq": seq, "version": expectedVersion + 1}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.TenantGroupMessageGetOne(id, topic, seq)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageEdit(id t.TenantID, topic string, seq int64, actor t.Uid, expectedVersion int, head t.KVMap, content any, reason string, editedAt time.Time) (*t.Message, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
@@ -1820,6 +2073,62 @@ func (a *adapter) TenantMessageEdit(id t.TenantID, topic string, seq int, actor
|
||||
return a.TenantMessageGetOne(id, topic, seq)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupMessageEdit(id t.TenantID, topic string, seq int64, actor t.Uid, expectedVersion int, head t.KVMap, content any, reason string, editedAt time.Time) (*t.Message, error) {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantGroupMessageEdit(id, topic, seq, actor, expectedVersion, head, content, reason, editedAt)
|
||||
}
|
||||
if topic == "" || seq <= 0 || actor.IsZero() || expectedVersion <= 0 {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
tx, err := a.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
msg, err := tenantGroupMessageGetOne(tx, ctx, id, topic, seq, " FOR UPDATE")
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if msg.Version != expectedVersion || msg.RecalledAt != nil || msg.DeletedAt != nil {
|
||||
return nil, t.ErrDuplicate
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"INSERT INTO im_message_revisions(createdat,updatedat,topic,seqid,version,actor,head,content,reason) VALUES(?,?,?,?,?,?,?,?,?)",
|
||||
editedAt, editedAt, topic, seq, msg.Version, decodeUID(actor), msg.Head, common.ToJSON(msg.Content), strings.TrimSpace(reason)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contentJSON := common.ToJSON(content)
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE im_group_messages SET updated_at=?,version=version+1,msg_type=?,head=?,content=?,content_size=? WHERE topic=? AND seqid=? AND version=?",
|
||||
editedAt, messageHeadType(head), head, contentJSON, len(contentJSON), topic, seq, expectedVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = insertBusinessEventTx(tx, t.StableEventID("message", topic, seq, expectedVersion+1, "edited"),
|
||||
"topic", topic, actor, "message.edited", map[string]any{"topic": topic, "seq": seq, "version": expectedVersion + 1}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.TenantGroupMessageGetOne(id, topic, seq)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageGetAll(id t.TenantID, topic string, uid t.Uid, opts *t.QueryOpt) ([]t.Message, error) {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil || topic == "" {
|
||||
@@ -1888,6 +2197,69 @@ func (a *adapter) TenantMessageGetAll(id t.TenantID, topic string, uid t.Uid, op
|
||||
return msgs, rows.Err()
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupMessageGetAll(id t.TenantID, topic string, uid t.Uid, opts *t.QueryOpt) ([]t.Message, error) {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil || topic == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantGroupMessageGetAll(id, topic, uid, opts)
|
||||
}
|
||||
limit := a.maxMessageResults
|
||||
args := []any{store.DecodeUid(uid), topic}
|
||||
seq := ""
|
||||
if opts != nil {
|
||||
seq = "AND m.seqid "
|
||||
if len(opts.IdRanges) > 0 {
|
||||
constr, addArgs := common.RangesToSql(opts.IdRanges)
|
||||
seq += constr
|
||||
args = append(args, addArgs...)
|
||||
} else {
|
||||
seq += "BETWEEN ? AND ?"
|
||||
if opts.Since > 0 {
|
||||
args = append(args, opts.Since)
|
||||
} else {
|
||||
args = append(args, 0)
|
||||
}
|
||||
if opts.Before > 1 {
|
||||
args = append(args, opts.Before-1)
|
||||
} else {
|
||||
args = append(args, 1<<31-1)
|
||||
}
|
||||
}
|
||||
if opts.Limit > 0 && opts.Limit < limit {
|
||||
limit = opts.Limit
|
||||
}
|
||||
}
|
||||
args = append(args, limit)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx,
|
||||
"SELECT m.id,m.created_at AS createdat,m.updated_at AS updatedat,m.deleted_at AS deletedat,0 AS delid,m.seqid,m.topic,m.from_user AS `from`,m.client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content "+
|
||||
"FROM im_group_messages AS m LEFT JOIN im_dellog AS d "+
|
||||
"ON d.topic=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? "+
|
||||
"WHERE m.status!='deleted' AND m.topic=? "+seq+" AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?",
|
||||
args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
msgs := make([]t.Message, 0, limit)
|
||||
for rows.Next() {
|
||||
row := struct {
|
||||
t.Message
|
||||
RecalledBy sql.NullInt64 `db:"recalled_by"`
|
||||
}{}
|
||||
if err = rows.StructScan(&row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msgs = append(msgs, normalizeGroupMessage(id, row.Message, row.RecalledBy))
|
||||
}
|
||||
return msgs, rows.Err()
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageSearch(id t.TenantID, topic string, uid t.Uid, queryText, messageType, cursor string, limit int) ([]t.Message, string, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
@@ -1951,7 +2323,7 @@ func (a *adapter) TenantMessageSearch(id t.TenantID, topic string, uid t.Uid, qu
|
||||
msg := finalizeTenantMessage(id, row.Message, row.RecalledBy)
|
||||
msg.Head = kvMapFromJSON(msg.Head)
|
||||
if len(msgs) == limit {
|
||||
nextCursor = strconv.Itoa(msg.SeqId)
|
||||
nextCursor = strconv.FormatInt(msg.SeqId, 10)
|
||||
continue
|
||||
}
|
||||
msgs = append(msgs, msg)
|
||||
@@ -2001,7 +2373,7 @@ func (a *adapter) TenantCallCreate(id t.TenantID, record *t.CallRecord) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantCallAccept(id t.TenantID, callID string, seq int, acceptedAt time.Time) error {
|
||||
func (a *adapter) TenantCallAccept(id t.TenantID, callID string, seq int64, acceptedAt time.Time) error {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
@@ -2026,7 +2398,7 @@ func (a *adapter) TenantCallAccept(id t.TenantID, callID string, seq int, accept
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantCallFinish(id t.TenantID, callID string, seq int, status string, durationMS int64, finishedAt time.Time) error {
|
||||
func (a *adapter) TenantCallFinish(id t.TenantID, callID string, seq int64, status string, durationMS int64, finishedAt time.Time) error {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
@@ -2102,7 +2474,7 @@ func (a *adapter) TenantCallList(id t.TenantID, uid t.Uid, topic, cursor string,
|
||||
ID int64
|
||||
CallID string `db:"call_id"`
|
||||
Topic string
|
||||
SeqID int `db:"seq_id"`
|
||||
SeqID int64 `db:"seq_id"`
|
||||
CallerID int64 `db:"caller_id"`
|
||||
CalleeID int64 `db:"callee_id"`
|
||||
CallType string `db:"call_type"`
|
||||
@@ -4509,7 +4881,7 @@ func (a *adapter) abuseReportByIdempotency(ctx context.Context, uid t.Uid, idemp
|
||||
TargetType string `db:"target_type"`
|
||||
TargetUserID int64 `db:"target_user_id"`
|
||||
TargetTopic string `db:"target_topic"`
|
||||
TargetSeq int `db:"target_seq"`
|
||||
TargetSeq int64 `db:"target_seq"`
|
||||
ReasonCode string `db:"reason_code"`
|
||||
Description string `db:"description"`
|
||||
EvidenceFileIDs []byte `db:"evidence_file_ids"`
|
||||
@@ -4699,7 +5071,7 @@ func isAllowedWorkbenchRoute(route string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func tenantMessageGetOne(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string, seq int, suffix string) (*t.Message, error) {
|
||||
func tenantMessageGetOne(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string, seq int64, suffix string) (*t.Message, error) {
|
||||
row := struct {
|
||||
t.Message
|
||||
RecalledBy sql.NullInt64 `db:"recalled_by"`
|
||||
@@ -4721,6 +5093,31 @@ func tenantMessageGetOne(q sqlx.QueryerContext, ctx context.Context, tenantID t.
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
func (a *adapter) groupIDByTopic(ctx context.Context, topic string) (int64, error) {
|
||||
var groupID int64
|
||||
err := a.db.QueryRowContext(ctx, "SELECT id FROM im_group_settings WHERE topic=? LIMIT 1", topic).Scan(&groupID)
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, t.ErrNotFound
|
||||
}
|
||||
return groupID, err
|
||||
}
|
||||
|
||||
func tenantGroupMessageGetOne(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string, seq int64, suffix string) (*t.Message, error) {
|
||||
row := struct {
|
||||
t.Message
|
||||
RecalledBy sql.NullInt64 `db:"recalled_by"`
|
||||
}{}
|
||||
err := sqlx.GetContext(ctx, q, &row,
|
||||
"SELECT id,created_at AS createdat,updated_at AS updatedat,deleted_at AS deletedat,0 AS delid,seqid,topic,from_user AS `from`,client_msg_id,version,recalled_at,recalled_by,head,content "+
|
||||
"FROM im_group_messages WHERE topic=? AND seqid=?"+suffix,
|
||||
topic, seq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg := normalizeGroupMessage(tenantID, row.Message, row.RecalledBy)
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
func messageVersion(version int) int {
|
||||
if version <= 0 {
|
||||
return 1
|
||||
@@ -5174,6 +5571,33 @@ func (a *adapter) TenantMessageDeleteList(id t.TenantID, topic string, del *t.De
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupMessageDeleteList(id t.TenantID, topic string, del *t.DelMessage) error {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil || topic == "" || (del != nil && del.TenantID != id) {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantGroupMessageDeleteList(id, topic, del)
|
||||
}
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
tx, err := a.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
if err = groupMessageDeleteList(tx, topic, del); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageGetDeleted(id t.TenantID, topic string, uid t.Uid, opts *t.QueryOpt) ([]t.DelMessage, error) {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil || topic == "" {
|
||||
|
||||
@@ -1201,7 +1201,7 @@ func TestTopicUpdateOnMessage(t *testing.T) {
|
||||
}
|
||||
var got struct {
|
||||
TouchedAt time.Time
|
||||
SeqId int
|
||||
SeqId int64
|
||||
}
|
||||
err = db.QueryRow("SELECT touchedat, seqid FROM im_topics WHERE name=?", testData.Topics[2].Id).
|
||||
Scan(&got.TouchedAt, &got.SeqId)
|
||||
|
||||
@@ -830,7 +830,7 @@ func TestTopicUpdateOnMessage(t *testing.T) {
|
||||
}
|
||||
var got struct {
|
||||
TouchedAt time.Time
|
||||
SeqId int
|
||||
SeqId int64
|
||||
}
|
||||
err = db.QueryRow(ctx, "SELECT touchedat, seqid FROM im_topics WHERE name=$1", testData.Topics[2].Id).
|
||||
Scan(&got.TouchedAt, &got.SeqId)
|
||||
|
||||
@@ -844,7 +844,7 @@ func TestTopicUpdateOnMessage(t *testing.T) {
|
||||
|
||||
var got struct {
|
||||
TouchedAt time.Time
|
||||
SeqId int
|
||||
SeqId int64
|
||||
}
|
||||
if err = cursor.One(&got); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
+90
-15
@@ -60,6 +60,7 @@ const (
|
||||
imVerificationExpiresInSeconds = 300
|
||||
imVerificationResendAfterSeconds = 60
|
||||
imVerificationMaxAttempts = 5
|
||||
imVerificationCreateRateWindow = time.Hour
|
||||
imActionTokenExpiresInSeconds = 300
|
||||
imIndependentPasswordMaxAttempts = 5
|
||||
imIndependentPasswordLockSeconds = 300
|
||||
@@ -380,7 +381,7 @@ type imMessageRevisionRequest struct {
|
||||
type imFavoritesBatchCreateRequest struct {
|
||||
Messages []struct {
|
||||
Topic string `json:"topic"`
|
||||
Seq int `json:"seq,string"`
|
||||
Seq int64 `json:"seq,string"`
|
||||
} `json:"messages"`
|
||||
}
|
||||
|
||||
@@ -479,7 +480,7 @@ type imGroupFileCreateRequest struct {
|
||||
FileType string `json:"file_type,omitempty"`
|
||||
FileSize int64 `json:"file_size,omitempty"`
|
||||
SourceTopic string `json:"source_topic,omitempty"`
|
||||
SourceSeq int `json:"source_seq,omitempty"`
|
||||
SourceSeq int64 `json:"source_seq,omitempty"`
|
||||
}
|
||||
|
||||
type imGroupAnnouncementPutRequest struct {
|
||||
@@ -874,6 +875,18 @@ func serveIMVerificationCodes(wrt http.ResponseWriter, req *http.Request) {
|
||||
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
phoneHash := hashIMSecret(tenant.ID, scene, phone, "phone")
|
||||
if allowed, _, ttl, err := store.AllowRate(tenant.ID, "verification:create:"+scene+":"+phoneHash, imVerificationMaxAttempts, imVerificationCreateRateWindow); err != nil {
|
||||
logs.Warn.Println("im verification create: redis rate limit failed", tenant.Code, scene, err)
|
||||
} else if !allowed {
|
||||
retryAfter := int(ttl.Seconds())
|
||||
if retryAfter < 1 {
|
||||
retryAfter = int(imVerificationCreateRateWindow.Seconds())
|
||||
}
|
||||
writeIMError(wrt, req, http.StatusTooManyRequests, 30020, imReasonVerificationRate, "verification rate limited", map[string]any{
|
||||
"retry_after_seconds": retryAfter,
|
||||
})
|
||||
return
|
||||
}
|
||||
active, err := store.Verifications.GetActive(tenant.ID, scene, phoneHash, now)
|
||||
if err != nil && !errors.Is(err, types.ErrNotFound) {
|
||||
logs.Warn.Println("im verification create: active lookup failed", tenant.Code, scene, err)
|
||||
@@ -3342,7 +3355,7 @@ func serveIMMessageContext(wrt http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
before := parseIMLimit(req.URL.Query().Get("before"), 20, 100)
|
||||
after := parseIMLimit(req.URL.Query().Get("after"), 20, 100)
|
||||
msgs, err := store.Messages.GetAll(principal.TenantID, topic, principal.Uid, &types.QueryOpt{Since: seq - before, Before: seq + after + 1, Limit: before + after + 1})
|
||||
msgs, err := store.Messages.GetAll(principal.TenantID, topic, principal.Uid, &types.QueryOpt{Since: seq - int64(before), Before: seq + int64(after) + 1, Limit: before + after + 1})
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
@@ -3599,7 +3612,7 @@ func serveIMConversationContext(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
peer := peerFromP2PTopic(topic, principal.Uid)
|
||||
topic, peer := normalizeP2PConversationTopic(topic, principal.Uid)
|
||||
if topic == "" || peer.IsZero() {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||
return
|
||||
@@ -3617,6 +3630,7 @@ func serveIMConversationSettings(wrt http.ResponseWriter, req *http.Request, pri
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||
return
|
||||
}
|
||||
topic = normalizeConversationSettingsTopic(topic, principal.Uid)
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
settings, err := store.Conversations.GetSettings(principal.TenantID, principal.Uid, topic)
|
||||
@@ -4043,6 +4057,11 @@ func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic strin
|
||||
roleFilter := strings.TrimSpace(req.URL.Query().Get("role"))
|
||||
eligibleRole := strings.TrimSpace(req.URL.Query().Get("eligible_role"))
|
||||
muteEligible := parseIMBool(req.URL.Query().Get("mute_eligible"))
|
||||
cacheKey := groupMemberPageCacheKey(topic, principal.Uid, selfAccess, query, roleFilter, eligibleRole, limit, cursor, mentionableOnly, muteEligible)
|
||||
if cached, ok := store.GetCachedGroupMemberPage(principal.TenantID, cacheKey); ok {
|
||||
writeIMSuccess(wrt, req, cached)
|
||||
return
|
||||
}
|
||||
members, err := store.Topics.GetUsers(principal.TenantID, topic, &types.QueryOpt{Limit: 1000})
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
@@ -4093,12 +4112,14 @@ func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic strin
|
||||
permissions := map[string]any{
|
||||
"can_mention_all": selfAccess.IsAdmin(),
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
payload := map[string]any{
|
||||
"topic": topic,
|
||||
"items": items,
|
||||
"next_cursor": nextCursor,
|
||||
"permissions": permissions,
|
||||
})
|
||||
}
|
||||
store.CacheGroupMemberPage(principal.TenantID, cacheKey, payload)
|
||||
writeIMSuccess(wrt, req, payload)
|
||||
}
|
||||
|
||||
func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string) {
|
||||
@@ -5767,6 +5788,18 @@ func currentUserPhone(tenantID types.TenantID, uid types.Uid) (string, error) {
|
||||
func createSensitiveChallenge(wrt http.ResponseWriter, req *http.Request, principal *imPrincipal, action, phone string) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
phoneHash := hashIMSecret(principal.TenantID, "sensitive_action", phone, "phone")
|
||||
if allowed, _, ttl, err := store.AllowRate(principal.TenantID, "verification:create:sensitive_action:"+phoneHash, imVerificationMaxAttempts, imVerificationCreateRateWindow); err != nil {
|
||||
logs.Warn.Println("im sensitive verification create: redis rate limit failed", principal.TenantID, err)
|
||||
} else if !allowed {
|
||||
retryAfter := int(ttl.Seconds())
|
||||
if retryAfter < 1 {
|
||||
retryAfter = int(imVerificationCreateRateWindow.Seconds())
|
||||
}
|
||||
writeIMError(wrt, req, http.StatusTooManyRequests, 30020, imReasonVerificationRate, "verification rate limited", map[string]any{
|
||||
"retry_after_seconds": retryAfter,
|
||||
})
|
||||
return
|
||||
}
|
||||
active, err := store.Verifications.GetActive(principal.TenantID, "sensitive_action", phoneHash, now)
|
||||
if err != nil && !errors.Is(err, types.ErrNotFound) {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
@@ -6712,7 +6745,23 @@ func groupMemberMatches(item map[string]any, query string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func authorizeIMMessageMutation(principal *imPrincipal, topic string, seq int, allowAdmin bool) (*types.Message, bool, error) {
|
||||
func groupMemberPageCacheKey(topic string, uid types.Uid, access types.AccessMode, query, roleFilter, eligibleRole string,
|
||||
limit, cursor int, mentionableOnly, muteEligible bool) string {
|
||||
return strings.Join([]string{
|
||||
topic,
|
||||
uid.UserId(),
|
||||
access.String(),
|
||||
strings.ToLower(strings.TrimSpace(query)),
|
||||
strings.TrimSpace(roleFilter),
|
||||
strings.TrimSpace(eligibleRole),
|
||||
strconv.Itoa(limit),
|
||||
strconv.Itoa(cursor),
|
||||
strconv.FormatBool(mentionableOnly),
|
||||
strconv.FormatBool(muteEligible),
|
||||
}, "|")
|
||||
}
|
||||
|
||||
func authorizeIMMessageMutation(principal *imPrincipal, topic string, seq int64, allowAdmin bool) (*types.Message, bool, error) {
|
||||
if principal == nil {
|
||||
return nil, false, types.ErrPermissionDenied
|
||||
}
|
||||
@@ -8385,7 +8434,7 @@ func cleanTenantCodeHint(code string) string {
|
||||
return code
|
||||
}
|
||||
|
||||
func normalizeIMAbuseTarget(tenantID types.TenantID, reporter types.Uid, target types.AbuseTarget) (string, types.Uid, string, int, map[string]any, error) {
|
||||
func normalizeIMAbuseTarget(tenantID types.TenantID, reporter types.Uid, target types.AbuseTarget) (string, types.Uid, string, int64, map[string]any, error) {
|
||||
targetType := strings.ToLower(strings.TrimSpace(target.Type))
|
||||
snapshot := map[string]any{"type": targetType}
|
||||
switch targetType {
|
||||
@@ -8413,7 +8462,7 @@ func normalizeIMAbuseTarget(tenantID types.TenantID, reporter types.Uid, target
|
||||
return targetType, 0, topic, 0, snapshot, nil
|
||||
case "message":
|
||||
topic := strings.TrimSpace(target.Topic)
|
||||
seq, err := strconv.Atoi(strings.TrimSpace(target.Seq))
|
||||
seq, err := strconv.ParseInt(strings.TrimSpace(target.Seq), 10, 64)
|
||||
if topic == "" || seq <= 0 || err != nil {
|
||||
return "", 0, "", 0, nil, types.ErrMalformed
|
||||
}
|
||||
@@ -8476,7 +8525,7 @@ func groupInvitationResourcePath(path string) (string, string, bool) {
|
||||
return strings.TrimSpace(invitationID), strings.Trim(tail, "/"), true
|
||||
}
|
||||
|
||||
func messageReceiptsPath(path string) (string, int, bool) {
|
||||
func messageReceiptsPath(path string) (string, int64, bool) {
|
||||
const marker = "/v1/im/topics/"
|
||||
idx := strings.Index(path, marker)
|
||||
if idx < 0 {
|
||||
@@ -8491,7 +8540,7 @@ func messageReceiptsPath(path string) (string, int, bool) {
|
||||
if !ok || suffix != "receipts" {
|
||||
return "", 0, false
|
||||
}
|
||||
seq, err := strconv.Atoi(seqValue)
|
||||
seq, err := strconv.ParseInt(seqValue, 10, 64)
|
||||
if err != nil {
|
||||
return topic, 0, true
|
||||
}
|
||||
@@ -8531,7 +8580,7 @@ func topicFromPathWithSuffix(path, prefix, suffix string) string {
|
||||
return strings.Trim(rest, "/")
|
||||
}
|
||||
|
||||
func messageContextPath(path string) (string, int, bool) {
|
||||
func messageContextPath(path string) (string, int64, bool) {
|
||||
const prefix = "/v1/im/topics/"
|
||||
idx := strings.Index(path, prefix)
|
||||
if idx < 0 {
|
||||
@@ -8542,14 +8591,14 @@ func messageContextPath(path string) (string, int, bool) {
|
||||
if len(parts) != 4 || parts[1] != "messages" || parts[3] != "context" {
|
||||
return "", 0, false
|
||||
}
|
||||
seq, err := strconv.Atoi(parts[2])
|
||||
seq, err := strconv.ParseInt(parts[2], 10, 64)
|
||||
if err != nil || seq <= 0 {
|
||||
return "", 0, false
|
||||
}
|
||||
return parts[0], seq, true
|
||||
}
|
||||
|
||||
func messageActionPath(path, action string) (string, int, bool) {
|
||||
func messageActionPath(path, action string) (string, int64, bool) {
|
||||
const marker = "/v1/im/topics/"
|
||||
idx := strings.Index(path, marker)
|
||||
if idx < 0 {
|
||||
@@ -8564,7 +8613,7 @@ func messageActionPath(path, action string) (string, int, bool) {
|
||||
if !ok || suffix != action {
|
||||
return "", 0, false
|
||||
}
|
||||
seq, err := strconv.Atoi(seqValue)
|
||||
seq, err := strconv.ParseInt(seqValue, 10, 64)
|
||||
if err != nil {
|
||||
return topic, 0, true
|
||||
}
|
||||
@@ -8756,3 +8805,29 @@ func peerFromP2PTopic(topic string, uid types.Uid) types.Uid {
|
||||
}
|
||||
return types.ZeroUid
|
||||
}
|
||||
|
||||
func normalizeP2PConversationTopic(topic string, uid types.Uid) (string, types.Uid) {
|
||||
topic = strings.TrimSpace(topic)
|
||||
if len(topic) < 3 || uid.IsZero() {
|
||||
return "", types.ZeroUid
|
||||
}
|
||||
if types.GetTopicCat(topic) == types.TopicCatMe {
|
||||
peer := types.ParseUserId(topic)
|
||||
if peer.IsZero() || peer == uid {
|
||||
return "", types.ZeroUid
|
||||
}
|
||||
return uid.P2PName(peer), peer
|
||||
}
|
||||
peer := peerFromP2PTopic(topic, uid)
|
||||
if peer.IsZero() {
|
||||
return "", types.ZeroUid
|
||||
}
|
||||
return topic, peer
|
||||
}
|
||||
|
||||
func normalizeConversationSettingsTopic(topic string, uid types.Uid) string {
|
||||
if normalized, peer := normalizeP2PConversationTopic(topic, uid); !peer.IsZero() {
|
||||
return normalized
|
||||
}
|
||||
return strings.TrimSpace(topic)
|
||||
}
|
||||
|
||||
+61
-7
@@ -861,11 +861,11 @@ func (s *testCallsStore) Create(_ types.TenantID, _ *types.CallRecord) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testCallsStore) Accept(_ types.TenantID, _ string, _ int, _ time.Time) error {
|
||||
func (s *testCallsStore) Accept(_ types.TenantID, _ string, _ int64, _ time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testCallsStore) Finish(_ types.TenantID, _ string, _ int, _ string, _ int64, _ time.Time) error {
|
||||
func (s *testCallsStore) Finish(_ types.TenantID, _ string, _ int64, _ string, _ int64, _ time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -885,6 +885,7 @@ func (s *testConversationsStore) EnsureP2P(_ types.TenantID, uid, peer types.Uid
|
||||
}
|
||||
|
||||
func (s *testConversationsStore) GetSettings(_ types.TenantID, _ types.Uid, topic string) (*types.ConversationSettings, error) {
|
||||
s.topic = topic
|
||||
if s.settings == nil {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
s.settings = &types.ConversationSettings{Topic: topic, Pinned: false, Muted: false, MessageRetention: "default", Version: 1, CreatedAt: now, UpdatedAt: now}
|
||||
@@ -3770,7 +3771,7 @@ func TestServeIMFavoriteBatchCreateCountsExisting(t *testing.T) {
|
||||
store.Messages = oldMessages
|
||||
})
|
||||
|
||||
messages.EXPECT().GetOne(types.TenantID(7), "grpAlpha", 3).Return(&types.Message{
|
||||
messages.EXPECT().GetOne(types.TenantID(7), "grpAlpha", int64(3)).Return(&types.Message{
|
||||
TenantID: 7,
|
||||
Topic: "grpAlpha",
|
||||
SeqId: 3,
|
||||
@@ -4307,7 +4308,7 @@ func TestServeIMMessageRecall(t *testing.T) {
|
||||
ModeWant: types.ModeCFull,
|
||||
ModeGiven: types.ModeCFull,
|
||||
}, nil)
|
||||
messages.EXPECT().GetOne(types.TenantID(7), "grpRecall", 9).Return(&types.Message{
|
||||
messages.EXPECT().GetOne(types.TenantID(7), "grpRecall", int64(9)).Return(&types.Message{
|
||||
TenantID: 7,
|
||||
SeqId: 9,
|
||||
Topic: "grpRecall",
|
||||
@@ -4315,7 +4316,7 @@ func TestServeIMMessageRecall(t *testing.T) {
|
||||
Version: 1,
|
||||
ObjHeader: types.ObjHeader{CreatedAt: time.Now().Add(-time.Second)},
|
||||
}, nil)
|
||||
messages.EXPECT().Recall(types.TenantID(7), "grpRecall", 9, types.Uid(42), 1, gomock.Any()).Return(&types.Message{
|
||||
messages.EXPECT().Recall(types.TenantID(7), "grpRecall", int64(9), types.Uid(42), 1, gomock.Any()).Return(&types.Message{
|
||||
TenantID: 7,
|
||||
SeqId: 9,
|
||||
Topic: "grpRecall",
|
||||
@@ -4363,7 +4364,7 @@ func TestServeIMMessageRevision(t *testing.T) {
|
||||
ModeWant: types.ModeCFull,
|
||||
ModeGiven: types.ModeCFull,
|
||||
}, nil)
|
||||
messages.EXPECT().GetOne(types.TenantID(7), "grpEdit", 3).Return(&types.Message{
|
||||
messages.EXPECT().GetOne(types.TenantID(7), "grpEdit", int64(3)).Return(&types.Message{
|
||||
TenantID: 7,
|
||||
SeqId: 3,
|
||||
Topic: "grpEdit",
|
||||
@@ -4373,7 +4374,7 @@ func TestServeIMMessageRevision(t *testing.T) {
|
||||
Content: "old",
|
||||
ObjHeader: types.ObjHeader{CreatedAt: time.Now().Add(-time.Second)},
|
||||
}, nil)
|
||||
messages.EXPECT().Edit(types.TenantID(7), "grpEdit", 3, types.Uid(42), 1, gomock.Any(), "new", "", gomock.Any()).Return(&types.Message{
|
||||
messages.EXPECT().Edit(types.TenantID(7), "grpEdit", int64(3), types.Uid(42), 1, gomock.Any(), "new", "", gomock.Any()).Return(&types.Message{
|
||||
TenantID: 7,
|
||||
SeqId: 3,
|
||||
Topic: "grpEdit",
|
||||
@@ -4431,6 +4432,31 @@ func TestServeIMConversationContext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMConversationContextAcceptsUserTopic(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 1)
|
||||
oldRelations := store.Relations
|
||||
peer := types.Uid(2)
|
||||
topic := types.Uid(1).P2PName(peer)
|
||||
store.Relations = &testRelationsStore{}
|
||||
defer func() { store.Relations = oldRelations }()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/conversations/"+peer.UserId()+"/context", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMConversationContext(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["topic"] != topic || data["peer_user_id"] != peer.UserId() {
|
||||
t.Fatalf("unexpected context response: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMP2PConversation(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -4487,6 +4513,34 @@ func TestServeIMConversationSettings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMConversationSettingsNormalizesUserTopic(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 1)
|
||||
oldConversations := store.Conversations
|
||||
peer := types.Uid(2)
|
||||
topic := types.Uid(1).P2PName(peer)
|
||||
conversations := &testConversationsStore{}
|
||||
store.Conversations = conversations
|
||||
defer func() { store.Conversations = oldConversations }()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/conversations/"+peer.UserId()+"/settings", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMConversationContext(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
if conversations.topic != topic {
|
||||
t.Fatalf("settings topic: got %q, want %q", conversations.topic, topic)
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["topic"] != topic {
|
||||
t.Fatalf("unexpected settings response: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMTopicDraftPutAndGet(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
@@ -11,6 +11,7 @@ package main
|
||||
//go:generate protoc --go_out=../pbx --go_opt=paths=source_relative --go-grpc_out=../pbx --go-grpc_opt=paths=source_relative ../pbx/model.proto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"net/http"
|
||||
@@ -676,6 +677,7 @@ func main() {
|
||||
globals.sessionStore = NewSessionStore(idleSessionTimeout + 15*time.Second)
|
||||
// The hub (the main message router)
|
||||
globals.hub = newHub()
|
||||
startRedisGroupFanout(context.Background())
|
||||
stopExternalBusinessEvents := startIMExternalBusinessEventPoller()
|
||||
defer stopExternalBusinessEvents()
|
||||
|
||||
|
||||
+31
-31
@@ -41,7 +41,7 @@ func pbServDataSerialize(data *MsgServerData) *pbx.ServerMsg_Data {
|
||||
FromUserId: data.From,
|
||||
Timestamp: timeToInt64(&data.Timestamp),
|
||||
DeletedAt: timeToInt64(data.DeletedAt),
|
||||
SeqId: int32(data.SeqId),
|
||||
SeqId: data.SeqId,
|
||||
Head: interfaceMapToByteMap(data.Head),
|
||||
Content: interfaceToBytes(data.Content),
|
||||
},
|
||||
@@ -86,7 +86,7 @@ func pbServPresSerialize(pres *MsgServerPres) *pbx.ServerMsg_Pres {
|
||||
Src: pres.Src,
|
||||
What: what,
|
||||
UserAgent: pres.UserAgent,
|
||||
SeqId: int32(pres.SeqId),
|
||||
SeqId: pres.SeqId,
|
||||
DelId: int32(pres.DelId),
|
||||
DelSeq: pbDelQuerySerialize(pres.DelSeq),
|
||||
TargetUserId: pres.AcsTarget,
|
||||
@@ -103,7 +103,7 @@ func pbServInfoSerialize(info *MsgServerInfo) *pbx.ServerMsg_Info {
|
||||
FromUserId: info.From,
|
||||
Src: info.Src,
|
||||
What: pbInfoNoteWhatSerialize(info.What),
|
||||
SeqId: int32(info.SeqId),
|
||||
SeqId: info.SeqId,
|
||||
Event: pbCallEventSerialize(info.Event),
|
||||
Payload: info.Payload,
|
||||
},
|
||||
@@ -165,7 +165,7 @@ func pbServDeserialize(pkt *pbx.ServerMsg) *ServerComMessage {
|
||||
From: data.GetFromUserId(),
|
||||
Timestamp: *tsptr,
|
||||
DeletedAt: int64ToTime(data.GetDeletedAt()),
|
||||
SeqId: int(data.GetSeqId()),
|
||||
SeqId: data.GetSeqId(),
|
||||
Head: byteMapToInterfaceMap(data.GetHead()),
|
||||
Content: data.GetContent(),
|
||||
}
|
||||
@@ -204,7 +204,7 @@ func pbServDeserialize(pkt *pbx.ServerMsg) *ServerComMessage {
|
||||
Src: pres.GetSrc(),
|
||||
What: what,
|
||||
UserAgent: pres.GetUserAgent(),
|
||||
SeqId: int(pres.GetSeqId()),
|
||||
SeqId: pres.GetSeqId(),
|
||||
DelId: int(pres.GetDelId()),
|
||||
DelSeq: pbDelQueryDeserialize(pres.GetDelSeq()),
|
||||
AcsTarget: pres.GetTargetUserId(),
|
||||
@@ -217,7 +217,7 @@ func pbServDeserialize(pkt *pbx.ServerMsg) *ServerComMessage {
|
||||
Src: info.GetSrc(),
|
||||
From: info.GetFromUserId(),
|
||||
What: pbInfoNoteWhatDeserialize(info.GetWhat()),
|
||||
SeqId: int(info.GetSeqId()),
|
||||
SeqId: info.GetSeqId(),
|
||||
Event: pbCallEventDeserialize(info.GetEvent()),
|
||||
Payload: info.GetPayload(),
|
||||
}
|
||||
@@ -365,7 +365,7 @@ func pbCliSerialize(msg *ClientComMessage) *pbx.ClientMsg {
|
||||
Note: &pbx.ClientNote{
|
||||
Topic: msg.Note.Topic,
|
||||
What: pbInfoNoteWhatSerialize(msg.Note.What),
|
||||
SeqId: int32(msg.Note.SeqId),
|
||||
SeqId: msg.Note.SeqId,
|
||||
Unread: int32(msg.Note.Unread),
|
||||
Event: pbCallEventSerialize(msg.Note.Event),
|
||||
Payload: msg.Note.Payload,
|
||||
@@ -485,7 +485,7 @@ func pbCliDeserialize(pkt *pbx.ClientMsg) *ClientComMessage {
|
||||
} else if note := pkt.GetNote(); note != nil {
|
||||
msg.Note = &MsgClientNote{
|
||||
Topic: note.GetTopic(),
|
||||
SeqId: int(note.GetSeqId()),
|
||||
SeqId: note.GetSeqId(),
|
||||
What: pbInfoNoteWhatDeserialize(note.GetWhat()),
|
||||
Unread: int(note.GetUnread()),
|
||||
Event: pbCallEventDeserialize(note.GetEvent()),
|
||||
@@ -585,15 +585,15 @@ func pbGetQuerySerialize(in *MsgGetQuery) *pbx.GetQuery {
|
||||
}
|
||||
if in.Data != nil {
|
||||
out.Data = &pbx.GetOpts{
|
||||
BeforeId: int32(in.Data.BeforeId),
|
||||
SinceId: int32(in.Data.SinceId),
|
||||
BeforeId: in.Data.BeforeId,
|
||||
SinceId: in.Data.SinceId,
|
||||
Limit: int32(in.Data.Limit),
|
||||
}
|
||||
|
||||
if len(in.Data.IdRanges) > 0 {
|
||||
out.Data.Ranges = make([]*pbx.SeqRange, len(in.Data.IdRanges))
|
||||
for i, dq := range in.Data.IdRanges {
|
||||
out.Data.Ranges[i] = &pbx.SeqRange{Low: int32(dq.LowId), Hi: int32(dq.HiId)}
|
||||
out.Data.Ranges[i] = &pbx.SeqRange{Low: dq.LowId, Hi: dq.HiId}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -623,16 +623,16 @@ func pbGetQueryDeserialize(in *pbx.GetQuery) *MsgGetQuery {
|
||||
}
|
||||
if data := in.GetData(); data != nil {
|
||||
msg.Data = &MsgGetOpts{
|
||||
BeforeId: int(data.GetBeforeId()),
|
||||
SinceId: int(data.GetSinceId()),
|
||||
BeforeId: data.GetBeforeId(),
|
||||
SinceId: data.GetSinceId(),
|
||||
Limit: int(data.GetLimit()),
|
||||
}
|
||||
|
||||
if ranges := data.GetRanges(); len(ranges) > 0 {
|
||||
msg.Data.IdRanges = make([]MsgRange, len(ranges))
|
||||
for i, sr := range ranges {
|
||||
msg.Data.IdRanges[i].LowId = int(sr.GetLow())
|
||||
msg.Data.IdRanges[i].HiId = int(sr.GetHi())
|
||||
msg.Data.IdRanges[i].LowId = sr.GetLow()
|
||||
msg.Data.IdRanges[i].HiId = sr.GetHi()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -890,9 +890,9 @@ func pbTopicDescSerialize(desc *MsgTopicDesc) *pbx.TopicDesc {
|
||||
IsChan: desc.IsChan,
|
||||
Defacs: pbDefaultAcsSerialize(desc.DefaultAcs),
|
||||
Acs: pbAccessModeSerialize(desc.Acs),
|
||||
SeqId: int32(desc.SeqId),
|
||||
ReadId: int32(desc.ReadSeqId),
|
||||
RecvId: int32(desc.RecvSeqId),
|
||||
SeqId: desc.SeqId,
|
||||
ReadId: desc.ReadSeqId,
|
||||
RecvId: desc.RecvSeqId,
|
||||
DelId: int32(desc.DelId),
|
||||
Public: interfaceToBytes(desc.Public),
|
||||
Trusted: interfaceToBytes(desc.Trusted),
|
||||
@@ -918,9 +918,9 @@ func pbTopicDescDeserialize(desc *pbx.TopicDesc) *MsgTopicDesc {
|
||||
IsChan: desc.GetIsChan(),
|
||||
DefaultAcs: pbDefaultAcsDeserialize(desc.GetDefacs()),
|
||||
Acs: pbAccessModeDeserialize(desc.GetAcs()),
|
||||
SeqId: int(desc.SeqId),
|
||||
ReadSeqId: int(desc.ReadId),
|
||||
RecvSeqId: int(desc.RecvId),
|
||||
SeqId: desc.SeqId,
|
||||
ReadSeqId: desc.ReadId,
|
||||
RecvSeqId: desc.RecvId,
|
||||
DelId: int(desc.DelId),
|
||||
Public: bytesToInterface(desc.Public),
|
||||
Trusted: bytesToInterface(desc.Trusted),
|
||||
@@ -947,7 +947,7 @@ func pbTopicSerializeToDesc(topic *Topic) *pbx.TopicDesc {
|
||||
Auth: topic.accessAuth.String(),
|
||||
Anon: topic.accessAnon.String(),
|
||||
},
|
||||
SeqId: int32(topic.lastID),
|
||||
SeqId: topic.lastID,
|
||||
DelId: int32(topic.delID),
|
||||
Public: interfaceToBytes(topic.public),
|
||||
Trusted: interfaceToBytes(topic.trusted),
|
||||
@@ -972,15 +972,15 @@ func pbTopicSubSerialize(sub *MsgTopicSub) *pbx.TopicSub {
|
||||
DeletedAt: timeToInt64(sub.DeletedAt),
|
||||
Online: sub.Online,
|
||||
Acs: pbAccessModeSerialize(&sub.Acs),
|
||||
ReadId: int32(sub.ReadSeqId),
|
||||
RecvId: int32(sub.RecvSeqId),
|
||||
ReadId: sub.ReadSeqId,
|
||||
RecvId: sub.RecvSeqId,
|
||||
Public: interfaceToBytes(sub.Public),
|
||||
Trusted: interfaceToBytes(sub.Trusted),
|
||||
Private: interfaceToBytes(sub.Private),
|
||||
UserId: sub.User,
|
||||
Topic: sub.Topic,
|
||||
TouchedAt: timeToInt64(sub.TouchedAt),
|
||||
SeqId: int32(sub.SeqId),
|
||||
SeqId: sub.SeqId,
|
||||
DelId: int32(sub.DelId),
|
||||
}
|
||||
if sub.LastSeen != nil {
|
||||
@@ -1001,15 +1001,15 @@ func pbTopicSubSliceDeserialize(subs []*pbx.TopicSub) []MsgTopicSub {
|
||||
UpdatedAt: int64ToTime(subs[i].GetUpdatedAt()),
|
||||
DeletedAt: int64ToTime(subs[i].GetDeletedAt()),
|
||||
Online: subs[i].GetOnline(),
|
||||
ReadSeqId: int(subs[i].GetReadId()),
|
||||
RecvSeqId: int(subs[i].GetRecvId()),
|
||||
ReadSeqId: subs[i].GetReadId(),
|
||||
RecvSeqId: subs[i].GetRecvId(),
|
||||
Public: bytesToInterface(subs[i].GetPublic()),
|
||||
Trusted: bytesToInterface(subs[i].GetTrusted()),
|
||||
Private: bytesToInterface(subs[i].GetPrivate()),
|
||||
User: subs[i].GetUserId(),
|
||||
Topic: subs[i].GetTopic(),
|
||||
TouchedAt: int64ToTime(subs[i].GetTouchedAt()),
|
||||
SeqId: int(subs[i].GetSeqId()),
|
||||
SeqId: subs[i].GetSeqId(),
|
||||
DelId: int(subs[i].GetDelId()),
|
||||
}
|
||||
if acs := subs[i].GetAcs(); acs != nil {
|
||||
@@ -1063,7 +1063,7 @@ func pbDelQuerySerialize(in []MsgRange) []*pbx.SeqRange {
|
||||
|
||||
out := make([]*pbx.SeqRange, len(in))
|
||||
for i, dq := range in {
|
||||
out[i] = &pbx.SeqRange{Low: int32(dq.LowId), Hi: int32(dq.HiId)}
|
||||
out[i] = &pbx.SeqRange{Low: dq.LowId, Hi: dq.HiId}
|
||||
}
|
||||
|
||||
return out
|
||||
@@ -1076,8 +1076,8 @@ func pbDelQueryDeserialize(in []*pbx.SeqRange) []MsgRange {
|
||||
|
||||
out := make([]MsgRange, len(in))
|
||||
for i, sr := range in {
|
||||
out[i].LowId = int(sr.GetLow())
|
||||
out[i].HiId = int(sr.GetHi())
|
||||
out[i].LowId = sr.GetLow()
|
||||
out[i].HiId = sr.GetHi()
|
||||
}
|
||||
|
||||
return out
|
||||
|
||||
+2
-2
@@ -577,8 +577,8 @@ func pluginSubscription(sub *types.Subscription, action int) {
|
||||
UserId: sub.User,
|
||||
|
||||
DelId: int32(sub.DelId),
|
||||
ReadId: int32(sub.ReadSeqId),
|
||||
RecvId: int32(sub.RecvSeqId),
|
||||
ReadId: sub.ReadSeqId,
|
||||
RecvId: sub.RecvSeqId,
|
||||
|
||||
Mode: &pbx.AccessMode{
|
||||
Want: sub.ModeWant.String(),
|
||||
|
||||
+5
-5
@@ -12,7 +12,7 @@ import (
|
||||
// presParams defines parameters for creating a presence notification.
|
||||
type presParams struct {
|
||||
userAgent string
|
||||
seqID int
|
||||
seqID int64
|
||||
delID int
|
||||
delSeq []MsgRange
|
||||
|
||||
@@ -483,7 +483,7 @@ func (t *Topic) presSubsOffline(what string, params *presParams,
|
||||
|
||||
// Publish {info what=read|recv|kp} to topic subscribers's sessions currently offline in the topic,
|
||||
// on subscriber's 'me'. Group and P2P.
|
||||
func (t *Topic) infoSubsOffline(from types.Uid, what string, seq int, skipSid string) {
|
||||
func (t *Topic) infoSubsOffline(from types.Uid, what string, seq int64, skipSid string) {
|
||||
user := from.UserId()
|
||||
|
||||
for uid, pud := range t.perUser {
|
||||
@@ -509,7 +509,7 @@ func (t *Topic) infoSubsOffline(from types.Uid, what string, seq int, skipSid st
|
||||
}
|
||||
|
||||
// Publish {info what=call} to topic subscribers's sessions on subscriber's 'me'.
|
||||
func (t *Topic) infoCallSubsOffline(from string, target types.Uid, event string, seq int,
|
||||
func (t *Topic) infoCallSubsOffline(from string, target types.Uid, event string, seq int64,
|
||||
sdp json.RawMessage, skipSid string, offlineOnly bool) {
|
||||
if target.IsZero() {
|
||||
logs.Err.Printf("callSubs could not find target: topic %s - from %s", t.name, from)
|
||||
@@ -672,9 +672,9 @@ func presSingleUserOfflineOffline(tenantID types.TenantID, uid types.Uid, origin
|
||||
// Let other sessions of a given user know what messages are now received/read.
|
||||
// If both 'read' and 'recv' != 0 then 'read' takes precedence over 'recv'.
|
||||
// Cases U
|
||||
func (t *Topic) presPubMessageCount(uid types.Uid, mode types.AccessMode, read, recv int, skip string) {
|
||||
func (t *Topic) presPubMessageCount(uid types.Uid, mode types.AccessMode, read, recv int64, skip string) {
|
||||
var what string
|
||||
var seq int
|
||||
var seq int64
|
||||
if read > 0 {
|
||||
what = "read"
|
||||
seq = read
|
||||
|
||||
+1
-1
@@ -164,7 +164,7 @@ func pushForChanDelete(tenantID types.TenantID, topicName string, now time.Time)
|
||||
}
|
||||
|
||||
// Prepares payload to be delivered to a mobile device as a push notification in response to receiving "read" notification.
|
||||
func (t *Topic) pushForReadRcpt(uid types.Uid, seq int, now time.Time) *push.Receipt {
|
||||
func (t *Topic) pushForReadRcpt(uid types.Uid, seq int64, now time.Time) *push.Receipt {
|
||||
// The `Topic` in the push receipt is `t.xoriginal` for group topics, `fromUid` for p2p topics,
|
||||
// not the t.original(fromUid) because it's the topic name as seen by the recipient, not by the sender.
|
||||
topic := t.xoriginal
|
||||
|
||||
@@ -39,7 +39,7 @@ func payloadToData(pl *push.Payload) (map[string]string, error) {
|
||||
// Must use "xfrom" because "from" is a reserved word. Google did not bother to document it anywhere.
|
||||
data["xfrom"] = pl.From
|
||||
if pl.What == push.ActMsg {
|
||||
data["seq"] = strconv.Itoa(pl.SeqId)
|
||||
data["seq"] = strconv.FormatInt(pl.SeqId, 10)
|
||||
if pl.ContentType != "" {
|
||||
data["mime"] = pl.ContentType
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func payloadToData(pl *push.Payload) (map[string]string, error) {
|
||||
data["modeWant"] = pl.ModeWant.String()
|
||||
data["modeGiven"] = pl.ModeGiven.String()
|
||||
} else if pl.What == push.ActRead {
|
||||
data["seq"] = strconv.Itoa(pl.SeqId)
|
||||
data["seq"] = strconv.FormatInt(pl.SeqId, 10)
|
||||
data["silent"] = "true"
|
||||
} else {
|
||||
return nil, errors.New("unknown push type")
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ type Payload struct {
|
||||
// Message sender 'usrXXX'
|
||||
From string `json:"from"`
|
||||
// Sequential ID of the message.
|
||||
SeqId int `json:"seq"`
|
||||
SeqId int64 `json:"seq"`
|
||||
// MIME-Type of the message content, text/x-drafty or text/plain
|
||||
ContentType string `json:"mime"`
|
||||
// Actual Data.Content of the message, if requested
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/tinode/chat/server/logs"
|
||||
"github.com/tinode/chat/server/store"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
func startRedisGroupFanout(ctx context.Context) {
|
||||
if err := store.StartGroupFanoutSubscriber(ctx, currentNodeID(), func(raw []byte) {
|
||||
var msg ServerComMessage
|
||||
if err := json.Unmarshal(raw, &msg); err != nil {
|
||||
logs.Warn.Println("redis group fanout: malformed message", err)
|
||||
return
|
||||
}
|
||||
if !msg.TenantID.IsValid() || msg.RcptTo == "" || types.GetTopicCat(msg.RcptTo) != types.TopicCatGrp {
|
||||
return
|
||||
}
|
||||
msg.sess = nil
|
||||
deliverRedisGroupFanout(&msg)
|
||||
}); err != nil {
|
||||
logs.Warn.Println("redis group fanout: subscriber disabled", err)
|
||||
}
|
||||
}
|
||||
|
||||
func deliverRedisGroupFanout(msg *ServerComMessage) {
|
||||
key := types.TopicKey{TenantID: msg.TenantID, Topic: msg.RcptTo}
|
||||
if globals.hub.topicGet(key) != nil {
|
||||
select {
|
||||
case globals.hub.routeSrv <- msg:
|
||||
default:
|
||||
logs.Err.Println("redis group fanout: hub routeSrv queue full", msg.RcptTo)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
delivered := 0
|
||||
localMembers := redisGroupFanoutMembers(msg.TenantID, msg.RcptTo)
|
||||
globals.sessionStore.Range(func(_ string, sess *Session) bool {
|
||||
if sess == nil || sess.isCluster() || sess.isProxy() || sess.tenantID != msg.TenantID || sess.sid == msg.SkipSid {
|
||||
return true
|
||||
}
|
||||
_, listedOnline := localMembers[sess.uid.UserId()]
|
||||
if !listedOnline && !sess.hasSub(msg.RcptTo) {
|
||||
return true
|
||||
}
|
||||
|
||||
msgCopy := msg.copy()
|
||||
prepareRedisGroupFanoutMessage(msgCopy, msg.RcptTo)
|
||||
if sess.queueOut(msgCopy) {
|
||||
delivered++
|
||||
} else {
|
||||
logs.Warn.Printf("redis group fanout: session queue full, sid=%s topic=%s", sess.sid, msg.RcptTo)
|
||||
}
|
||||
return true
|
||||
})
|
||||
if delivered == 0 {
|
||||
logs.Info.Println("redis group fanout: no local subscribers for unloaded topic", msg.RcptTo)
|
||||
}
|
||||
}
|
||||
|
||||
func redisGroupFanoutMembers(tenantID types.TenantID, topic string) map[string]struct{} {
|
||||
uids, err := store.GetTopicNodeOnlineMembers(tenantID, topic, currentNodeID())
|
||||
if err != nil {
|
||||
logs.Warn.Println("redis group fanout: failed to read local online members", topic, err)
|
||||
return nil
|
||||
}
|
||||
members := make(map[string]struct{}, len(uids))
|
||||
for _, uid := range uids {
|
||||
members[uid.UserId()] = struct{}{}
|
||||
}
|
||||
return members
|
||||
}
|
||||
|
||||
func prepareRedisGroupFanoutMessage(msg *ServerComMessage, topic string) {
|
||||
switch {
|
||||
case msg.Data != nil && msg.Data.Topic == "":
|
||||
msg.Data.Topic = topic
|
||||
case msg.Pres != nil && msg.Pres.Topic == "":
|
||||
msg.Pres.Topic = topic
|
||||
case msg.Info != nil && msg.Info.Topic == "":
|
||||
msg.Info.Topic = topic
|
||||
}
|
||||
}
|
||||
@@ -205,6 +205,32 @@ func (s *Session) getSub(topic string) *Subscription {
|
||||
return s.subs[topic]
|
||||
}
|
||||
|
||||
func (s *Session) hasSub(topic string) bool {
|
||||
if s.multi != nil {
|
||||
return s.multi.hasSub(topic)
|
||||
}
|
||||
s.subsLock.RLock()
|
||||
defer s.subsLock.RUnlock()
|
||||
_, ok := s.subs[topic]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *Session) groupSubs() []string {
|
||||
if s.multi != nil {
|
||||
return s.multi.groupSubs()
|
||||
}
|
||||
s.subsLock.RLock()
|
||||
defer s.subsLock.RUnlock()
|
||||
|
||||
topics := make([]string, 0, len(s.subs))
|
||||
for topic := range s.subs {
|
||||
if len(topic) >= 3 && types.GetTopicCat(topic) == types.TopicCatGrp {
|
||||
topics = append(topics, topic)
|
||||
}
|
||||
}
|
||||
return topics
|
||||
}
|
||||
|
||||
func (s *Session) delSub(topic string) {
|
||||
if s.multi != nil {
|
||||
s.multi.delSub(topic)
|
||||
@@ -434,6 +460,11 @@ func (s *Session) purgeChannels() {
|
||||
// cleanUp is called when the session is terminated to perform resource cleanup.
|
||||
func (s *Session) cleanUp(expired bool) {
|
||||
atomic.StoreInt32(&s.terminating, 1)
|
||||
if !s.uid.IsZero() && s.tenantID.IsValid() {
|
||||
if err := store.MarkUserOffline(s.tenantID, s.uid, s.sid, currentNodeID()); err != nil {
|
||||
logs.Warn.Println("s.cleanUp: failed to mark user offline", s.uid, err, s.sid)
|
||||
}
|
||||
}
|
||||
s.purgeChannels()
|
||||
s.inflightReqs.Wait()
|
||||
s.inflightReqs = nil
|
||||
@@ -488,6 +519,16 @@ func (s *Session) dispatchRaw(raw []byte) {
|
||||
func (s *Session) dispatch(msg *ClientComMessage) {
|
||||
now := types.TimeNow()
|
||||
atomic.StoreInt64(&s.lastAction, now.UnixNano())
|
||||
if !s.uid.IsZero() && s.tenantID.IsValid() {
|
||||
if err := store.RefreshUserPresence(s.tenantID, s.uid, s.sid, currentNodeID()); err != nil {
|
||||
logs.Warn.Println("s.dispatch: failed to refresh user presence", s.uid, err, s.sid)
|
||||
}
|
||||
for _, topic := range s.groupSubs() {
|
||||
if err := store.MarkTopicMemberOnline(s.tenantID, topic, s.uid, currentNodeID()); err != nil {
|
||||
logs.Warn.Println("s.dispatch: failed to refresh group member presence", topic, s.uid, err, s.sid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This should be the first block here, before any other checks.
|
||||
var resp *ServerComMessage
|
||||
@@ -1200,6 +1241,9 @@ func (s *Session) onLogin(msgID string, timestamp time.Time, rec *auth.Rec, miss
|
||||
// Authenticate the session.
|
||||
s.uid = rec.Uid
|
||||
s.authLvl = rec.AuthLevel
|
||||
if err := store.MarkUserOnline(s.tenantID, s.uid, s.sid, currentNodeID(), s.platf, s.userAgent); err != nil {
|
||||
logs.Warn.Println("s.onLogin: failed to mark user online", s.uid, err, s.sid)
|
||||
}
|
||||
// Reset expiration time.
|
||||
rec.Lifetime = 0
|
||||
}
|
||||
@@ -1228,6 +1272,25 @@ func (s *Session) onLogin(msgID string, timestamp time.Time, rec *auth.Rec, miss
|
||||
return reply
|
||||
}
|
||||
|
||||
func currentNodeID() string {
|
||||
if globals.cluster != nil && globals.cluster.thisNodeName != "" {
|
||||
return globals.cluster.thisNodeName
|
||||
}
|
||||
return "local"
|
||||
}
|
||||
|
||||
func redisUserOnline(tenantID types.TenantID, uid types.Uid, fallback bool) bool {
|
||||
if !store.UserPresenceEnabled() || !tenantID.IsValid() || uid.IsZero() {
|
||||
return fallback
|
||||
}
|
||||
presence, err := store.GetUserPresence(tenantID, uid)
|
||||
if err != nil {
|
||||
logs.Warn.Printf("presence[%s]: failed to read redis presence: %v", uid.UserId(), err)
|
||||
return fallback
|
||||
}
|
||||
return presence.Online
|
||||
}
|
||||
|
||||
func (s *Session) get(msg *ClientComMessage) {
|
||||
// Expand topic name.
|
||||
var resp *ServerComMessage
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
const configCacheTTL = time.Minute
|
||||
|
||||
type configCacheEntry[T any] struct {
|
||||
value T
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type configCache[T any] struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]configCacheEntry[T]
|
||||
}
|
||||
|
||||
func newConfigCache[T any]() *configCache[T] {
|
||||
return &configCache[T]{items: make(map[string]configCacheEntry[T])}
|
||||
}
|
||||
|
||||
func (c *configCache[T]) get(key string) (T, bool) {
|
||||
var zero T
|
||||
c.mu.RLock()
|
||||
entry, ok := c.items[key]
|
||||
c.mu.RUnlock()
|
||||
if !ok || time.Now().After(entry.expiresAt) {
|
||||
return zero, false
|
||||
}
|
||||
return entry.value, true
|
||||
}
|
||||
|
||||
func (c *configCache[T]) set(key string, value T) {
|
||||
c.mu.Lock()
|
||||
c.items[key] = configCacheEntry[T]{value: value, expiresAt: time.Now().Add(configCacheTTL)}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
var (
|
||||
tenantByCodeCache = newConfigCache[*types.Tenant]()
|
||||
tenantByIDCache = newConfigCache[*types.Tenant]()
|
||||
registrationSMSCache = newConfigCache[bool]()
|
||||
thirdPartyConfigCache = newConfigCache[map[string]string]()
|
||||
thirdPartyConfigAllKeys = "*"
|
||||
)
|
||||
|
||||
func cloneTenant(t *types.Tenant) *types.Tenant {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
cp := *t
|
||||
return &cp
|
||||
}
|
||||
|
||||
func cloneStringMap(src map[string]string) map[string]string {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := make(map[string]string, len(src))
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func thirdPartyCacheKey(group string, keys ...string) string {
|
||||
group = strings.TrimSpace(group)
|
||||
if len(keys) == 0 {
|
||||
return group + ":" + thirdPartyConfigAllKeys
|
||||
}
|
||||
normalized := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
if key = strings.TrimSpace(key); key != "" {
|
||||
normalized = append(normalized, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(normalized)
|
||||
return group + ":" + strings.Join(normalized, ",")
|
||||
}
|
||||
@@ -0,0 +1,866 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
type redisConfig struct {
|
||||
Addr string `json:"addr"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DB int `json:"db"`
|
||||
KeyPrefix string `json:"key_prefix"`
|
||||
DialMS int `json:"dial_timeout_ms"`
|
||||
ReadMS int `json:"read_timeout_ms"`
|
||||
WriteMS int `json:"write_timeout_ms"`
|
||||
PendingTTL int `json:"pending_ttl_seconds"`
|
||||
DoneTTL int `json:"done_ttl_seconds"`
|
||||
FailedTTL int `json:"failed_ttl_seconds"`
|
||||
HotMsgTTL int `json:"hot_message_ttl_seconds"`
|
||||
PresenceTTL int `json:"presence_ttl_seconds"`
|
||||
CacheTTL int `json:"cache_ttl_seconds"`
|
||||
ShortTTL int `json:"short_state_ttl_seconds"`
|
||||
GroupFanout bool `json:"group_fanout_enabled"`
|
||||
}
|
||||
|
||||
type groupMessageUniqueRecord struct {
|
||||
Status string `json:"status"`
|
||||
Seq int64 `json:"seq,omitempty"`
|
||||
MessageID string `json:"message_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type groupFanoutEnvelope struct {
|
||||
OriginNodeID string `json:"origin_node_id"`
|
||||
Message json.RawMessage `json:"message"`
|
||||
}
|
||||
|
||||
type groupMessageRuntime interface {
|
||||
Close() error
|
||||
Claim(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) (*groupMessageUniqueRecord, bool, error)
|
||||
NextSeq(tenantID types.TenantID, topic string, baseline int64) (int64, error)
|
||||
MarkDone(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string, seq int64, msgID string) error
|
||||
MarkFailed(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) error
|
||||
CacheHotMessage(tenantID types.TenantID, msg *types.Message) error
|
||||
GetHotMessages(tenantID types.TenantID, topic string, opts *types.QueryOpt, maxLimit int) ([]types.Message, bool, error)
|
||||
InvalidateHotMessages(tenantID types.TenantID, topic string, ranges []types.Range) error
|
||||
MarkUserOnline(tenantID types.TenantID, uid types.Uid, sid, nodeID, device, userAgent string) error
|
||||
RefreshUserPresence(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error
|
||||
MarkUserOffline(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error
|
||||
GetUserPresence(tenantID types.TenantID, uid types.Uid) (*UserPresence, error)
|
||||
MarkTopicMemberOnline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error
|
||||
MarkTopicMemberOffline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error
|
||||
GetTopicNodeOnlineMembers(tenantID types.TenantID, topic, nodeID string) ([]types.Uid, error)
|
||||
PublishGroupFanout(originNodeID string, message any) error
|
||||
StartGroupFanoutSubscriber(ctx context.Context, nodeID string, handler func([]byte)) error
|
||||
CacheObject(tenantID types.TenantID, namespace, id string, value any) error
|
||||
GetCachedObject(tenantID types.TenantID, namespace, id string, value any) (bool, error)
|
||||
DeleteCachedObject(tenantID types.TenantID, namespace, id string) error
|
||||
AddCachedSetMember(tenantID types.TenantID, namespace, id, member string) error
|
||||
RemoveCachedSetMember(tenantID types.TenantID, namespace, id, member string) error
|
||||
CachedSetContains(tenantID types.TenantID, namespace, id, member string) (bool, error)
|
||||
AllowRate(tenantID types.TenantID, scope string, limit int, window time.Duration) (bool, int, time.Duration, error)
|
||||
}
|
||||
|
||||
var groupMsgRuntime groupMessageRuntime
|
||||
|
||||
type UserPresence struct {
|
||||
Online bool `json:"online"`
|
||||
NodeID string `json:"node_id"`
|
||||
SessionCount int `json:"session_count"`
|
||||
LastActiveAt time.Time `json:"last_active_at"`
|
||||
LastSeenAt time.Time `json:"last_seen_at"`
|
||||
}
|
||||
|
||||
func MarkUserOnline(tenantID types.TenantID, uid types.Uid, sid, nodeID, device, userAgent string) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() || sid == "" {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.MarkUserOnline(tenantID, uid, sid, nodeID, device, userAgent)
|
||||
}
|
||||
|
||||
func RefreshUserPresence(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() || sid == "" {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.RefreshUserPresence(tenantID, uid, sid, nodeID)
|
||||
}
|
||||
|
||||
func MarkUserOffline(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() || sid == "" {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.MarkUserOffline(tenantID, uid, sid, nodeID)
|
||||
}
|
||||
|
||||
func GetUserPresence(tenantID types.TenantID, uid types.Uid) (*UserPresence, error) {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() {
|
||||
return &UserPresence{}, nil
|
||||
}
|
||||
return groupMsgRuntime.GetUserPresence(tenantID, uid)
|
||||
}
|
||||
|
||||
func UserPresenceEnabled() bool {
|
||||
return groupMsgRuntime != nil
|
||||
}
|
||||
|
||||
func MarkTopicMemberOnline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || topic == "" || uid.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.MarkTopicMemberOnline(tenantID, topic, uid, nodeID)
|
||||
}
|
||||
|
||||
func MarkTopicMemberOffline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || topic == "" || uid.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.MarkTopicMemberOffline(tenantID, topic, uid, nodeID)
|
||||
}
|
||||
|
||||
func GetTopicNodeOnlineMembers(tenantID types.TenantID, topic, nodeID string) ([]types.Uid, error) {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || topic == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return groupMsgRuntime.GetTopicNodeOnlineMembers(tenantID, topic, nodeID)
|
||||
}
|
||||
|
||||
func PublishGroupFanout(originNodeID string, message any) error {
|
||||
if groupMsgRuntime == nil || originNodeID == "" || message == nil {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.PublishGroupFanout(originNodeID, message)
|
||||
}
|
||||
|
||||
func StartGroupFanoutSubscriber(ctx context.Context, nodeID string, handler func([]byte)) error {
|
||||
if groupMsgRuntime == nil || nodeID == "" || handler == nil {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.StartGroupFanoutSubscriber(ctx, nodeID, handler)
|
||||
}
|
||||
|
||||
func CacheObject(tenantID types.TenantID, namespace, id string, value any) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" || value == nil {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.CacheObject(tenantID, namespace, id, value)
|
||||
}
|
||||
|
||||
func GetCachedObject(tenantID types.TenantID, namespace, id string, value any) (bool, error) {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" || value == nil {
|
||||
return false, nil
|
||||
}
|
||||
return groupMsgRuntime.GetCachedObject(tenantID, namespace, id, value)
|
||||
}
|
||||
|
||||
func DeleteCachedObject(tenantID types.TenantID, namespace, id string) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.DeleteCachedObject(tenantID, namespace, id)
|
||||
}
|
||||
|
||||
func AddCachedSetMember(tenantID types.TenantID, namespace, id, member string) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" || member == "" {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.AddCachedSetMember(tenantID, namespace, id, member)
|
||||
}
|
||||
|
||||
func RemoveCachedSetMember(tenantID types.TenantID, namespace, id, member string) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" || member == "" {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.RemoveCachedSetMember(tenantID, namespace, id, member)
|
||||
}
|
||||
|
||||
func CachedSetContains(tenantID types.TenantID, namespace, id, member string) (bool, error) {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" || member == "" {
|
||||
return false, nil
|
||||
}
|
||||
return groupMsgRuntime.CachedSetContains(tenantID, namespace, id, member)
|
||||
}
|
||||
|
||||
func AllowRate(tenantID types.TenantID, scope string, limit int, window time.Duration) (bool, int, time.Duration, error) {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || scope == "" || limit <= 0 || window <= 0 {
|
||||
return true, 0, 0, nil
|
||||
}
|
||||
return groupMsgRuntime.AllowRate(tenantID, scope, limit, window)
|
||||
}
|
||||
|
||||
func initGroupMessageRuntime(raw json.RawMessage) error {
|
||||
closeGroupMessageRuntime()
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return nil
|
||||
}
|
||||
var cfg redisConfig
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.Addr = strings.TrimSpace(cfg.Addr)
|
||||
if cfg.Addr == "" {
|
||||
return nil
|
||||
}
|
||||
if cfg.KeyPrefix == "" {
|
||||
cfg.KeyPrefix = "im"
|
||||
}
|
||||
if cfg.PendingTTL <= 0 {
|
||||
cfg.PendingTTL = 120
|
||||
}
|
||||
if cfg.DoneTTL <= 0 {
|
||||
cfg.DoneTTL = 86400
|
||||
}
|
||||
if cfg.FailedTTL <= 0 {
|
||||
cfg.FailedTTL = 600
|
||||
}
|
||||
if cfg.HotMsgTTL <= 0 {
|
||||
cfg.HotMsgTTL = 86400
|
||||
}
|
||||
if cfg.PresenceTTL <= 0 {
|
||||
cfg.PresenceTTL = 120
|
||||
}
|
||||
if cfg.CacheTTL <= 0 {
|
||||
cfg.CacheTTL = 300
|
||||
}
|
||||
if cfg.ShortTTL <= 0 {
|
||||
cfg.ShortTTL = 600
|
||||
}
|
||||
dialTimeout := timeoutOrDefault(cfg.DialMS, 2*time.Second)
|
||||
readTimeout := timeoutOrDefault(cfg.ReadMS, 2*time.Second)
|
||||
writeTimeout := timeoutOrDefault(cfg.WriteMS, 2*time.Second)
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: cfg.Addr,
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
DialTimeout: dialTimeout,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dialTimeout)
|
||||
defer cancel()
|
||||
if err := client.Ping(ctx).Err(); err != nil {
|
||||
_ = client.Close()
|
||||
return err
|
||||
}
|
||||
groupMsgRuntime = &redisGroupMessageRuntime{client: client, prefix: cfg.KeyPrefix, cfg: cfg}
|
||||
return nil
|
||||
}
|
||||
|
||||
func closeGroupMessageRuntime() {
|
||||
if groupMsgRuntime != nil {
|
||||
_ = groupMsgRuntime.Close()
|
||||
groupMsgRuntime = nil
|
||||
}
|
||||
}
|
||||
|
||||
func timeoutOrDefault(ms int, fallback time.Duration) time.Duration {
|
||||
if ms <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return time.Duration(ms) * time.Millisecond
|
||||
}
|
||||
|
||||
type redisGroupMessageRuntime struct {
|
||||
client *redis.Client
|
||||
prefix string
|
||||
cfg redisConfig
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) Close() error {
|
||||
return r.client.Close()
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) Claim(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) (*groupMessageUniqueRecord, bool, error) {
|
||||
if clientMsgID == "" {
|
||||
return nil, true, nil
|
||||
}
|
||||
key := r.uniqueKey(tenantID, topic, from, clientMsgID)
|
||||
rec := &groupMessageUniqueRecord{Status: "pending", CreatedAt: time.Now().UTC()}
|
||||
raw, _ := json.Marshal(rec)
|
||||
ctx := context.Background()
|
||||
ok, err := r.client.SetNX(ctx, key, raw, time.Duration(r.cfg.PendingTTL)*time.Second).Result()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if ok {
|
||||
return rec, true, nil
|
||||
}
|
||||
existing, err := r.client.Get(ctx, key).Bytes()
|
||||
if err == redis.Nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
var current groupMessageUniqueRecord
|
||||
if err = json.Unmarshal(existing, ¤t); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if current.Status == "failed" {
|
||||
_ = r.client.Del(ctx, key).Err()
|
||||
ok, err = r.client.SetNX(ctx, key, raw, time.Duration(r.cfg.PendingTTL)*time.Second).Result()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if ok {
|
||||
return rec, true, nil
|
||||
}
|
||||
}
|
||||
return ¤t, false, nil
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) NextSeq(tenantID types.TenantID, topic string, baseline int64) (int64, error) {
|
||||
if baseline < 0 {
|
||||
baseline = 0
|
||||
}
|
||||
seqKey := r.seqKey(tenantID, topic)
|
||||
seq, err := r.client.Eval(context.Background(), `
|
||||
if redis.call("EXISTS", KEYS[1]) == 0 then
|
||||
redis.call("SET", KEYS[1], ARGV[1])
|
||||
end
|
||||
return redis.call("INCR", KEYS[1])
|
||||
`, []string{seqKey}, baseline).Int64()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return seq, nil
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) MarkDone(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string, seq int64, msgID string) error {
|
||||
if clientMsgID == "" {
|
||||
return nil
|
||||
}
|
||||
rec := &groupMessageUniqueRecord{Status: "done", Seq: seq, MessageID: msgID, CreatedAt: time.Now().UTC()}
|
||||
raw, _ := json.Marshal(rec)
|
||||
return r.client.Set(context.Background(), r.uniqueKey(tenantID, topic, from, clientMsgID), raw, time.Duration(r.cfg.DoneTTL)*time.Second).Err()
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) MarkFailed(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) error {
|
||||
if clientMsgID == "" {
|
||||
return nil
|
||||
}
|
||||
rec := &groupMessageUniqueRecord{Status: "failed", CreatedAt: time.Now().UTC()}
|
||||
raw, _ := json.Marshal(rec)
|
||||
return r.client.Set(context.Background(), r.uniqueKey(tenantID, topic, from, clientMsgID), raw, time.Duration(r.cfg.FailedTTL)*time.Second).Err()
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) CacheHotMessage(tenantID types.TenantID, msg *types.Message) error {
|
||||
if msg == nil || msg.Topic == "" || msg.SeqId <= 0 {
|
||||
return types.ErrMalformed
|
||||
}
|
||||
payload, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
ttl := time.Duration(r.cfg.HotMsgTTL) * time.Second
|
||||
seq := strconv.FormatInt(msg.SeqId, 10)
|
||||
pipe := r.client.Pipeline()
|
||||
pipe.Set(ctx, r.hotMsgKey(tenantID, msg.Topic, msg.SeqId), payload, ttl)
|
||||
pipe.ZAdd(ctx, r.hotSeqKey(tenantID, msg.Topic), redis.Z{Score: float64(msg.SeqId), Member: seq})
|
||||
pipe.Expire(ctx, r.hotSeqKey(tenantID, msg.Topic), ttl+time.Hour)
|
||||
_, err = pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) GetHotMessages(tenantID types.TenantID, topic string, opts *types.QueryOpt, maxLimit int) ([]types.Message, bool, error) {
|
||||
if topic == "" {
|
||||
return nil, false, types.ErrMalformed
|
||||
}
|
||||
limit := maxLimit
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
if opts != nil && opts.Limit > 0 && opts.Limit < limit {
|
||||
limit = opts.Limit
|
||||
}
|
||||
seqs, completeRange, err := r.hotSeqIDs(tenantID, topic, opts, limit)
|
||||
if err != nil || len(seqs) == 0 || !completeRange {
|
||||
return nil, false, err
|
||||
}
|
||||
keys := make([]string, 0, len(seqs))
|
||||
for _, seq := range seqs {
|
||||
keys = append(keys, r.hotMsgKey(tenantID, topic, seq))
|
||||
}
|
||||
values, err := r.client.MGet(context.Background(), keys...).Result()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(values) != len(seqs) {
|
||||
return nil, false, nil
|
||||
}
|
||||
msgs := make([]types.Message, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
raw, ok := value.(string)
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
var msg types.Message
|
||||
if err := json.Unmarshal([]byte(raw), &msg); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
msg.TenantID = tenantID
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
return msgs, true, nil
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) InvalidateHotMessages(tenantID types.TenantID, topic string, ranges []types.Range) error {
|
||||
if topic == "" {
|
||||
return types.ErrMalformed
|
||||
}
|
||||
seqs := expandRanges(ranges, int(^uint(0)>>1))
|
||||
ctx := context.Background()
|
||||
if len(seqs) == 0 {
|
||||
pipe := r.client.Pipeline()
|
||||
pipe.Del(ctx, r.hotSeqKey(tenantID, topic))
|
||||
keys, err := r.client.Keys(ctx, r.hotMsgKey(tenantID, topic, "*")).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
pipe.Del(ctx, keys...)
|
||||
}
|
||||
_, err = pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
pipe := r.client.Pipeline()
|
||||
members := make([]any, 0, len(seqs))
|
||||
for _, seq := range seqs {
|
||||
pipe.Del(ctx, r.hotMsgKey(tenantID, topic, seq))
|
||||
members = append(members, strconv.FormatInt(int64(seq), 10))
|
||||
}
|
||||
pipe.ZRem(ctx, r.hotSeqKey(tenantID, topic), members...)
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) MarkUserOnline(tenantID types.TenantID, uid types.Uid, sid, nodeID, device, userAgent string) error {
|
||||
if nodeID == "" {
|
||||
nodeID = "local"
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
ttl := time.Duration(r.cfg.PresenceTTL) * time.Second
|
||||
ctx := context.Background()
|
||||
userKey := r.presenceUserKey(tenantID, uid)
|
||||
sessionsKey := r.presenceSessionsKey(tenantID, uid)
|
||||
sessionKey := r.presenceSessionKey(tenantID, uid, sid)
|
||||
nodeKey := r.presenceNodeUsersKey(tenantID, nodeID)
|
||||
member := nodeID + ":" + sid
|
||||
pipe := r.client.Pipeline()
|
||||
pipe.HSet(ctx, sessionKey, map[string]any{
|
||||
"node_id": nodeID,
|
||||
"session_id": sid,
|
||||
"device": device,
|
||||
"user_agent": userAgent,
|
||||
"last_active_at": now.Format(time.RFC3339Nano),
|
||||
})
|
||||
pipe.Expire(ctx, sessionKey, ttl)
|
||||
pipe.SAdd(ctx, sessionsKey, member)
|
||||
pipe.Expire(ctx, sessionsKey, ttl)
|
||||
pipe.SAdd(ctx, nodeKey, uid.UserId())
|
||||
pipe.Expire(ctx, nodeKey, ttl)
|
||||
pipe.HSet(ctx, userKey, map[string]any{
|
||||
"online": "1",
|
||||
"node_id": nodeID,
|
||||
"last_active_at": now.Format(time.RFC3339Nano),
|
||||
"last_seen_at": now.Format(time.RFC3339Nano),
|
||||
})
|
||||
pipe.Expire(ctx, userKey, ttl)
|
||||
if _, err := pipe.Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
count, err := r.client.SCard(ctx, sessionsKey).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.client.HSet(ctx, userKey, "session_count", count).Err()
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) RefreshUserPresence(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error {
|
||||
if nodeID == "" {
|
||||
nodeID = "local"
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
ttl := time.Duration(r.cfg.PresenceTTL) * time.Second
|
||||
ctx := context.Background()
|
||||
userKey := r.presenceUserKey(tenantID, uid)
|
||||
sessionsKey := r.presenceSessionsKey(tenantID, uid)
|
||||
sessionKey := r.presenceSessionKey(tenantID, uid, sid)
|
||||
nodeKey := r.presenceNodeUsersKey(tenantID, nodeID)
|
||||
pipe := r.client.Pipeline()
|
||||
pipe.HSet(ctx, sessionKey, "last_active_at", now.Format(time.RFC3339Nano), "node_id", nodeID)
|
||||
pipe.Expire(ctx, sessionKey, ttl)
|
||||
pipe.Expire(ctx, sessionsKey, ttl)
|
||||
pipe.SAdd(ctx, nodeKey, uid.UserId())
|
||||
pipe.Expire(ctx, nodeKey, ttl)
|
||||
pipe.HSet(ctx, userKey, "online", "1", "node_id", nodeID, "last_active_at", now.Format(time.RFC3339Nano), "last_seen_at", now.Format(time.RFC3339Nano))
|
||||
pipe.Expire(ctx, userKey, ttl)
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) MarkUserOffline(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error {
|
||||
if nodeID == "" {
|
||||
nodeID = "local"
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
ttl := time.Duration(r.cfg.PresenceTTL) * time.Second
|
||||
ctx := context.Background()
|
||||
userKey := r.presenceUserKey(tenantID, uid)
|
||||
sessionsKey := r.presenceSessionsKey(tenantID, uid)
|
||||
sessionKey := r.presenceSessionKey(tenantID, uid, sid)
|
||||
nodeKey := r.presenceNodeUsersKey(tenantID, nodeID)
|
||||
member := nodeID + ":" + sid
|
||||
pipe := r.client.Pipeline()
|
||||
pipe.Del(ctx, sessionKey)
|
||||
pipe.SRem(ctx, sessionsKey, member)
|
||||
pipe.SRem(ctx, nodeKey, uid.UserId())
|
||||
if _, err := pipe.Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
count, err := r.client.SCard(ctx, sessionsKey).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
pipe = r.client.Pipeline()
|
||||
pipe.HSet(ctx, userKey, "online", "1", "session_count", count, "last_seen_at", now.Format(time.RFC3339Nano))
|
||||
pipe.Expire(ctx, userKey, ttl)
|
||||
pipe.Expire(ctx, sessionsKey, ttl)
|
||||
_, err = pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
pipe = r.client.Pipeline()
|
||||
pipe.HSet(ctx, userKey, "online", "0", "session_count", 0, "node_id", nodeID, "last_seen_at", now.Format(time.RFC3339Nano))
|
||||
pipe.Expire(ctx, userKey, ttl)
|
||||
pipe.Del(ctx, sessionsKey)
|
||||
_, err = pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) GetUserPresence(tenantID types.TenantID, uid types.Uid) (*UserPresence, error) {
|
||||
raw, err := r.client.HGetAll(context.Background(), r.presenceUserKey(tenantID, uid)).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
presence := &UserPresence{}
|
||||
if len(raw) == 0 {
|
||||
return presence, nil
|
||||
}
|
||||
presence.Online = raw["online"] == "1"
|
||||
presence.NodeID = raw["node_id"]
|
||||
presence.SessionCount, _ = strconv.Atoi(raw["session_count"])
|
||||
presence.LastActiveAt, _ = time.Parse(time.RFC3339Nano, raw["last_active_at"])
|
||||
presence.LastSeenAt, _ = time.Parse(time.RFC3339Nano, raw["last_seen_at"])
|
||||
return presence, nil
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) MarkTopicMemberOnline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error {
|
||||
if nodeID == "" {
|
||||
nodeID = "local"
|
||||
}
|
||||
ttl := time.Duration(r.cfg.PresenceTTL) * time.Second
|
||||
ctx := context.Background()
|
||||
pipe := r.client.Pipeline()
|
||||
pipe.SAdd(ctx, r.topicOnlineMembersKey(tenantID, topic), uid.UserId())
|
||||
pipe.Expire(ctx, r.topicOnlineMembersKey(tenantID, topic), ttl)
|
||||
pipe.SAdd(ctx, r.topicNodeOnlineMembersKey(tenantID, topic, nodeID), uid.UserId())
|
||||
pipe.Expire(ctx, r.topicNodeOnlineMembersKey(tenantID, topic, nodeID), ttl)
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) MarkTopicMemberOffline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error {
|
||||
if nodeID == "" {
|
||||
nodeID = "local"
|
||||
}
|
||||
ctx := context.Background()
|
||||
pipe := r.client.Pipeline()
|
||||
pipe.SRem(ctx, r.topicOnlineMembersKey(tenantID, topic), uid.UserId())
|
||||
pipe.SRem(ctx, r.topicNodeOnlineMembersKey(tenantID, topic, nodeID), uid.UserId())
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) GetTopicNodeOnlineMembers(tenantID types.TenantID, topic, nodeID string) ([]types.Uid, error) {
|
||||
if nodeID == "" {
|
||||
nodeID = "local"
|
||||
}
|
||||
members, err := r.client.SMembers(context.Background(), r.topicNodeOnlineMembersKey(tenantID, topic, nodeID)).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uids := make([]types.Uid, 0, len(members))
|
||||
for _, member := range members {
|
||||
uid := types.ParseUserId(member)
|
||||
if !uid.IsZero() {
|
||||
uids = append(uids, uid)
|
||||
}
|
||||
}
|
||||
return uids, nil
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) PublishGroupFanout(originNodeID string, message any) error {
|
||||
if !r.cfg.GroupFanout {
|
||||
return nil
|
||||
}
|
||||
payload, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
envelope, err := json.Marshal(groupFanoutEnvelope{
|
||||
OriginNodeID: originNodeID,
|
||||
Message: payload,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.client.Publish(context.Background(), r.groupFanoutChannel(), envelope).Err()
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) StartGroupFanoutSubscriber(ctx context.Context, nodeID string, handler func([]byte)) error {
|
||||
if !r.cfg.GroupFanout {
|
||||
return nil
|
||||
}
|
||||
pubsub := r.client.Subscribe(ctx, r.groupFanoutChannel())
|
||||
if _, err := pubsub.Receive(ctx); err != nil {
|
||||
_ = pubsub.Close()
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
defer pubsub.Close()
|
||||
ch := pubsub.Channel()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case msg, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var envelope groupFanoutEnvelope
|
||||
if err := json.Unmarshal([]byte(msg.Payload), &envelope); err != nil || envelope.OriginNodeID == nodeID || len(envelope.Message) == 0 {
|
||||
continue
|
||||
}
|
||||
handler(envelope.Message)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) CacheObject(tenantID types.TenantID, namespace, id string, value any) error {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.client.Set(context.Background(), r.objectCacheKey(tenantID, namespace, id), payload, r.cacheTTL(namespace)).Err()
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) GetCachedObject(tenantID types.TenantID, namespace, id string, value any) (bool, error) {
|
||||
payload, err := r.client.Get(context.Background(), r.objectCacheKey(tenantID, namespace, id)).Bytes()
|
||||
if err == redis.Nil {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err = json.Unmarshal(payload, value); err != nil {
|
||||
_ = r.client.Del(context.Background(), r.objectCacheKey(tenantID, namespace, id)).Err()
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) DeleteCachedObject(tenantID types.TenantID, namespace, id string) error {
|
||||
return r.client.Del(context.Background(), r.objectCacheKey(tenantID, namespace, id)).Err()
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) AddCachedSetMember(tenantID types.TenantID, namespace, id, member string) error {
|
||||
key := r.setCacheKey(tenantID, namespace, id)
|
||||
ctx := context.Background()
|
||||
pipe := r.client.Pipeline()
|
||||
pipe.SAdd(ctx, key, member)
|
||||
pipe.Expire(ctx, key, r.cacheTTL(namespace))
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) RemoveCachedSetMember(tenantID types.TenantID, namespace, id, member string) error {
|
||||
key := r.setCacheKey(tenantID, namespace, id)
|
||||
ctx := context.Background()
|
||||
pipe := r.client.Pipeline()
|
||||
pipe.SRem(ctx, key, member)
|
||||
pipe.Expire(ctx, key, r.cacheTTL(namespace))
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) CachedSetContains(tenantID types.TenantID, namespace, id, member string) (bool, error) {
|
||||
return r.client.SIsMember(context.Background(), r.setCacheKey(tenantID, namespace, id), member).Result()
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) AllowRate(tenantID types.TenantID, scope string, limit int, window time.Duration) (bool, int, time.Duration, error) {
|
||||
key := r.rateKey(tenantID, scope)
|
||||
ctx := context.Background()
|
||||
count, err := r.client.Incr(ctx, key).Result()
|
||||
if err != nil {
|
||||
return true, 0, 0, err
|
||||
}
|
||||
if count == 1 {
|
||||
if err = r.client.Expire(ctx, key, window).Err(); err != nil {
|
||||
return true, int(count), 0, err
|
||||
}
|
||||
}
|
||||
ttl, _ := r.client.TTL(ctx, key).Result()
|
||||
return count <= int64(limit), int(count), ttl, nil
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) hotSeqIDs(tenantID types.TenantID, topic string, opts *types.QueryOpt, limit int) ([]int64, bool, error) {
|
||||
if opts != nil && len(opts.IdRanges) > 0 {
|
||||
seqs := expandRanges(opts.IdRanges, limit)
|
||||
sort.Slice(seqs, func(i, j int) bool { return seqs[i] > seqs[j] })
|
||||
return seqs, len(seqs) > 0 && len(seqs) <= limit, nil
|
||||
}
|
||||
min := "-inf"
|
||||
max := "+inf"
|
||||
if opts != nil {
|
||||
if opts.Since > 0 {
|
||||
min = strconv.FormatInt(opts.Since, 10)
|
||||
}
|
||||
if opts.Before > 1 {
|
||||
max = strconv.FormatInt(opts.Before-1, 10)
|
||||
}
|
||||
}
|
||||
raw, err := r.client.ZRevRangeByScore(context.Background(), r.hotSeqKey(tenantID, topic), &redis.ZRangeBy{
|
||||
Min: min,
|
||||
Max: max,
|
||||
Offset: 0,
|
||||
Count: int64(limit),
|
||||
}).Result()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(raw) < limit {
|
||||
return nil, false, nil
|
||||
}
|
||||
seqs := make([]int64, 0, len(raw))
|
||||
for _, value := range raw {
|
||||
seq, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
seqs = append(seqs, seq)
|
||||
}
|
||||
return seqs, true, nil
|
||||
}
|
||||
|
||||
func expandRanges(ranges []types.Range, limit int) []int64 {
|
||||
if limit <= 0 {
|
||||
return nil
|
||||
}
|
||||
seqs := make([]int64, 0)
|
||||
for _, rng := range ranges {
|
||||
if rng.Low <= 0 {
|
||||
continue
|
||||
}
|
||||
if rng.Hi == 0 {
|
||||
seqs = append(seqs, rng.Low)
|
||||
} else {
|
||||
for seq := rng.Low; seq < rng.Hi; seq++ {
|
||||
seqs = append(seqs, seq)
|
||||
if len(seqs) >= limit {
|
||||
return seqs
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(seqs) >= limit {
|
||||
return seqs
|
||||
}
|
||||
}
|
||||
return seqs
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) seqKey(tenantID types.TenantID, topic string) string {
|
||||
return r.key(tenantID, "topic", topic, "last_seq")
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) uniqueKey(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) string {
|
||||
return r.key(tenantID, "topic", topic, "uniq", "msg", from.UserId(), clientMsgID)
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) hotSeqKey(tenantID types.TenantID, topic string) string {
|
||||
return r.key(tenantID, "topic", topic, "hot_msg_seqs")
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) hotMsgKey(tenantID types.TenantID, topic string, seq any) string {
|
||||
return r.key(tenantID, "topic", topic, "hot_msg", fmt.Sprint(seq))
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) presenceUserKey(tenantID types.TenantID, uid types.Uid) string {
|
||||
return r.key(tenantID, "presence", "user", uid.UserId())
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) presenceSessionsKey(tenantID types.TenantID, uid types.Uid) string {
|
||||
return r.key(tenantID, "presence", "user", uid.UserId(), "sessions")
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) presenceSessionKey(tenantID types.TenantID, uid types.Uid, sid string) string {
|
||||
return r.key(tenantID, "presence", "user", uid.UserId(), "session", sid)
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) presenceNodeUsersKey(tenantID types.TenantID, nodeID string) string {
|
||||
return r.key(tenantID, "presence", "node", nodeID, "users")
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) topicOnlineMembersKey(tenantID types.TenantID, topic string) string {
|
||||
return r.key(tenantID, "topic", topic, "online_members")
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) topicNodeOnlineMembersKey(tenantID types.TenantID, topic, nodeID string) string {
|
||||
return r.key(tenantID, "topic", topic, "node", nodeID, "online_members")
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) groupFanoutChannel() string {
|
||||
return r.key(types.ZeroTenantID, "group_fanout")
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) objectCacheKey(tenantID types.TenantID, namespace, id string) string {
|
||||
return r.key(tenantID, "cache", namespace, id)
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) setCacheKey(tenantID types.TenantID, namespace, id string) string {
|
||||
return r.key(tenantID, "set", namespace, id)
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) rateKey(tenantID types.TenantID, scope string) string {
|
||||
return r.key(tenantID, "rate", scope)
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) cacheTTL(namespace string) time.Duration {
|
||||
ttl := r.cfg.CacheTTL
|
||||
if namespace == "verification" || namespace == "action_token" {
|
||||
ttl = r.cfg.ShortTTL
|
||||
}
|
||||
return time.Duration(ttl) * time.Second
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) key(tenantID types.TenantID, parts ...string) string {
|
||||
all := []string{strings.TrimSuffix(r.prefix, ":"), strconv.FormatInt(int64(tenantID), 10)}
|
||||
all = append(all, parts...)
|
||||
return strings.Join(all, ":")
|
||||
}
|
||||
@@ -1114,7 +1114,7 @@ func (mr *MockMessagesPersistenceInterfaceMockRecorder) DeleteList(tenantID, top
|
||||
}
|
||||
|
||||
// Edit mocks base method.
|
||||
func (m *MockMessagesPersistenceInterface) Edit(tenantID types.TenantID, topic string, seq int, actor types.Uid, expectedVersion int, head types.KVMap, content any, reason string, editedAt time.Time) (*types.Message, error) {
|
||||
func (m *MockMessagesPersistenceInterface) Edit(tenantID types.TenantID, topic string, seq int64, actor types.Uid, expectedVersion int, head types.KVMap, content any, reason string, editedAt time.Time) (*types.Message, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Edit", tenantID, topic, seq, actor, expectedVersion, head, content, reason, editedAt)
|
||||
ret0, _ := ret[0].(*types.Message)
|
||||
@@ -1176,7 +1176,7 @@ func (mr *MockMessagesPersistenceInterfaceMockRecorder) Search(tenantID, topic,
|
||||
}
|
||||
|
||||
// GetOne mocks base method.
|
||||
func (m *MockMessagesPersistenceInterface) GetOne(tenantID types.TenantID, topic string, seq int) (*types.Message, error) {
|
||||
func (m *MockMessagesPersistenceInterface) GetOne(tenantID types.TenantID, topic string, seq int64) (*types.Message, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOne", tenantID, topic, seq)
|
||||
ret0, _ := ret[0].(*types.Message)
|
||||
@@ -1191,7 +1191,7 @@ func (mr *MockMessagesPersistenceInterfaceMockRecorder) GetOne(tenantID, topic,
|
||||
}
|
||||
|
||||
// Recall mocks base method.
|
||||
func (m *MockMessagesPersistenceInterface) Recall(tenantID types.TenantID, topic string, seq int, actor types.Uid, expectedVersion int, recalledAt time.Time) (*types.Message, error) {
|
||||
func (m *MockMessagesPersistenceInterface) Recall(tenantID types.TenantID, topic string, seq int64, actor types.Uid, expectedVersion int, recalledAt time.Time) (*types.Message, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Recall", tenantID, topic, seq, actor, expectedVersion, recalledAt)
|
||||
ret0, _ := ret[0].(*types.Message)
|
||||
|
||||
+645
-47
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ type CallRecord struct {
|
||||
TenantID TenantID `db:"tenant_id" json:"-" bson:"tenant_id"`
|
||||
CallID string `db:"call_id"`
|
||||
Topic string
|
||||
SeqID int `db:"seq_id"`
|
||||
SeqID int64 `db:"seq_id"`
|
||||
CallerID Uid `db:"caller_id"`
|
||||
CalleeID Uid `db:"callee_id"`
|
||||
CallType string `db:"call_type"`
|
||||
|
||||
@@ -9,7 +9,7 @@ type FavoriteItem struct {
|
||||
UserID Uid `db:"user_id"`
|
||||
ItemType string `db:"item_type"`
|
||||
SourceTopic string `db:"source_topic"`
|
||||
SourceSeq int `db:"source_seq"`
|
||||
SourceSeq int64 `db:"source_seq"`
|
||||
Snapshot any
|
||||
AttachmentRefs any `db:"attachment_refs"`
|
||||
Availability string `db:"availability"`
|
||||
|
||||
@@ -187,7 +187,7 @@ type GroupFile struct {
|
||||
Topic string `db:"topic"`
|
||||
FileID string `db:"file_id"`
|
||||
SourceTopic string `db:"source_topic"`
|
||||
SourceSeq int `db:"source_seq"`
|
||||
SourceSeq int64 `db:"source_seq"`
|
||||
UploaderID Uid `db:"-"`
|
||||
FileName string `db:"file_name"`
|
||||
MIMEType string `db:"mime_type"`
|
||||
|
||||
@@ -63,7 +63,7 @@ type AbuseReport struct {
|
||||
TargetType string
|
||||
TargetUserID Uid
|
||||
TargetTopic string
|
||||
TargetSeq int
|
||||
TargetSeq int64
|
||||
ReasonCode string
|
||||
Description string
|
||||
EvidenceFileIDs []string
|
||||
|
||||
+14
-14
@@ -922,9 +922,9 @@ type Subscription struct {
|
||||
// ID of the latest Soft-delete operation
|
||||
DelId int
|
||||
// Last SeqId reported by user as received by at least one of his sessions
|
||||
RecvSeqId int
|
||||
RecvSeqId int64
|
||||
// Last SeqID reported read by the user
|
||||
ReadSeqId int
|
||||
ReadSeqId int64
|
||||
|
||||
// Access mode requested by this user
|
||||
ModeWant AccessMode
|
||||
@@ -941,7 +941,7 @@ type Subscription struct {
|
||||
// In case of P2P topics this is the Trusted value of the other user.
|
||||
trusted any
|
||||
// deserialized SeqID from user or topic
|
||||
seqId int
|
||||
seqId int64
|
||||
// Deserialized TouchedAt from topic
|
||||
touchedAt time.Time
|
||||
// Timestamp & user agent of when the user was last online.
|
||||
@@ -1013,12 +1013,12 @@ func (s *Subscription) LastModified() time.Time {
|
||||
}
|
||||
|
||||
// GetSeqId returns seqId.
|
||||
func (s *Subscription) GetSeqId() int {
|
||||
func (s *Subscription) GetSeqId() int64 {
|
||||
return s.seqId
|
||||
}
|
||||
|
||||
// SetSeqId sets seqId field.
|
||||
func (s *Subscription) SetSeqId(id int) {
|
||||
func (s *Subscription) SetSeqId(id int64) {
|
||||
s.seqId = id
|
||||
}
|
||||
|
||||
@@ -1144,7 +1144,7 @@ type Topic struct {
|
||||
Access DefaultAccess
|
||||
|
||||
// Server-issued sequential ID
|
||||
SeqId int
|
||||
SeqId int64
|
||||
// If messages were deleted, sequential id of the last operation to delete them
|
||||
DelId int
|
||||
|
||||
@@ -1239,7 +1239,7 @@ type Message struct {
|
||||
DelId int `json:"DelId,omitempty" bson:",omitempty"`
|
||||
// List of users who have marked this message as soft-deleted
|
||||
DeletedFor []SoftDelete `json:"DeletedFor,omitempty" bson:",omitempty"`
|
||||
SeqId int
|
||||
SeqId int64
|
||||
Topic string
|
||||
// Sender's user ID as string (without 'usr' prefix), could be empty.
|
||||
From string
|
||||
@@ -1256,7 +1256,7 @@ type MessageRevision struct {
|
||||
ObjHeader `bson:",inline"`
|
||||
TenantID TenantID `db:"tenant_id" json:"-" bson:"tenant_id"`
|
||||
Topic string
|
||||
SeqId int
|
||||
SeqId int64
|
||||
Version int
|
||||
Actor string
|
||||
Head KVMap `json:"Head,omitempty" bson:",omitempty"`
|
||||
@@ -1266,7 +1266,7 @@ type MessageRevision struct {
|
||||
|
||||
// MessageSaveResult describes the outcome of a message persistence attempt.
|
||||
type MessageSaveResult struct {
|
||||
SeqId int
|
||||
SeqId int64
|
||||
MarkedReadBySender bool
|
||||
Duplicate bool
|
||||
}
|
||||
@@ -1274,8 +1274,8 @@ type MessageSaveResult struct {
|
||||
// Range is a range of message SeqIDs. Low end is inclusive (closed), high end is exclusive (open): [Low, Hi).
|
||||
// If the range contains just one ID, Hi is set to 0
|
||||
type Range struct {
|
||||
Low int
|
||||
Hi int `json:"Hi,omitempty" bson:",omitempty"`
|
||||
Low int64
|
||||
Hi int64 `json:"Hi,omitempty" bson:",omitempty"`
|
||||
}
|
||||
|
||||
// RangeSorter is a helper type required by 'sort' package.
|
||||
@@ -1334,7 +1334,7 @@ func (rs RangeSorter) Normalize() RangeSorter {
|
||||
|
||||
// Convert a slice of int values to a slice of ranges.
|
||||
// The int slice must be sorted low -> high.
|
||||
func SliceToRanges(in []int) []Range {
|
||||
func SliceToRanges(in []int64) []Range {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -1390,8 +1390,8 @@ type QueryOpt struct {
|
||||
Topic string
|
||||
IfModifiedSince *time.Time
|
||||
// ID-based query parameters: Messages
|
||||
Since int
|
||||
Before int
|
||||
Since int64
|
||||
Before int64
|
||||
// Common parameter
|
||||
Limit int
|
||||
// Ranges of IDs.
|
||||
|
||||
@@ -85,6 +85,24 @@
|
||||
"conn_max_lifetime": 60,
|
||||
"sql_timeout": 10
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"addr": "64.118.137.182:56379",
|
||||
"username": "",
|
||||
"password": "HMTJzkm.h>Y+E9ep",
|
||||
"db": 0,
|
||||
"key_prefix": "im",
|
||||
"dial_timeout_ms": 2000,
|
||||
"read_timeout_ms": 2000,
|
||||
"write_timeout_ms": 2000,
|
||||
"pending_ttl_seconds": 120,
|
||||
"done_ttl_seconds": 86400,
|
||||
"failed_ttl_seconds": 600,
|
||||
"hot_message_ttl_seconds": 86400,
|
||||
"presence_ttl_seconds": 120,
|
||||
"cache_ttl_seconds": 300,
|
||||
"short_state_ttl_seconds": 600,
|
||||
"group_fanout_enabled": true
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+47
-14
@@ -45,7 +45,7 @@ type Topic struct {
|
||||
touched time.Time
|
||||
|
||||
// Server-side ID of the last data message
|
||||
lastID int
|
||||
lastID int64
|
||||
// ID of the deletion operation. Not an ID of the message.
|
||||
delID int
|
||||
|
||||
@@ -142,8 +142,8 @@ type perUserData struct {
|
||||
online int
|
||||
|
||||
// Last t.lastId reported by user through {pres} as received or read
|
||||
recvID int
|
||||
readID int
|
||||
recvID int64
|
||||
readID int64
|
||||
// ID of the latest Delete operation
|
||||
delID int
|
||||
|
||||
@@ -646,14 +646,14 @@ func (t *Topic) handleServerMsg(msg *ServerComMessage) {
|
||||
logs.Warn.Printf("topic[%s]: rejected cross-tenant server message: topic=%d message=%d", t.name, t.tenantID, msg.TenantID)
|
||||
return
|
||||
}
|
||||
// Server-generated message: {info} or {pres}.
|
||||
// Server-generated message: {data}, {info} or {pres}.
|
||||
if t.isInactive() {
|
||||
// Ignore message - the topic is paused or being deleted.
|
||||
return
|
||||
}
|
||||
if msg.Pres != nil {
|
||||
t.handlePresence(msg)
|
||||
} else if msg.Info != nil {
|
||||
} else if msg.Info != nil || msg.Data != nil {
|
||||
t.broadcastToSessions(msg)
|
||||
} else {
|
||||
// TODO(gene): maybe remove this panic.
|
||||
@@ -797,6 +797,11 @@ func (t *Topic) handleLeaveRequest(msg *ClientComMessage, sess *Session) {
|
||||
if !sess.background {
|
||||
pud.online--
|
||||
t.perUser[uid] = pud
|
||||
if t.cat == types.TopicCatGrp && pud.online == 0 {
|
||||
if err := store.MarkTopicMemberOffline(t.tenantID, t.name, uid, currentNodeID()); err != nil {
|
||||
logs.Warn.Printf("topic[%s]: failed to mark member offline in redis: %v", t.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if len(pssd.muids) > 0 {
|
||||
// UID is zero: multiplexing session is dropped altogether.
|
||||
@@ -805,6 +810,11 @@ func (t *Topic) handleLeaveRequest(msg *ClientComMessage, sess *Session) {
|
||||
pud := t.perUser[uid]
|
||||
pud.online--
|
||||
t.perUser[uid] = pud
|
||||
if t.cat == types.TopicCatGrp && pud.online == 0 {
|
||||
if err := store.MarkTopicMemberOffline(t.tenantID, t.name, uid, currentNodeID()); err != nil {
|
||||
logs.Warn.Printf("topic[%s]: failed to mark member offline in redis: %v", t.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !sess.isCluster() {
|
||||
logs.Warn.Panic("cannot determine uid: leave req", msg, sess)
|
||||
@@ -1119,8 +1129,11 @@ func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid,
|
||||
return nil
|
||||
}
|
||||
markedReadBySender := saveResult != nil && saveResult.MarkedReadBySender
|
||||
|
||||
t.lastID++
|
||||
if saveResult != nil && saveResult.SeqId > t.lastID {
|
||||
t.lastID = saveResult.SeqId
|
||||
} else {
|
||||
t.lastID++
|
||||
}
|
||||
t.touched = msg.Timestamp
|
||||
|
||||
if userFound {
|
||||
@@ -1163,6 +1176,11 @@ func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid,
|
||||
pluginMessage(t.tenantID, data.Data, plgActCreate)
|
||||
|
||||
t.broadcastToSessions(data)
|
||||
if t.cat == types.TopicCatGrp {
|
||||
if err := store.PublishGroupFanout(currentNodeID(), data); err != nil {
|
||||
logs.Warn.Printf("topic[%s]: failed to publish redis group fanout: %v", t.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// sendPush will update unread message count and send push notification.
|
||||
if pushRcpt := t.pushForData(asUid, data.Data, markedReadBySender); pushRcpt != nil {
|
||||
@@ -1332,7 +1350,7 @@ func (t *Topic) handleNoteBroadcast(msg *ClientComMessage) {
|
||||
return
|
||||
}
|
||||
|
||||
var read, recv, unread, seq int
|
||||
var read, recv, unread, seq int64
|
||||
|
||||
switch msg.Note.What {
|
||||
case "read":
|
||||
@@ -1593,8 +1611,14 @@ func (t *Topic) subscriptionReply(asChan bool, msg *ClientComMessage) error {
|
||||
// The user is online in the topic. Increment the counter if notifications are not deferred.
|
||||
if !msg.sess.background {
|
||||
userData := t.perUser[asUid]
|
||||
wasOffline := userData.online == 0
|
||||
userData.online++
|
||||
t.perUser[asUid] = userData
|
||||
if t.cat == types.TopicCatGrp && wasOffline {
|
||||
if err := store.MarkTopicMemberOnline(t.tenantID, t.name, asUid, currentNodeID()); err != nil {
|
||||
logs.Warn.Printf("topic[%s]: failed to mark member online in redis: %v", t.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if t.cat == types.TopicCatGrp && msgsub.Newsub {
|
||||
@@ -2342,7 +2366,7 @@ func (t *Topic) replyGetDesc(sess *Session, asUid types.Uid, _ bool, opts *MsgGe
|
||||
// t.delID <= pud.delID; t.readID <= t.recvID <= t.lastID
|
||||
desc.DelId = max(pud.delID, t.delID)
|
||||
desc.ReadSeqId = pud.readID
|
||||
desc.RecvSeqId = max(pud.recvID, pud.readID)
|
||||
desc.RecvSeqId = max64(pud.recvID, pud.readID)
|
||||
} else {
|
||||
// Send some sane value of touched.
|
||||
desc.TouchedAt = &t.updated
|
||||
@@ -2755,12 +2779,18 @@ func (t *Topic) replyGetSub(sess *Session, asUid types.Uid, authLevel auth.Level
|
||||
mts.Topic = with
|
||||
canViewLastSeen = imCanViewLastSeen(t.tenantID, asUid, types.ParseUserId(with))
|
||||
mts.Online = t.perSubs[with].online && !deleted && presencer
|
||||
if !deleted && presencer {
|
||||
mts.Online = redisUserOnline(t.tenantID, types.ParseUserId(with), mts.Online)
|
||||
}
|
||||
} else if strings.HasPrefix(sub.Topic, "slf") {
|
||||
mts.Topic = "slf"
|
||||
// Not reporting Online as it makes no sense for slf.
|
||||
} else {
|
||||
mts.Topic = sub.Topic
|
||||
mts.Online = t.perSubs[sub.Topic].online && !deleted && presencer
|
||||
if !deleted && presencer && strings.HasPrefix(sub.Topic, "usr") {
|
||||
mts.Online = redisUserOnline(t.tenantID, types.ParseUserId(sub.Topic), mts.Online)
|
||||
}
|
||||
}
|
||||
|
||||
if !deleted && !banned {
|
||||
@@ -2808,6 +2838,9 @@ func (t *Topic) replyGetSub(sess *Session, asUid types.Uid, authLevel auth.Level
|
||||
if t.cat == types.TopicCatGrp {
|
||||
pud := t.perUser[uid]
|
||||
mts.Online = pud.online > 0 && presencer
|
||||
if presencer {
|
||||
mts.Online = redisUserOnline(t.tenantID, uid, mts.Online)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3392,7 +3425,7 @@ func (t *Topic) replyDelMsg(sess *Session, asUid types.Uid, asChan bool, msg *Cl
|
||||
if dq.HiId == 0 {
|
||||
count++
|
||||
} else {
|
||||
count += dq.HiId - dq.LowId
|
||||
count += int(dq.HiId - dq.LowId)
|
||||
}
|
||||
|
||||
ranges = append(ranges, types.Range{Low: dq.LowId, Hi: dq.HiId})
|
||||
@@ -3440,7 +3473,7 @@ func (t *Topic) replyDelMsg(sess *Session, asUid types.Uid, asChan bool, msg *Cl
|
||||
unreadDeleted := calculateUnreadInRanges(pud.readID, t.lastID, ranges)
|
||||
if unreadDeleted > 0 {
|
||||
// Decrease unread count (negative value)
|
||||
usersUpdateUnread(t.tenantID, uid, -unreadDeleted, true)
|
||||
usersUpdateUnread(t.tenantID, uid, -int64(unreadDeleted), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4183,7 +4216,7 @@ func topicNameForUser(name string, uid types.Uid, isChan bool) string {
|
||||
// calculateUnreadInRanges calculates how many unread messages are within the given ranges.
|
||||
// unreadStart is the first unread message SeqId (readID + 1), unreadEnd is the last possible message SeqId.
|
||||
// Assumes ranges are sorted by Low ascending.
|
||||
func calculateUnreadInRanges(readID, lastID int, ranges []types.Range) int {
|
||||
func calculateUnreadInRanges(readID, lastID int64, ranges []types.Range) int {
|
||||
if readID >= lastID {
|
||||
// No unread messages
|
||||
return 0
|
||||
@@ -4207,11 +4240,11 @@ func calculateUnreadInRanges(readID, lastID int, ranges []types.Range) int {
|
||||
}
|
||||
|
||||
// Find intersection of [unreadStart, unreadEnd] and [rangeStart, rangeEnd)
|
||||
intersectionStart := max(unreadStart, rangeStart)
|
||||
intersectionStart := max64(unreadStart, rangeStart)
|
||||
intersectionEnd := min(unreadEnd+1, rangeEnd) // +1 because unreadEnd is inclusive
|
||||
|
||||
if intersectionStart < intersectionEnd {
|
||||
count += intersectionEnd - intersectionStart
|
||||
count += int(intersectionEnd - intersectionStart)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,9 @@ func (t *Topic) runProxy(hub *Hub) {
|
||||
}
|
||||
|
||||
case msg := <-t.serverMsg:
|
||||
if msg.Info != nil || msg.Pres != nil {
|
||||
if msg.Data != nil {
|
||||
t.broadcastToSessions(msg)
|
||||
} else if msg.Info != nil || msg.Pres != nil {
|
||||
globals.cluster.routeToTopicIntraCluster(t.name, msg, msg.sess)
|
||||
} else {
|
||||
// FIXME: should something be done here?
|
||||
|
||||
+12
-12
@@ -777,7 +777,7 @@ func TestHandleBroadcastDataDuplicateClientMessage(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("User1: expected ctrl params map, received %#v", ack.Ctrl.Params)
|
||||
}
|
||||
if seq, ok := params["seq"].(int); !ok || seq != 4 {
|
||||
if seq, ok := params["seq"].(int64); !ok || seq != 4 {
|
||||
t.Errorf("User1: expected duplicate ack seq 4, received %#v", params["seq"])
|
||||
}
|
||||
if len(helper.results[1].messages) != 0 {
|
||||
@@ -949,7 +949,7 @@ func TestHandleBroadcastDataInactiveTopic(t *testing.T) {
|
||||
func TestHandleBroadcastInfoP2P(t *testing.T) {
|
||||
topicName := "usrP2P"
|
||||
numUsers := 2
|
||||
readId := 8
|
||||
readId := int64(8)
|
||||
helper := TopicTestHelper{}
|
||||
helper.setUp(t, numUsers, types.TopicCatP2P, topicName, true)
|
||||
defer helper.tearDown()
|
||||
@@ -1065,7 +1065,7 @@ func TestHandleBroadcastInfoBogusNotification(t *testing.T) {
|
||||
// Pretend we have 10 messages.
|
||||
helper.topic.lastID = 10
|
||||
// uid1 notifies uid2 that uid1 has read messages up to seqid 11.
|
||||
readId := 11
|
||||
readId := int64(11)
|
||||
from := helper.uids[0]
|
||||
to := helper.uids[1]
|
||||
|
||||
@@ -1113,7 +1113,7 @@ func TestHandleBroadcastInfoFilterOutRecvWithoutRPermission(t *testing.T) {
|
||||
// Pretend we have 10 messages.
|
||||
helper.topic.lastID = 10
|
||||
// uid1 notifies uid2 that uid1 has read messages up to seqid 11.
|
||||
readId := 8
|
||||
readId := int64(8)
|
||||
from := helper.uids[0]
|
||||
to := helper.uids[1]
|
||||
|
||||
@@ -1166,7 +1166,7 @@ func TestHandleBroadcastInfoFilterOutKpWithoutWPermission(t *testing.T) {
|
||||
// Pretend we have 10 messages.
|
||||
helper.topic.lastID = 10
|
||||
// uid1 notifies uid2 that uid1 has read messages up to seqid 11.
|
||||
readId := 8
|
||||
readId := int64(8)
|
||||
from := helper.uids[0]
|
||||
to := helper.uids[1]
|
||||
|
||||
@@ -1219,7 +1219,7 @@ func TestHandleBroadcastInfoDuplicatedRead(t *testing.T) {
|
||||
// Pretend we have 10 messages.
|
||||
helper.topic.lastID = 10
|
||||
// uid1 notifies uid2 that uid1 has read messages up to seqid 11.
|
||||
readId := 8
|
||||
readId := int64(8)
|
||||
from := helper.uids[0]
|
||||
to := helper.uids[1]
|
||||
|
||||
@@ -1272,7 +1272,7 @@ func TestHandleBroadcastInfoDbError(t *testing.T) {
|
||||
// Pretend we have 10 messages.
|
||||
helper.topic.lastID = 10
|
||||
// uid1 notifies uid2 that uid1 has read messages up to seqid 11.
|
||||
readId := 8
|
||||
readId := int64(8)
|
||||
from := helper.uids[0]
|
||||
to := helper.uids[1]
|
||||
|
||||
@@ -1326,7 +1326,7 @@ func TestHandleBroadcastInfoInvalidChannelAccess(t *testing.T) {
|
||||
// Pretend we have 10 messages.
|
||||
helper.topic.lastID = 10
|
||||
// uid1 notifies uid2 that uid1 has read messages up to seqid 11.
|
||||
readId := 8
|
||||
readId := int64(8)
|
||||
from := helper.uids[0]
|
||||
for i := 1; i < numUsers; i++ {
|
||||
uid := helper.uids[i]
|
||||
@@ -1380,7 +1380,7 @@ func TestHandleBroadcastInfoChannelProcessing(t *testing.T) {
|
||||
// Pretend we have 10 messages.
|
||||
helper.topic.lastID = 10
|
||||
// uid1 notifies uid2 that uid1 has read messages up to seqid 11.
|
||||
readId := 8
|
||||
readId := int64(8)
|
||||
from := helper.uids[0]
|
||||
for i := 1; i < numUsers; i++ {
|
||||
uid := helper.uids[i]
|
||||
@@ -3222,7 +3222,7 @@ func TestHandleBroadcastInfoChannelWithMultipleReaders(t *testing.T) {
|
||||
defer helper.tearDown()
|
||||
helper.topic.lastID = 15
|
||||
|
||||
readId := 12
|
||||
readId := int64(12)
|
||||
from := helper.uids[0]
|
||||
|
||||
// Set up multiple channel readers
|
||||
@@ -3606,8 +3606,8 @@ func TestReplyDelMsgUpdatesUnreadCounters(t *testing.T) {
|
||||
func TestCalculateUnreadInRanges(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
readID int
|
||||
lastID int
|
||||
readID int64
|
||||
lastID int64
|
||||
ranges []types.Range
|
||||
expected int
|
||||
}{
|
||||
|
||||
+5
-2
@@ -1069,12 +1069,12 @@ func usersShutdown() {
|
||||
}
|
||||
}
|
||||
|
||||
func usersUpdateUnread(tenantID types.TenantID, uid types.Uid, val int, inc bool) {
|
||||
func usersUpdateUnread(tenantID types.TenantID, uid types.Uid, val int64, inc bool) {
|
||||
if globals.usersUpdate == nil || (val == 0 && inc) {
|
||||
return
|
||||
}
|
||||
|
||||
upd := &UserCacheReq{TenantID: tenantID, UserId: uid, Unread: val, Inc: inc}
|
||||
upd := &UserCacheReq{TenantID: tenantID, UserId: uid, Unread: int(val), Inc: inc}
|
||||
if globals.cluster.isRemoteTopic(types.TopicKey{TenantID: tenantID, Topic: uid.UserId()}) {
|
||||
// Send request to remote node which owns the user.
|
||||
globals.cluster.routeUserReq(upd)
|
||||
@@ -1240,6 +1240,7 @@ func userUpdater() {
|
||||
|
||||
usersCache[key] = uce
|
||||
counts[uid] = uce.unread
|
||||
store.CacheUserUnreadCount(tenantID, uid, uce.unread)
|
||||
}
|
||||
|
||||
if len(dbPending) > 0 {
|
||||
@@ -1287,6 +1288,7 @@ func userUpdater() {
|
||||
}
|
||||
uce.unread = count
|
||||
usersCache[key] = uce
|
||||
store.CacheUserUnreadCount(io.tenantID, uid, count)
|
||||
} else {
|
||||
logs.Warn.Println("users: missing users cache entry after IO completion, uid", uid)
|
||||
}
|
||||
@@ -1416,6 +1418,7 @@ func userUpdater() {
|
||||
if upd.Gone {
|
||||
// User is being deleted. Don't care if there is a record.
|
||||
delete(usersCache, types.UserKey{TenantID: upd.TenantID, User: upd.UserId})
|
||||
store.DeleteCachedUserUnreadCount(upd.TenantID, upd.UserId)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -694,6 +694,13 @@ func max(a, b int) int {
|
||||
return b
|
||||
}
|
||||
|
||||
func max64(a, b int64) int64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Truncate string if it's too long. Used in logging.
|
||||
func truncateStringIfTooLong(s string) string {
|
||||
if len(s) <= 1024 {
|
||||
|
||||
Reference in New Issue
Block a user