feat: add redis cache and fanout support
This commit is contained in:
+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)
|
||||
|
||||
Reference in New Issue
Block a user