feat: add message mutation sync

This commit is contained in:
richjohnrambo
2026-09-15 15:54:52 +08:00
parent 55afd976c9
commit bc27a5c574
12 changed files with 949 additions and 13 deletions
+3 -2
View File
@@ -2747,7 +2747,7 @@ func (a *adapter) TopicGet(topic string) (*t.Topic, error) {
// Fetch topic by name
var tt = new(t.Topic)
if err := a.db.GetContext(ctx, tt,
"SELECT createdat,updatedat,state,stateat,touchedat,name AS id,usebt,access,owner,seqid,delid,subcnt,public,trusted,tags,aux "+
"SELECT createdat,updatedat,state,stateat,touchedat,name AS id,usebt,access,owner,seqid,delid,subcnt,IFNULL(mutation_version,0) AS mutation_version,public,trusted,tags,aux "+
"FROM im_topics WHERE name=?", topic); err != nil {
if err == sql.ErrNoRows {
// Nothing found - clear the error
@@ -2901,7 +2901,7 @@ func (a *adapter) TopicsForUser(uid t.Uid, keepDeleted bool, opts *t.QueryOpt) (
// Fetch grp topics and join to subscriptions.
if len(topq) > 0 {
q = "SELECT updatedat,state,touchedat,name AS id,usebt,access,seqid,delid,subcnt,public,trusted " +
q = "SELECT updatedat,state,touchedat,name AS id,usebt,access,seqid,delid,subcnt,IFNULL(mutation_version,0) AS mutation_version,public,trusted " +
"FROM im_topics WHERE name IN (?)"
q, args, _ = sqlx.In(q, topq)
@@ -2943,6 +2943,7 @@ func (a *adapter) TopicsForUser(uid t.Uid, keepDeleted bool, opts *t.QueryOpt) (
sub.SetState(top.State)
sub.SetTouchedAt(top.TouchedAt)
sub.SetSeqId(top.SeqId)
sub.Private = ensurePrivateMutationVersion(sub.Private, top.MutationVersion)
if t.GetTopicCat(sub.Topic) == t.TopicCatGrp {
sub.SetSubCnt(top.SubCnt)
sub.SetPublic(common.FromJSON(top.Public))
+201 -2
View File
@@ -172,6 +172,15 @@ func ensureP2PPrivatePeerRemark(private any, peerID, remark string) any {
return data
}
func ensurePrivateMutationVersion(private any, version int64) any {
data, _ := private.(map[string]any)
if data == nil {
data = map[string]any{}
}
data["mutation_version"] = version
return data
}
func clearProfilePublicFields(public map[string]any) {
for _, key := range []string{"display_name", "nickname", "fn", "name", "avatar_file_id", "photo", "avatar", "signature", "gender", "region", "public_account"} {
delete(public, key)
@@ -309,6 +318,17 @@ func messageHeadBool(head t.KVMap, key string) bool {
}
}
func (a *adapter) tenantTopicBumpMutationVersionTx(ctx context.Context, tx *sqlx.Tx, id t.TenantID, topic string, at time.Time) error {
if a.tenantScopedDB {
_, err := tx.ExecContext(ctx, "UPDATE im_topics SET mutation_version=mutation_version+1,updatedat=?,touchedat=? WHERE name=?",
at, at, topic)
return err
}
_, err := tx.ExecContext(ctx, "UPDATE im_topics SET mutation_version=mutation_version+1,updatedat=?,touchedat=? WHERE tenant_id=? AND name=?",
at, at, id, topic)
return err
}
func stampTenantFile(id t.TenantID, file *t.FileDef) *t.FileDef {
if file != nil {
file.TenantID = id
@@ -888,7 +908,7 @@ func (a *adapter) TenantTopicGet(id t.TenantID, topic string) (*t.Topic, error)
}
var top t.Topic
err = a.db.GetContext(ctx, &top,
"SELECT tenant_id,createdat,updatedat,state,stateat,touchedat,name AS id,usebt,access,owner,seqid,delid,subcnt,public,trusted,tags,aux "+
"SELECT tenant_id,createdat,updatedat,state,stateat,touchedat,name AS id,usebt,access,owner,seqid,delid,subcnt,IFNULL(mutation_version,0) AS mutation_version,public,trusted,tags,aux "+
"FROM im_topics WHERE tenant_id=? AND name=?", id, topic)
if err == sql.ErrNoRows {
return nil, nil
@@ -1013,7 +1033,7 @@ func (a *adapter) TenantTopicsForUser(id t.TenantID, uid t.Uid, keepDeleted bool
return nil, err
}
if len(topq) > 0 {
q = "SELECT tenant_id,updatedat,state,touchedat,name AS id,usebt,access,seqid,delid,subcnt,public,trusted FROM im_topics WHERE tenant_id=? AND name IN (?)"
q = "SELECT tenant_id,updatedat,state,touchedat,name AS id,usebt,access,seqid,delid,subcnt,IFNULL(mutation_version,0) AS mutation_version,public,trusted FROM im_topics WHERE tenant_id=? AND name IN (?)"
q, args, _ = sqlx.In(q, id, topq)
if !keepDeleted {
q += " AND state!=?"
@@ -1042,6 +1062,7 @@ func (a *adapter) TenantTopicsForUser(id t.TenantID, uid t.Uid, keepDeleted bool
sub.SetState(top.State)
sub.SetTouchedAt(top.TouchedAt)
sub.SetSeqId(top.SeqId)
sub.Private = ensurePrivateMutationVersion(sub.Private, top.MutationVersion)
if t.GetTopicCat(sub.Topic) == t.TopicCatGrp {
sub.SetSubCnt(top.SubCnt)
sub.SetPublic(common.FromJSON(top.Public))
@@ -1575,6 +1596,49 @@ func (a *adapter) TenantTopicUpdate(id t.TenantID, topic string, update map[stri
return tx.Commit()
}
func (a *adapter) TenantTopicBumpMutationVersion(id t.TenantID, topic string, at time.Time) (int64, error) {
tenantAdp, err := a.routedTenantAdapter(id)
if err != nil || topic == "" {
return 0, t.ErrMalformed
}
if tenantAdp != a {
return tenantAdp.TenantTopicBumpMutationVersion(id, topic, at)
}
if at.IsZero() {
at = time.Now().UTC().Round(time.Millisecond)
}
ctx, cancel := a.getContextForTx()
if cancel != nil {
defer cancel()
}
tx, err := a.db.BeginTxx(ctx, nil)
if err != nil {
return 0, err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
if err = a.tenantTopicBumpMutationVersionTx(ctx, tx, id, topic, at); err != nil {
return 0, err
}
var version int64
query := "SELECT mutation_version FROM im_topics WHERE name=?"
args := []any{topic}
if !a.tenantScopedDB {
query = "SELECT mutation_version FROM im_topics WHERE tenant_id=? AND name=?"
args = []any{id, topic}
}
if err = tx.GetContext(ctx, &version, query, args...); err != nil {
return 0, err
}
if err = tx.Commit(); err != nil {
return 0, err
}
return version, nil
}
func (a *adapter) TenantTopicOwnerChange(id t.TenantID, topic string, owner t.Uid) error {
tenantAdp, err := a.routedTenantAdapter(id)
if err != nil || topic == "" {
@@ -2173,6 +2237,9 @@ func (a *adapter) TenantMessageRecall(id t.TenantID, topic string, seq int64, ac
if err != nil {
return nil, err
}
if err = a.tenantTopicBumpMutationVersionTx(ctx, tx, id, topic, recalledAt); err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
@@ -2219,6 +2286,9 @@ func (a *adapter) TenantGroupMessageRecall(id t.TenantID, topic string, seq int6
if err != nil {
return nil, err
}
if err = a.tenantTopicBumpMutationVersionTx(ctx, tx, id, topic, recalledAt); err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
@@ -2272,6 +2342,9 @@ func (a *adapter) TenantMessageEdit(id t.TenantID, topic string, seq int64, acto
if err != nil {
return nil, err
}
if err = a.tenantTopicBumpMutationVersionTx(ctx, tx, id, topic, editedAt); err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
@@ -2324,6 +2397,9 @@ func (a *adapter) TenantGroupMessageEdit(id t.TenantID, topic string, seq int64,
if err != nil {
return nil, err
}
if err = a.tenantTopicBumpMutationVersionTx(ctx, tx, id, topic, editedAt); err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
@@ -2601,6 +2677,129 @@ func (a *adapter) tenantGroupMessageSearch(id t.TenantID, topic string, uid t.Ui
return msgs, nextCursor, nil
}
func (a *adapter) TenantMessageMutations(id t.TenantID, topic string, uid t.Uid, sinceVersion int64, limit int) ([]t.MessageMutation, string, bool, int64, error) {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)
if err != nil {
return nil, "", false, 0, err
}
if tenantAdp != a {
return tenantAdp.TenantMessageMutations(id, topic, uid, sinceVersion, limit)
}
}
if err := validTenant(id); err != nil || topic == "" || uid.IsZero() || sinceVersion < 0 {
return nil, "", false, 0, t.ErrMalformed
}
if limit <= 0 || limit > 100 {
limit = 50
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
var currentVersion int64
if a.tenantScopedDB {
if err := a.db.GetContext(ctx, &currentVersion, "SELECT IFNULL(mutation_version,0) FROM im_topics WHERE name=?", topic); err != nil {
if err == sql.ErrNoRows {
return nil, "", false, 0, t.ErrNotFound
}
return nil, "", false, 0, err
}
} else {
if err := a.db.GetContext(ctx, &currentVersion, "SELECT IFNULL(mutation_version,0) FROM im_topics WHERE tenant_id=? AND name=?", id, topic); err != nil {
if err == sql.ErrNoRows {
return nil, "", false, 0, t.ErrNotFound
}
return nil, "", false, 0, err
}
}
if sinceVersion >= currentVersion {
return []t.MessageMutation{}, strconv.FormatInt(currentVersion, 10), false, currentVersion, nil
}
if t.GetTopicCat(topic) == t.TopicCatGrp {
return a.tenantGroupMessageMutations(ctx, id, topic, uid, limit, currentVersion)
}
return a.tenantP2PMessageMutations(ctx, id, topic, uid, limit, currentVersion)
}
func (a *adapter) tenantP2PMessageMutations(ctx context.Context, id t.TenantID, topic string, uid t.Uid, limit int, currentVersion int64) ([]t.MessageMutation, string, bool, int64, error) {
rows, err := a.db.QueryxContext(ctx,
"SELECT m.seqid,m.version,m.updatedat AS mutated_at,m.recalled_at,m.recalled_by,rev.actor "+
"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=? "+
"LEFT JOIN im_message_revisions AS rev ON rev.topic=m.topic AND rev.seqid=m.seqid AND rev.version=m.version-1 "+
"WHERE m.topic=? AND d.deletedfor IS NULL AND (m.version>1 OR m.recalled_at IS NOT NULL OR m.deletedat IS NOT NULL) "+
"ORDER BY m.updatedat DESC,m.seqid DESC LIMIT ?",
decodeUID(uid), topic, limit+1)
if err != nil {
return nil, "", false, 0, err
}
defer rows.Close()
return scanTenantMessageMutations(rows, limit, currentVersion)
}
func (a *adapter) tenantGroupMessageMutations(ctx context.Context, id t.TenantID, topic string, uid t.Uid, limit int, currentVersion int64) ([]t.MessageMutation, string, bool, int64, error) {
rows, err := a.db.QueryxContext(ctx,
"SELECT m.seqid,m.version,m.updated_at AS mutated_at,m.recalled_at,m.recalled_by,rev.actor "+
"FROM im_group_messages 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=? "+
"LEFT JOIN im_message_revisions AS rev ON rev.topic=m.topic AND rev.seqid=m.seqid AND rev.version=m.version-1 "+
"WHERE m.topic=? AND d.deletedfor IS NULL AND (m.version>1 OR m.recalled_at IS NOT NULL OR m.deleted_at IS NOT NULL OR m.status='recalled') "+
"ORDER BY m.updated_at DESC,m.seqid DESC LIMIT ?",
decodeUID(uid), topic, limit+1)
if err != nil {
return nil, "", false, 0, err
}
defer rows.Close()
return scanTenantMessageMutations(rows, limit, currentVersion)
}
func scanTenantMessageMutations(rows *sqlx.Rows, limit int, currentVersion int64) ([]t.MessageMutation, string, bool, int64, error) {
items := make([]t.MessageMutation, 0, limit)
hasMore := false
for rows.Next() {
row := struct {
SeqId int64 `db:"seqid"`
Version int `db:"version"`
MutatedAt time.Time `db:"mutated_at"`
RecalledAt sql.NullTime `db:"recalled_at"`
RecalledBy sql.NullInt64 `db:"recalled_by"`
Actor sql.NullInt64 `db:"actor"`
}{}
if err := rows.StructScan(&row); err != nil {
return nil, "", false, 0, err
}
if len(items) == limit {
hasMore = true
continue
}
event := "message.edited"
operator := ""
if row.Actor.Valid {
operator = store.EncodeUid(row.Actor.Int64).String()
}
if row.RecalledAt.Valid {
event = "message.recalled"
if row.RecalledBy.Valid {
operator = store.EncodeUid(row.RecalledBy.Int64).String()
}
row.MutatedAt = row.RecalledAt.Time
}
items = append(items, t.MessageMutation{
SeqId: row.SeqId,
Version: row.Version,
Event: event,
Operator: operator,
MutatedAt: row.MutatedAt,
MutationVersion: currentVersion,
})
}
if err := rows.Err(); err != nil {
return nil, "", false, 0, err
}
return items, strconv.FormatInt(currentVersion, 10), hasMore, currentVersion, nil
}
func (a *adapter) TenantCallCreate(id t.TenantID, record *t.CallRecord) error {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)