feat: 接入消息加密与实名认证能力

This commit is contained in:
richjohnrambo
2026-09-21 09:53:39 +08:00
parent 841c0c77cd
commit e3b93113f1
33 changed files with 1747 additions and 99 deletions
+6
View File
@@ -228,6 +228,12 @@ type ThirdPartyConfigAdapter interface {
ThirdPartyConfig(group string, keys ...string) (map[string]string, error)
}
// RealNamePolicyConfigAdapter reads tenant-level real-name review policy from
// the control database.
type RealNamePolicyConfigAdapter interface {
RealNamePolicy(t.TenantID) (*t.RealNamePolicy, error)
}
// TenantEntryAdapter stores public tenant entry data before a user is authenticated.
type TenantEntryAdapter interface {
PublicUploadCreate(*t.PublicUpload) (*t.PublicUpload, error)
+8 -3
View File
@@ -594,6 +594,11 @@ func createTenantRegistry(tx *sql.Tx, registryDB, businessDB string) error {
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
realname_review_policy TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Real-name review policy: 0 none, 1 auto then manual',
realname_provider_mode VARCHAR(32) NOT NULL DEFAULT 'id_card_2factor' COMMENT 'id_card_2factor or smart_verify',
realname_required TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Require real-name verification',
realname_allow_skip TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Allow skipping real-name reminder',
realname_remind_after VARCHAR(32) NOT NULL DEFAULT '24h' COMMENT 'Real-name reminder interval',
aliyun_moderation_enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Enable Aliyun content moderation',
local_moderation_enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Enable local content moderation',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
@@ -3309,9 +3314,9 @@ func (a *adapter) MessageSave(msg *t.Message) error {
// Using a sequential ID provided by the database.
clientMsgID := sql.NullString{String: msg.ClientMsgID, Valid: msg.ClientMsgID != ""}
res, err := a.db.ExecContext(ctx,
"INSERT INTO im_messages(createdAt,updatedAt,seqid,topic,`from`,client_msg_id,version,head,content) VALUES(?,?,?,?,?,?,?,?,?)",
"INSERT INTO im_messages(createdAt,updatedAt,seqid,topic,`from`,client_msg_id,version,head,content,content_nonce,key_version) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
msg.CreatedAt, msg.UpdatedAt, msg.SeqId, msg.Topic,
store.DecodeUid(t.ParseUid(msg.From)), clientMsgID, messageVersion(msg.Version), msg.Head, common.ToJSON(msg.Content))
store.DecodeUid(t.ParseUid(msg.From)), clientMsgID, messageVersion(msg.Version), msg.Head, common.ToJSON(msg.Content), msg.ContentNonce, msg.KeyVersion)
if isDupe(err) {
return t.ErrDuplicate
}
@@ -3364,7 +3369,7 @@ func (a *adapter) MessageGetAll(topic string, forUser t.Uid, opts *t.QueryOpt) (
rows, err := a.db.QueryxContext(
ctx,
"SELECT m.createdat,m.updatedat,m.deletedat,m.delid,m.seqid,m.topic,m.`from`,m.head,m.content"+
"SELECT m.createdat,m.updatedat,m.deletedat,m.delid,m.seqid,m.topic,m.`from`,m.head,m.content,m.content_nonce,m.key_version"+
" FROM im_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.delid=0 AND m.topic=? "+seqIdConstraint+" AND d.deletedfor IS NULL"+
+2 -1
View File
@@ -97,7 +97,8 @@ func ensureGroupMessageShard(exec sqlExecutor, start time.Time, status string) e
message_id BIGINT NOT NULL, 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,
content JSON, content_nonce VARBINARY(12), key_version INT NOT NULL DEFAULT 0,
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,
+2 -1
View File
@@ -27,7 +27,8 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error {
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,
content JSON, content_nonce VARBINARY(12), key_version INT NOT NULL DEFAULT 0,
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,
+57
View File
@@ -0,0 +1,57 @@
//go:build mysql
// +build mysql
package mysql
import (
"database/sql"
"errors"
t "github.com/tinode/chat/server/store/types"
)
func (a *adapter) RealNamePolicy(tenantID t.TenantID) (*t.RealNamePolicy, error) {
if !tenantID.IsValid() {
return nil, t.ErrMalformed
}
db := a.registryDB
if db == nil {
db = a.db
}
if db == nil {
return nil, t.ErrInternal
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
var policy t.RealNamePolicy
err := db.GetContext(ctx, &policy, `SELECT tenant_id, realname_review_policy, realname_provider_mode,
realname_required, realname_allow_skip, realname_remind_after
FROM im_tenant_config WHERE tenant_id=? LIMIT 1`, tenantID)
if errors.Is(err, sql.ErrNoRows) {
policy = *defaultRealNamePolicy(tenantID)
return &policy, nil
}
if isUnknownColumn(err) {
policy = *defaultRealNamePolicy(tenantID)
return &policy, nil
}
if err != nil {
return nil, err
}
policy.Normalize()
return &policy, nil
}
func defaultRealNamePolicy(tenantID t.TenantID) *t.RealNamePolicy {
policy := &t.RealNamePolicy{
TenantID: tenantID,
ReviewPolicy: t.RealNameReviewPolicyNone,
ProviderMode: "id_card_2factor",
AllowSkip: true,
RemindAfter: "24h",
}
policy.Normalize()
return policy
}
+39 -24
View File
@@ -2065,8 +2065,8 @@ func (a *adapter) TenantMessageSave(id t.TenantID, msg *t.Message) error {
}
clientMsgID := sql.NullString{String: msg.ClientMsgID, Valid: msg.ClientMsgID != ""}
res, err := a.db.ExecContext(ctx,
"INSERT INTO im_messages(tenant_id,createdat,updatedat,seqid,topic,`from`,client_msg_id,version,head,content) VALUES(?,?,?,?,?,?,?,?,?,?)",
id, msg.CreatedAt, msg.UpdatedAt, msg.SeqId, msg.Topic, decodeUID(t.ParseUid(msg.From)), clientMsgID, messageVersion(msg.Version), msg.Head, common.ToJSON(msg.Content))
"INSERT INTO im_messages(tenant_id,createdat,updatedat,seqid,topic,`from`,client_msg_id,version,head,content,content_nonce,key_version) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
id, msg.CreatedAt, msg.UpdatedAt, msg.SeqId, msg.Topic, decodeUID(t.ParseUid(msg.From)), clientMsgID, messageVersion(msg.Version), msg.Head, common.ToJSON(msg.Content), msg.ContentNonce, msg.KeyVersion)
if isDupe(err) {
return t.ErrDuplicate
}
@@ -2139,12 +2139,12 @@ func (a *adapter) TenantGroupMessageSave(id t.TenantID, msg *t.Message) error {
}
query := fmt.Sprintf(`INSERT INTO %s
(message_id,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(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, table)
file_count,content_size,version,created_at,updated_at,content_nonce,key_version)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, table)
_, err = tx.ExecContext(ctx, query,
messageID, 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)
0, len(contentJSON), messageVersion(msg.Version), msg.CreatedAt, msg.UpdatedAt, msg.ContentNonce, msg.KeyVersion)
if isDupe(err) {
return t.ErrDuplicate
}
@@ -2191,7 +2191,7 @@ func (a *adapter) TenantMessageGetByClientID(id t.TenantID, from t.Uid, clientMs
RecalledBy sql.NullInt64 `db:"recalled_by"`
}{}
err := a.db.QueryRowxContext(ctx,
"SELECT id,createdat,updatedat,deletedat,delid,seqid,topic,`from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content "+
"SELECT id,createdat,updatedat,deletedat,delid,seqid,topic,`from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content,content_nonce,key_version "+
"FROM im_messages WHERE `from`=? AND client_msg_id=?",
decodeUID(from), clientMsgID).StructScan(&row)
if err == sql.ErrNoRows {
@@ -2246,7 +2246,7 @@ func (a *adapter) TenantGroupMessageGetByClientID(id t.TenantID, topic string, f
t.Message
RecalledBy sql.NullInt64 `db:"recalled_by"`
}{}
query := fmt.Sprintf("SELECT %s AS id,created_at AS createdat,updated_at AS updatedat,deleted_at AS deletedat,0 AS delid,seqid,topic,from_user AS `from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content,'%s' AS shard_month FROM %s WHERE topic=? AND from_user=? AND client_msg_id=?", idColumn, month, table)
query := fmt.Sprintf("SELECT %s AS id,created_at AS createdat,updated_at AS updatedat,deleted_at AS deletedat,0 AS delid,seqid,topic,from_user AS `from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content,content_nonce,key_version,'%s' AS shard_month FROM %s WHERE topic=? AND from_user=? AND client_msg_id=?", idColumn, month, table)
err = a.db.QueryRowxContext(ctx, query, topic, decodeUID(from), clientMsgID).StructScan(&row)
if err == nil {
msg := normalizeGroupMessage(id, row.Message, row.RecalledBy)
@@ -2442,13 +2442,14 @@ func (a *adapter) TenantMessageEdit(id t.TenantID, topic string, seq int64, acto
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 {
"INSERT INTO im_message_revisions(createdat,updatedat,topic,seqid,version,actor,head,content,content_nonce,key_version,reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
editedAt, editedAt, topic, seq, msg.Version, decodeUID(actor), msg.Head, common.ToJSON(msg.Content), msg.ContentNonce, msg.KeyVersion, strings.TrimSpace(reason)); err != nil {
return nil, err
}
storedContent, nonce, keyVersion := messageStoredContent(content)
_, err = tx.ExecContext(ctx,
"UPDATE im_messages SET updatedat=?,version=version+1,head=?,content=? WHERE topic=? AND seqid=? AND version=?",
editedAt, head, common.ToJSON(content), topic, seq, expectedVersion)
"UPDATE im_messages SET updatedat=?,version=version+1,head=?,content=?,content_nonce=?,key_version=? WHERE topic=? AND seqid=? AND version=?",
editedAt, head, common.ToJSON(storedContent), nonce, keyVersion, topic, seq, expectedVersion)
if err != nil {
return nil, err
}
@@ -2496,14 +2497,15 @@ func (a *adapter) TenantGroupMessageEdit(id t.TenantID, topic string, seq int64,
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 {
"INSERT INTO im_message_revisions(createdat,updatedat,topic,seqid,version,actor,head,content,content_nonce,key_version,reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
editedAt, editedAt, topic, seq, msg.Version, decodeUID(actor), msg.Head, common.ToJSON(msg.Content), msg.ContentNonce, msg.KeyVersion, strings.TrimSpace(reason)); err != nil {
return nil, err
}
contentJSON := common.ToJSON(content)
storedContent, nonce, keyVersion := messageStoredContent(content)
contentJSON := common.ToJSON(storedContent)
_, err = tx.ExecContext(ctx, fmt.Sprintf(
"UPDATE %s SET updated_at=?,version=version+1,msg_type=?,head=?,content=?,content_size=? WHERE topic=? AND seqid=? AND version=?", messageTable),
editedAt, messageHeadType(head), head, contentJSON, len(contentJSON), topic, seq, expectedVersion)
"UPDATE %s SET updated_at=?,version=version+1,msg_type=?,head=?,content=?,content_nonce=?,key_version=?,content_size=? WHERE topic=? AND seqid=? AND version=?", messageTable),
editedAt, messageHeadType(head), head, contentJSON, nonce, keyVersion, len(contentJSON), topic, seq, expectedVersion)
if err != nil {
return nil, err
}
@@ -2561,7 +2563,7 @@ func (a *adapter) TenantMessageGetAll(id t.TenantID, topic string, uid t.Uid, op
defer cancel()
}
rows, err := a.db.QueryxContext(ctx,
"SELECT m.tenant_id,m.createdat,m.updatedat,m.deletedat,m.delid,m.seqid,m.topic,m.`from`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content "+
"SELECT m.tenant_id,m.createdat,m.updatedat,m.deletedat,m.delid,m.seqid,m.topic,m.`from`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content,m.content_nonce,m.key_version "+
"FROM im_messages AS m LEFT JOIN im_dellog AS d ON d.tenant_id=m.tenant_id AND d.topic=m.topic "+
"AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE m.tenant_id=? AND m.delid=0 AND m.topic=? "+
seq+" AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?",
@@ -2640,7 +2642,7 @@ func (a *adapter) TenantGroupMessageGetAll(id t.TenantID, topic string, uid t.Ui
args := []any{store.DecodeUid(uid), topic}
args = append(args, seqArgs...)
args = append(args, limit-len(msgs))
query := fmt.Sprintf("SELECT m.%s AS 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`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content,'%s' AS shard_month FROM %s AS m LEFT JOIN im_dellog AS d ON d.topic COLLATE utf8mb4_unicode_ci=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE m.status!='deleted' AND m.topic=? %s AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?", idColumn, month, table, seqClause)
query := fmt.Sprintf("SELECT m.%s AS 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`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content,m.content_nonce,m.key_version,'%s' AS shard_month FROM %s AS m LEFT JOIN im_dellog AS d ON d.topic COLLATE utf8mb4_unicode_ci=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE m.status!='deleted' AND m.topic=? %s AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?", idColumn, month, table, seqClause)
rows, queryErr := a.db.QueryxContext(ctx, query, args...)
if queryErr != nil {
return nil, queryErr
@@ -2715,7 +2717,7 @@ func (a *adapter) TenantMessageSearch(id t.TenantID, topic string, uid t.Uid, qu
queryArgs := append([]any{decodeUID(uid)}, args...)
queryArgs = append(queryArgs, scanLimit)
rows, err := a.db.QueryxContext(ctx,
"SELECT m.createdat,m.updatedat,m.deletedat,m.delid,m.seqid,m.topic,m.`from`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content "+
"SELECT m.createdat,m.updatedat,m.deletedat,m.delid,m.seqid,m.topic,m.`from`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content,m.content_nonce,m.key_version "+
"FROM im_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 "+strings.Join(where, " AND ")+" AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?",
queryArgs...)
@@ -2796,7 +2798,7 @@ func (a *adapter) tenantGroupMessageSearch(id t.TenantID, topic string, uid t.Ui
}
queryArgs := append([]any{store.DecodeUid(uid)}, args...)
queryArgs = append(queryArgs, scanLimit-len(seen))
query := fmt.Sprintf("SELECT m.%s AS 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`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content,'%s' AS shard_month FROM %s AS m LEFT JOIN im_dellog AS d ON d.topic COLLATE utf8mb4_unicode_ci=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE %s AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?", idColumn, month, table, strings.Join(where, " AND "))
query := fmt.Sprintf("SELECT m.%s AS 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`,IFNULL(m.client_msg_id,'') AS client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content,m.content_nonce,m.key_version,'%s' AS shard_month FROM %s AS m LEFT JOIN im_dellog AS d ON d.topic COLLATE utf8mb4_unicode_ci=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE %s AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?", idColumn, month, table, strings.Join(where, " AND "))
rows, queryErr := a.db.QueryxContext(ctx, query, queryArgs...)
if queryErr != nil {
return nil, "", queryErr
@@ -2863,6 +2865,10 @@ func messageSearchScanLimit(limit int, queryText string) int {
}
func messageMatchesSearch(msg t.Message, queryText string) bool {
if msg.KeyVersion > 0 {
// Store decrypts and filters encrypted messages after the adapter returns.
return true
}
queryText = normalizeMessageSearchText(queryText)
if queryText == "" {
return true
@@ -3144,8 +3150,10 @@ func (a *adapter) TenantCallFinish(id t.TenantID, callID string, seq int64, stat
_, err := a.db.ExecContext(ctx,
`UPDATE im_call_records
SET status=?,end_reason=?,duration_ms=?,finished_at=?,updated_at=?
WHERE call_id=? AND seq_id=?`,
status, status, durationMS, finishedAt, finishedAt, callID, seq)
WHERE call_id=? AND seq_id=? AND finished_at IS NULL
AND NOT (status=? AND ? IN (?,?))`,
status, status, durationMS, finishedAt, finishedAt, callID, seq,
t.CallStatusAccepted, status, t.CallStatusMissed, t.CallStatusDeclined)
return err
}
@@ -4074,7 +4082,7 @@ func tenantMessageGetOne(q sqlx.QueryerContext, ctx context.Context, tenantID t.
RecalledBy sql.NullInt64 `db:"recalled_by"`
}{}
err := sqlx.GetContext(ctx, q, &row,
"SELECT id,createdat,updatedat,deletedat,delid,seqid,topic,`from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content "+
"SELECT id,createdat,updatedat,deletedat,delid,seqid,topic,`from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content,content_nonce,key_version "+
"FROM im_messages WHERE topic=? AND seqid=?"+suffix,
topic, seq)
if err != nil {
@@ -4119,7 +4127,7 @@ func tenantGroupMessageGetOneWithTable(q sqlx.QueryerContext, ctx context.Contex
t.Message
RecalledBy sql.NullInt64 `db:"recalled_by"`
}{}
query := fmt.Sprintf("SELECT %s AS id,created_at AS createdat,updated_at AS updatedat,deleted_at AS deletedat,0 AS delid,seqid,topic,from_user AS `from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content,'%s' AS shard_month FROM %s WHERE topic=? AND seqid=?%s", idColumn, month, table, suffix)
query := fmt.Sprintf("SELECT %s AS id,created_at AS createdat,updated_at AS updatedat,deleted_at AS deletedat,0 AS delid,seqid,topic,from_user AS `from`,IFNULL(client_msg_id,'') AS client_msg_id,version,recalled_at,recalled_by,head,content,content_nonce,key_version,'%s' AS shard_month FROM %s WHERE topic=? AND seqid=?%s", idColumn, month, table, suffix)
err = sqlx.GetContext(ctx, q, &row, query, topic, seq)
if err == nil {
msg := normalizeGroupMessage(tenantID, row.Message, row.RecalledBy)
@@ -4139,6 +4147,13 @@ func messageVersion(version int) int {
return version
}
func messageStoredContent(value any) (content any, nonce []byte, keyVersion int) {
if stored, ok := value.(t.StoredMessageContent); ok {
return stored.Content, stored.Nonce, stored.KeyVersion
}
return value, nil, 0
}
func kvMapFromJSON(value any) t.KVMap {
switch parsed := common.FromJSON(value).(type) {
case map[string]any: