From 5f717c21629a7436cee3610ae656b5cc78ff74ec Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Mon, 21 Sep 2026 13:43:51 +0800 Subject: [PATCH] refactor: read user profile fields from profile table --- server/auth_audit_test.go | 4 +- server/db/mysql/adapter.go | 26 ++--- server/db/mysql/tenant_adapter.go | 137 +++++++++++++---------- server/db/mysql/tenant_groups.go | 10 +- server/db/mysql/tests/mysql_test.go | 24 ++-- server/db/mysql/user_identity_schema.go | 90 --------------- server/db/mysql/user_identity_test.go | 20 +--- server/internal_admin_query_resources.go | 7 +- server/internal_api.go | 2 +- 9 files changed, 117 insertions(+), 203 deletions(-) diff --git a/server/auth_audit_test.go b/server/auth_audit_test.go index 9871acc..962e260 100644 --- a/server/auth_audit_test.go +++ b/server/auth_audit_test.go @@ -136,8 +136,8 @@ func TestUserQuerySupportsKeywordAndUserIDs(t *testing.T) { "AS register_type", "im_registration_invite_uses", "im_fileuploads AS avatar", - "JSON_EXTRACT(u.public,'$.avatar')", - "JSON_EXTRACT(u.public,'$.photo.data')", + "p.avatar_file_id", + "im_user_profiles AS p", } { if !strings.Contains(usersInternalQuerySpec().Select+usersInternalQuerySpec().From, field) { t.Fatalf("user query missing list field %q", field) diff --git a/server/db/mysql/adapter.go b/server/db/mysql/adapter.go index 5d63e19..5e7a8e9 100644 --- a/server/db/mysql/adapter.go +++ b/server/db/mysql/adapter.go @@ -1352,13 +1352,14 @@ func (a *adapter) UserCreate(user *t.User) error { if err != nil { return err } - insert := "INSERT INTO im_users(id,username,createdat,updatedat,state,access,phone,public,trusted) VALUES(?,?,?,?,?,?,?,?,?)" + nickname, avatarFileID := profileSeedFromPublic(user.Public) + insert := "INSERT INTO im_users(id,username,createdat,updatedat,state,access,phone,trusted) VALUES(?,?,?,?,?,?,?,?)" args := []any{decoded_uid, user.Username, user.CreatedAt, user.UpdatedAt, user.State, user.Access, phone, - common.ToJSON(user.Public), common.ToJSON(user.Trusted)} + common.ToJSON(user.Trusted)} if hasAccountNo { - insert = "INSERT INTO im_users(id,account_no,username,createdat,updatedat,state,access,phone,public,trusted) VALUES(?,?,?,?,?,?,?,?,?,?)" + insert = "INSERT INTO im_users(id,account_no,username,createdat,updatedat,state,access,phone,trusted) VALUES(?,?,?,?,?,?,?,?,?)" args = []any{decoded_uid, accountNo, user.Username, user.CreatedAt, user.UpdatedAt, user.State, user.Access, phone, - common.ToJSON(user.Public), common.ToJSON(user.Trusted)} + common.ToJSON(user.Trusted)} } if _, err = tx.Exec(insert, args...); err != nil { if isDupe(err) { @@ -1371,7 +1372,6 @@ func (a *adapter) UserCreate(user *t.User) error { securityID, decoded_uid, 1, user.CreatedAt, user.UpdatedAt); err != nil { return err } - nickname, avatarFileID := profileSeedFromPublic(user.Public) if _, err = tx.Exec(`INSERT IGNORE INTO im_user_profiles(id,user_id,avatar_file_id,nickname,public_account,created_at,updated_at) VALUES(?,?,?,?,?,?,?)`, profileID, decoded_uid, avatarFileID, nickname, user.Uid().UserId(), user.CreatedAt, user.UpdatedAt); err != nil { return err @@ -1689,7 +1689,7 @@ func (a *adapter) UserGet(uid t.Uid) (*t.User, error) { } var row profileUserRow err := a.db.GetContext(ctx, &row, `SELECT u.id,u.username,u.createdat,u.updatedat,u.state,u.stateat,u.access,u.lastseen,u.useragent, - COALESCE(u.phone,'') AS phone,u.public,u.trusted, + COALESCE(u.phone,'') AS phone,u.trusted, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account,p.updated_at AS profile_updated_at FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.user_id=u.id @@ -1723,7 +1723,7 @@ func (a *adapter) UserGetAll(ids ...t.Uid) ([]t.User, error) { defer cancel() } q, uids, _ := sqlx.In(`SELECT u.id,u.username,u.createdat,u.updatedat,u.state,u.stateat,u.access,u.lastseen,u.useragent, - COALESCE(u.phone,'') AS phone,u.public,u.trusted, + COALESCE(u.phone,'') AS phone,u.trusted, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account,p.updated_at AS profile_updated_at FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.user_id=u.id @@ -1946,7 +1946,7 @@ func (a *adapter) UserUpdate(uid t.Uid, update map[string]any) error { } }() - update = withoutUserTags(update) + update = sanitizeUserUpdate(update) cols, args := common.UpdateByMap(update) if len(cols) == 0 { return tx.Commit() @@ -2487,7 +2487,7 @@ func (a *adapter) TopicsForUser(uid t.Uid, keepDeleted bool, opts *t.QueryOpt) ( // Fetch p2p users and join to p2p subscriptions. if len(usrq) > 0 { - q = `SELECT u.id,u.updatedat,u.state,u.access,u.lastseen,u.useragent,u.public,u.trusted, + q = `SELECT u.id,u.updatedat,u.state,u.access,u.lastseen,u.useragent,u.trusted, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account,p.updated_at AS profile_updated_at, fs.remark AS friendship_remark @@ -2561,7 +2561,7 @@ func (a *adapter) UsersForTopic(topic string, keepDeleted bool, opts *t.QueryOpt // Fetch all subscribed users. The number of users is not large. q := `SELECT s.createdat,s.updatedat,s.deletedat,s.userid,s.topic,s.delid,s.recvseqid, - s.readseqid,s.modewant,s.modegiven,u.public,u.trusted,u.lastseen,u.useragent,s.private, + s.readseqid,s.modewant,s.modegiven,u.trusted,u.lastseen,u.useragent,s.private, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account FROM im_subscriptions AS s JOIN im_users AS u ON s.userid=u.id @@ -2617,14 +2617,14 @@ func (a *adapter) UsersForTopic(topic string, keepDeleted bool, opts *t.QueryOpt var subs []t.Subscription var lastSeen sql.NullTime var userAgent string - var public, trusted any + var trusted any var profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount sql.NullString for rows.Next() { if err = rows.Scan( &sub.CreatedAt, &sub.UpdatedAt, &sub.DeletedAt, &sub.User, &sub.Topic, &sub.DelId, &sub.RecvSeqId, &sub.ReadSeqId, &sub.ModeWant, &sub.ModeGiven, - &public, &trusted, &lastSeen, &userAgent, &sub.Private, + &trusted, &lastSeen, &userAgent, &sub.Private, &profileNickname, &profileAvatarFileID, &profileSignature, &profileGender, &profileRegion, &profilePublicAccount); err != nil { break @@ -2632,7 +2632,7 @@ func (a *adapter) UsersForTopic(topic string, keepDeleted bool, opts *t.QueryOpt sub.User = common.EncodeUidString(sub.User).String() sub.Private = common.FromJSON(sub.Private) - sub.SetPublic(profilePublicFromFields(common.FromJSON(public), profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount)) + sub.SetPublic(profilePublicFromFields(profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount)) sub.SetTrusted(common.FromJSON(trusted)) sub.SetLastSeenAndUA(&lastSeen.Time, userAgent) subs = append(subs, sub) diff --git a/server/db/mysql/tenant_adapter.go b/server/db/mysql/tenant_adapter.go index 6c15bfe..683f6b0 100644 --- a/server/db/mysql/tenant_adapter.go +++ b/server/db/mysql/tenant_adapter.go @@ -59,7 +59,6 @@ type profileUserRow struct { Phone string `db:"phone"` LastSeen sql.NullTime `db:"lastseen"` UserAgent string `db:"useragent"` - Public any `db:"public"` Trusted any `db:"trusted"` ProfileNickname sql.NullString `db:"profile_nickname"` ProfileAvatarFileID sql.NullString `db:"profile_avatar_file_id"` @@ -86,7 +85,7 @@ func (row profileUserRow) lastSeen() *time.Time { } func (row profileUserRow) profilePublic() any { - return profilePublicFromFields(common.FromJSON(row.Public), row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount) + return profilePublicFromFields(row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount) } func profileUserFromRow(row profileUserRow) t.User { @@ -111,9 +110,8 @@ func profileUserFromRow(row profileUserRow) t.User { return user } -func profilePublicFromFields(base any, nickname, avatarFileID, signature, gender, region, publicAccount sql.NullString) any { - public := publicMap(base) - clearProfilePublicFields(public) +func profilePublicFromFields(nickname, avatarFileID, signature, gender, region, publicAccount sql.NullString) any { + public := map[string]any{} if nickname.Valid { value := strings.TrimSpace(nickname.String) if value != "" { @@ -181,10 +179,11 @@ func ensurePrivateMutationVersion(private any, version int64) any { 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) - } +func sanitizeUserUpdate(update map[string]any) map[string]any { + update = withoutUserTags(update) + delete(update, "Public") + delete(update, "public") + return update } func profileSeedFromPublic(public any) (nickname, avatarFileID string) { @@ -403,7 +402,7 @@ func (a *adapter) TenantUserGet(id t.TenantID, uid t.Uid) (*t.User, error) { } var row profileUserRow err = a.db.GetContext(ctx, &row, `SELECT ? AS tenant_id,u.id,u.username,u.createdat,u.updatedat,u.state,u.stateat,u.access,u.lastseen,u.useragent, - COALESCE(u.phone,'') AS phone,u.public,u.trusted, + COALESCE(u.phone,'') AS phone,u.trusted, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account,p.updated_at AS profile_updated_at FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.user_id=u.id @@ -493,10 +492,18 @@ func (a *adapter) tenantUserGetAllTx(ctx context.Context, tx *sqlx.Tx, id t.Tena var params []any if a.tenantScopedDB { q, params, _ = sqlx.In(`SELECT id,username,createdat,updatedat,state,stateat,access,lastseen,useragent, - COALESCE(phone,'') AS phone,public,trusted FROM im_users WHERE id IN (?) AND state!=?`, args, t.StateDeleted) + COALESCE(phone,'') AS phone,trusted, + p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, + p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account,p.updated_at AS profile_updated_at + FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.user_id=u.id + WHERE u.id IN (?) AND u.state!=?`, args, t.StateDeleted) } else { - q, params, _ = sqlx.In(`SELECT tenant_id,id,username,createdat,updatedat,state,stateat,access,lastseen,useragent, - COALESCE(phone,'') AS phone,public,trusted FROM im_users WHERE tenant_id=? AND id IN (?) AND state!=?`, id, args, t.StateDeleted) + q, params, _ = sqlx.In(`SELECT u.tenant_id,u.id,u.username,u.createdat,u.updatedat,u.state,u.stateat,u.access,u.lastseen,u.useragent, + COALESCE(u.phone,'') AS phone,u.trusted, + p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, + p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account,p.updated_at AS profile_updated_at + FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.tenant_id=u.tenant_id AND p.user_id=u.id + WHERE u.tenant_id=? AND u.id IN (?) AND u.state!=?`, id, args, t.StateDeleted) } query := a.db.Rebind(q) var rows *sqlx.Rows @@ -513,12 +520,11 @@ func (a *adapter) tenantUserGetAllTx(ctx context.Context, tx *sqlx.Tx, id t.Tena var users []t.User for rows.Next() { - var user t.User - if err = rows.StructScan(&user); err != nil { + var row profileUserRow + if err = rows.StructScan(&row); err != nil { return nil, err } - scanTenantUser(&user) - users = append(users, user) + users = append(users, profileUserFromRow(row)) } return users, rows.Err() } @@ -680,7 +686,7 @@ func (a *adapter) TenantUserUpdate(id t.TenantID, uid t.Uid, update map[string]a } }() decoded := decodeUID(uid) - update = withoutUserTags(update) + update = sanitizeUserUpdate(update) cols, args := common.UpdateByMap(update) if len(cols) == 0 { return tx.Commit() @@ -1080,7 +1086,7 @@ func (a *adapter) TenantTopicsForUser(id t.TenantID, uid t.Uid, keepDeleted bool } } if len(usrq) > 0 { - q = `SELECT u.tenant_id,u.id,u.updatedat,u.state,u.access,u.lastseen,u.useragent,u.public,u.trusted, + q = `SELECT u.tenant_id,u.id,u.updatedat,u.state,u.access,u.lastseen,u.useragent,u.trusted, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account,p.updated_at AS profile_updated_at, fs.remark AS friendship_remark @@ -1147,7 +1153,7 @@ func (a *adapter) TenantUsersForTopic(id t.TenantID, topic string, keepDeleted b } tcat := t.GetTopicCat(topic) q := `SELECT s.tenant_id,s.createdat,s.updatedat,s.deletedat,s.userid,s.topic,s.delid,s.recvseqid, - s.readseqid,s.modewant,s.modegiven,u.public,u.trusted,u.lastseen,u.useragent,s.private, + s.readseqid,s.modewant,s.modegiven,u.trusted,u.lastseen,u.useragent,s.private, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account FROM im_subscriptions AS s JOIN im_users AS u ON s.tenant_id=u.tenant_id AND s.userid=u.id @@ -1191,18 +1197,18 @@ func (a *adapter) TenantUsersForTopic(id t.TenantID, topic string, keepDeleted b for rows.Next() { var sub t.Subscription var lastSeen sql.NullTime - var public, trusted any + var trusted any var profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount sql.NullString if err = rows.Scan(&sub.TenantID, &sub.CreatedAt, &sub.UpdatedAt, &sub.DeletedAt, &sub.User, &sub.Topic, &sub.DelId, &sub.RecvSeqId, &sub.ReadSeqId, &sub.ModeWant, &sub.ModeGiven, - &public, &trusted, &lastSeen, &userAgent, &sub.Private, + &trusted, &lastSeen, &userAgent, &sub.Private, &profileNickname, &profileAvatarFileID, &profileSignature, &profileGender, &profileRegion, &profilePublicAccount); err != nil { return nil, err } sub.User = common.EncodeUidString(sub.User).String() sub.Private = common.FromJSON(sub.Private) - sub.SetPublic(profilePublicFromFields(common.FromJSON(public), profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount)) + sub.SetPublic(profilePublicFromFields(profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount)) sub.SetTrusted(common.FromJSON(trusted)) sub.SetLastSeenAndUA(&lastSeen.Time, userAgent) subs = append(subs, sub) @@ -6280,9 +6286,12 @@ func (a *adapter) TenantFriendRequestList(id t.TenantID, uid t.Uid, direction, s args = append(args, parsed) } args = append(args, limit+1) - query := `SELECT fr.*,u.public,u.trusted + query := `SELECT fr.*,u.trusted, + p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, + p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account FROM im_friend_requests AS fr JOIN im_users AS u ON u.id=IF(fr.applicant_id=?,fr.target_id,fr.applicant_id) + LEFT JOIN im_user_profiles AS p ON p.user_id=u.id WHERE ` + strings.Join(where, " AND ") + ` ORDER BY fr.id DESC LIMIT ?` args = append([]any{decodedUID}, args...) @@ -6299,21 +6308,26 @@ func (a *adapter) TenantFriendRequestList(id t.TenantID, uid t.Uid, direction, s nextCursor := "" for rows.Next() { row := struct { - ID int64 - RequestID string `db:"request_id"` - ApplicantID int64 `db:"applicant_id"` - TargetID int64 `db:"target_id"` - Message string - Source string - SourceTopic string `db:"source_topic"` - Status string - IdempotencyKey string `db:"idempotency_key"` - ExpiresAt time.Time `db:"expires_at"` - HandledAt *time.Time `db:"handled_at"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` - Public any - Trusted any + ID int64 + RequestID string `db:"request_id"` + ApplicantID int64 `db:"applicant_id"` + TargetID int64 `db:"target_id"` + Message string + Source string + SourceTopic string `db:"source_topic"` + Status string + IdempotencyKey string `db:"idempotency_key"` + ExpiresAt time.Time `db:"expires_at"` + HandledAt *time.Time `db:"handled_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + Trusted any + ProfileNickname sql.NullString `db:"profile_nickname"` + ProfileAvatarFileID sql.NullString `db:"profile_avatar_file_id"` + ProfileSignature sql.NullString `db:"profile_signature"` + ProfileGender sql.NullString `db:"profile_gender"` + ProfileRegion sql.NullString `db:"profile_region"` + ProfilePublicAccount sql.NullString `db:"profile_public_account"` }{} if err = rows.StructScan(&row); err != nil { return nil, "", err @@ -6350,7 +6364,7 @@ func (a *adapter) TenantFriendRequestList(id t.TenantID, uid t.Uid, direction, s Peer: &t.User{ ObjHeader: t.ObjHeader{Id: peerID.String()}, TenantID: id, - Public: common.FromJSON(row.Public), + Public: profilePublicFromFields(row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount), Trusted: common.FromJSON(row.Trusted), }, Direction: direction, @@ -6699,20 +6713,20 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init } if q := strings.TrimSpace(queryText); q != "" { like := "%" + q + "%" - where = append(where, "(fs.remark LIKE ? OR p.nickname LIKE ? OR p.public_account LIKE ? OR CAST(u.public AS CHAR) LIKE ?)") - args = append(args, like, like, like, like) + where = append(where, "(fs.remark LIKE ? OR p.nickname LIKE ? OR p.public_account LIKE ?)") + args = append(args, like, like, like) } if initial = strings.TrimSpace(initial); initial != "" { like := initial + "%" - where = append(where, "(fs.remark LIKE ? OR p.nickname LIKE ? OR CAST(u.public AS CHAR) LIKE ?)") - args = append(args, like, like, like) + where = append(where, "(fs.remark LIKE ? OR p.nickname LIKE ?)") + args = append(args, like, like) } args = append(args, limit+1) ctx, cancel := a.getContext() if cancel != nil { defer cancel() } - rows, err := a.db.QueryxContext(ctx, `SELECT fs.*,u.public,u.trusted, + rows, err := a.db.QueryxContext(ctx, `SELECT fs.*,u.trusted, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account,p.updated_at AS profile_updated_at FROM im_friendships AS fs @@ -6736,7 +6750,6 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init Topic string CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` - Public any Trusted any ProfileNickname sql.NullString `db:"profile_nickname"` ProfileAvatarFileID sql.NullString `db:"profile_avatar_file_id"` @@ -6768,7 +6781,7 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init Peer: &t.User{ ObjHeader: t.ObjHeader{Id: peerID.String()}, TenantID: id, - Public: profilePublicFromFields(common.FromJSON(row.Public), row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount), + Public: profilePublicFromFields(row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount), Trusted: common.FromJSON(row.Trusted), }, }) @@ -6956,9 +6969,12 @@ func (a *adapter) TenantUserBlockList(id t.TenantID, blockerID t.Uid, cursor str if cancel != nil { defer cancel() } - rows, err := a.db.QueryxContext(ctx, `SELECT b.blocker_id,b.blocked_id,b.created_at,b.updated_at,u.public,u.trusted + rows, err := a.db.QueryxContext(ctx, `SELECT b.blocker_id,b.blocked_id,b.created_at,b.updated_at,u.trusted, + p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, + p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account FROM im_user_blocks AS b LEFT JOIN im_users AS u ON u.id=b.blocked_id + LEFT JOIN im_user_profiles AS p ON p.user_id=b.blocked_id WHERE `+strings.Join(where, " AND ")+` ORDER BY b.blocked_id DESC LIMIT ?`, args...) if err != nil { @@ -6969,12 +6985,17 @@ func (a *adapter) TenantUserBlockList(id t.TenantID, blockerID t.Uid, cursor str nextCursor := "" for rows.Next() { row := struct { - BlockerID int64 `db:"blocker_id"` - BlockedID int64 `db:"blocked_id"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` - Public any - Trusted any + BlockerID int64 `db:"blocker_id"` + BlockedID int64 `db:"blocked_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + Trusted any + ProfileNickname sql.NullString `db:"profile_nickname"` + ProfileAvatarFileID sql.NullString `db:"profile_avatar_file_id"` + ProfileSignature sql.NullString `db:"profile_signature"` + ProfileGender sql.NullString `db:"profile_gender"` + ProfileRegion sql.NullString `db:"profile_region"` + ProfilePublicAccount sql.NullString `db:"profile_public_account"` }{} if err = rows.StructScan(&row); err != nil { return nil, "", err @@ -6995,7 +7016,7 @@ func (a *adapter) TenantUserBlockList(id t.TenantID, blockerID t.Uid, cursor str User: &t.User{ ObjHeader: t.ObjHeader{Id: blockedID.String()}, TenantID: id, - Public: common.FromJSON(row.Public), + Public: profilePublicFromFields(row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount), Trusted: common.FromJSON(row.Trusted), }, }) @@ -7101,8 +7122,8 @@ func (a *adapter) TenantUserSearch(id t.TenantID, viewer t.Uid, queryText, searc if phone, err := normalizeUserPhone(queryText); err == nil { phoneCandidate = phone } - where = append(where, "(profile.nickname LIKE ? OR profile.public_account LIKE ? OR u.username LIKE ? OR u.phone=? OR u.phone=? OR CAST(u.public AS CHAR) LIKE ? OR u.id=?)") - args = append(args, like, like, like, queryText, phoneCandidate, like, userIDNumericArg(queryText)) + where = append(where, "(profile.nickname LIKE ? OR profile.public_account LIKE ? OR u.username LIKE ? OR u.phone=? OR u.phone=? OR u.id=?)") + args = append(args, like, like, like, queryText, phoneCandidate, userIDNumericArg(queryText)) case "exact_account": where = append(where, "(profile.public_account=? OR u.username=? OR u.id=?)") args = append(args, queryText, strings.TrimPrefix(queryText, "basic:"), userIDNumericArg(queryText)) @@ -7125,7 +7146,7 @@ func (a *adapter) TenantUserSearch(id t.TenantID, viewer t.Uid, queryText, searc defer cancel() } rows, err := a.db.QueryxContext(ctx, `SELECT DISTINCT ? AS tenant_id,u.id,u.username,u.createdat,u.updatedat,u.state,u.stateat,u.access,u.lastseen,u.useragent, - COALESCE(u.phone,'') AS phone,u.public,u.trusted, + COALESCE(u.phone,'') AS phone,u.trusted, profile.nickname AS profile_nickname,profile.avatar_file_id AS profile_avatar_file_id,profile.signature AS profile_signature, profile.gender AS profile_gender,profile.region AS profile_region,profile.public_account AS profile_public_account,profile.updated_at AS profile_updated_at FROM im_users AS u `+strings.Join(joins, " ")+` diff --git a/server/db/mysql/tenant_groups.go b/server/db/mysql/tenant_groups.go index b54212f..00c3135 100644 --- a/server/db/mysql/tenant_groups.go +++ b/server/db/mysql/tenant_groups.go @@ -322,11 +322,8 @@ func (a *adapter) TenantGroupMemberList(id t.TenantID, topic string, opts t.Grou if opts.Query != "" { like := "%" + opts.Query + "%" where = append(where, `(gm.nickname LIKE ? OR p.nickname LIKE ? OR p.public_account LIKE ? - OR JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.fn')) LIKE ? - OR JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.name')) LIKE ? - OR JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.display_name')) LIKE ? OR u.username LIKE ?)`) - args = append(args, like, like, like, like, like, like, like) + args = append(args, like, like, like, like) } if opts.Cursor != "" { parsed, err := strconv.ParseInt(opts.Cursor, 10, 64) @@ -346,7 +343,7 @@ func (a *adapter) TenantGroupMemberList(id t.TenantID, topic string, opts t.Grou if cancel != nil { defer cancel() } - rows, err := a.db.QueryxContext(ctx, `SELECT STRAIGHT_JOIN gm.*,s.modewant,s.modegiven,u.public,u.trusted, + rows, err := a.db.QueryxContext(ctx, `SELECT STRAIGHT_JOIN gm.*,s.modewant,s.modegiven,u.trusted, p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.signature AS profile_signature, p.gender AS profile_gender,p.region AS profile_region,p.public_account AS profile_public_account FROM im_group_members AS gm FORCE INDEX (`+memberIndex+`) @@ -363,7 +360,6 @@ func (a *adapter) TenantGroupMemberList(id t.TenantID, topic string, opts t.Grou for rows.Next() { row := struct { t.GroupMember - Public any Trusted any ProfileNickname sql.NullString `db:"profile_nickname"` ProfileAvatarFileID sql.NullString `db:"profile_avatar_file_id"` @@ -383,7 +379,7 @@ func (a *adapter) TenantGroupMemberList(id t.TenantID, topic string, opts t.Grou } continue } - item.Public = profilePublicFromFields(common.FromJSON(row.Public), row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount) + item.Public = profilePublicFromFields(row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount) item.Trusted = common.FromJSON(row.Trusted) items = append(items, item) } diff --git a/server/db/mysql/tests/mysql_test.go b/server/db/mysql/tests/mysql_test.go index 1407690..4538af3 100644 --- a/server/db/mysql/tests/mysql_test.go +++ b/server/db/mysql/tests/mysql_test.go @@ -289,8 +289,8 @@ func TestTenantAuthIsolation(t *testing.T) { userBID := userAID + 1 for index, userID := range []int64{userAID, userBID} { tenantDB := quoteIdent(dbNames[index]) - if _, err := db.Exec("INSERT INTO "+tenantDB+".im_users(id,account_no,username,createdat,updatedat,state,access,phone,public,trusted) "+ - "VALUES(?,?,?,?,?,?,?,?,?,?)", userID, 1000+index, "alice", now, now, types.StateOK, `{}`, "13800000000", nil, nil); err != nil { + if _, err := db.Exec("INSERT INTO "+tenantDB+".im_users(id,account_no,username,createdat,updatedat,state,access,phone,trusted) "+ + "VALUES(?,?,?,?,?,?,?,?,?)", userID, 1000+index, "alice", now, now, types.StateOK, `{}`, "13800000000", nil); err != nil { t.Fatal(err) } if _, err := db.Exec("INSERT INTO "+tenantDB+".im_auth(userid,authlvl,secret) VALUES(?,?,?)", @@ -354,17 +354,25 @@ func TestTenantUserSearchMatchesUsernamePhoneAndPublicAccount(t *testing.T) { id int64 username string phone any - public string + nickname string }{ - {viewerID, "viewerx", nil, `{"fn":"Viewer"}`}, - {targetID, "potter", "+8615500000002", `{"fn":"Potter"}`}, + {viewerID, "viewerx", nil, "Viewer"}, + {targetID, "potter", "+8615500000002", "Potter"}, } { if _, err := db.Exec( - `INSERT INTO im_users(id,account_no,username,createdat,updatedat,state,access,phone,public,trusted) - VALUES(?,?,?,?,?,?,?,?,?,?)`, - user.id, user.id%1000000, user.username, now, now, types.StateOK, `{"Auth":"JRWPAS","Anon":"N"}`, user.phone, user.public, `{}`); err != nil { + `INSERT INTO im_users(id,account_no,username,createdat,updatedat,state,access,phone,trusted) + VALUES(?,?,?,?,?,?,?,?,?)`, + user.id, user.id%1000000, user.username, now, now, types.StateOK, `{"Auth":"JRWPAS","Anon":"N"}`, user.phone, `{}`); err != nil { t.Fatal(err) } + if user.id != targetID { + if _, err := db.Exec( + `INSERT INTO im_user_profiles(id,user_id,nickname,public_account,created_at,updated_at) + VALUES(?,?,?,?,?,?)`, + user.id+2000, user.id, user.nickname, store.EncodeUid(user.id).UserId(), now, now); err != nil { + t.Fatal(err) + } + } if _, err := db.Exec( `INSERT INTO im_user_privacy_settings(id,user_id,add_me_methods,created_at,updated_at) VALUES(?,?,?,?,?)`, diff --git a/server/db/mysql/user_identity_schema.go b/server/db/mysql/user_identity_schema.go index b335a17..7a9932d 100644 --- a/server/db/mysql/user_identity_schema.go +++ b/server/db/mysql/user_identity_schema.go @@ -156,12 +156,6 @@ func ensureUserIdentitySchema(db *sqlx.DB) error { if err = backfillProfileEmails(ctx, conn, usersScoped); err != nil { return err } - if err = backfillProfileDisplay(ctx, conn, usersScoped); err != nil { - return err - } - if err = backfillUserPublicFromProfile(ctx, conn, usersScoped); err != nil { - return err - } } indexName := "users_phone" @@ -490,90 +484,6 @@ func backfillProfileEmails(ctx context.Context, conn *sqlx.Conn, usersScoped boo return nil } -func backfillProfileDisplay(ctx context.Context, conn *sqlx.Conn, usersScoped bool) error { - profilesScoped, err := identityColumnExists(ctx, conn, "im_user_profiles", "tenant_id") - if err != nil { - return err - } - if profilesScoped != usersScoped { - return errors.New("mysql: im_users and im_user_profiles tenant scope mismatch") - } - nicknameExpr := `COALESCE( - NULLIF(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.display_name')), ''), - NULLIF(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.nickname')), ''), - NULLIF(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.fn')), ''), - NULLIF(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.name')), '') - )` - avatarExpr := `COALESCE( - NULLIF(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.avatar_file_id')), '') - )` - if usersScoped { - if _, err = conn.ExecContext(ctx, `INSERT IGNORE INTO im_user_profiles(id,tenant_id,user_id,nickname,avatar_file_id,public_account) - SELECT u.id,u.tenant_id,u.id,COALESCE(`+nicknameExpr+`,''),COALESCE(`+avatarExpr+`,''),'' - FROM im_users AS u - WHERE `+nicknameExpr+` IS NOT NULL OR `+avatarExpr+` IS NOT NULL`); err != nil { - return err - } - _, err = conn.ExecContext(ctx, `UPDATE im_user_profiles AS p - JOIN im_users AS u ON u.tenant_id=p.tenant_id AND u.id=p.user_id - SET p.nickname=IF(p.nickname='' AND `+nicknameExpr+` IS NOT NULL, `+nicknameExpr+`, p.nickname), - p.avatar_file_id=IF(p.avatar_file_id='' AND `+avatarExpr+` IS NOT NULL, `+avatarExpr+`, p.avatar_file_id) - WHERE (p.nickname='' AND `+nicknameExpr+` IS NOT NULL) OR (p.avatar_file_id='' AND `+avatarExpr+` IS NOT NULL)`) - return err - } - if _, err = conn.ExecContext(ctx, `INSERT IGNORE INTO im_user_profiles(id,user_id,nickname,avatar_file_id,public_account) - SELECT u.id,u.id,COALESCE(`+nicknameExpr+`,''),COALESCE(`+avatarExpr+`,''),'' - FROM im_users AS u - WHERE `+nicknameExpr+` IS NOT NULL OR `+avatarExpr+` IS NOT NULL`); err != nil { - return err - } - _, err = conn.ExecContext(ctx, `UPDATE im_user_profiles AS p - JOIN im_users AS u ON u.id=p.user_id - SET p.nickname=IF(p.nickname='' AND `+nicknameExpr+` IS NOT NULL, `+nicknameExpr+`, p.nickname), - p.avatar_file_id=IF(p.avatar_file_id='' AND `+avatarExpr+` IS NOT NULL, `+avatarExpr+`, p.avatar_file_id) - WHERE (p.nickname='' AND `+nicknameExpr+` IS NOT NULL) OR (p.avatar_file_id='' AND `+avatarExpr+` IS NOT NULL)`) - return err -} - -func backfillUserPublicFromProfile(ctx context.Context, conn *sqlx.Conn, usersScoped bool) error { - profilesScoped, err := identityColumnExists(ctx, conn, "im_user_profiles", "tenant_id") - if err != nil { - return err - } - if profilesScoped != usersScoped { - return errors.New("mysql: im_users and im_user_profiles tenant scope mismatch") - } - publicDoc := "COALESCE(NULLIF(u.public,''), JSON_OBJECT())" - nicknamePatch := `u.public=JSON_SET(` + publicDoc + `, - '$.display_name',p.nickname,'$.nickname',p.nickname,'$.fn',p.nickname,'$.name',p.nickname)` - avatarPatch := `u.public=JSON_SET(` + publicDoc + `,'$.avatar_file_id',p.avatar_file_id)` - accountPatch := `u.public=JSON_SET(` + publicDoc + `,'$.public_account',p.public_account)` - join := "JOIN im_users AS u ON u.id=p.user_id" - if usersScoped { - join = "JOIN im_users AS u ON u.tenant_id=p.tenant_id AND u.id=p.user_id" - } - for _, query := range []string{ - `UPDATE im_user_profiles AS p ` + join + ` - SET ` + nicknamePatch + ` - WHERE p.nickname<>'' AND ( - COALESCE(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.display_name')),'')<>p.nickname OR - COALESCE(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.nickname')),'')<>p.nickname OR - COALESCE(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.fn')),'')<>p.nickname OR - COALESCE(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.name')),'')<>p.nickname)`, - `UPDATE im_user_profiles AS p ` + join + ` - SET ` + avatarPatch + ` - WHERE p.avatar_file_id<>'' AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.avatar_file_id')),'')<>p.avatar_file_id`, - `UPDATE im_user_profiles AS p ` + join + ` - SET ` + accountPatch + ` - WHERE p.public_account<>'' AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.public_account')),'')<>p.public_account`, - } { - if _, err = conn.ExecContext(ctx, query); err != nil { - return err - } - } - return nil -} - func identityCandidates(ctx context.Context, conn *sqlx.Conn, scoped bool, method string) ([]identityValue, error) { var out []identityValue appendQuery := func(query string, args ...any) error { diff --git a/server/db/mysql/user_identity_test.go b/server/db/mysql/user_identity_test.go index 6a43159..678e9fb 100644 --- a/server/db/mysql/user_identity_test.go +++ b/server/db/mysql/user_identity_test.go @@ -28,18 +28,7 @@ func TestNormalizeUserPhone(t *testing.T) { } func TestProfilePublicFromFields(t *testing.T) { - public := profilePublicFromFields(map[string]any{ - "fn": "旧昵称", - "name": "旧昵称", - "nickname": "旧昵称", - "display_name": "旧昵称", - "avatar_file_id": "old_avatar", - "signature": "旧签名", - "gender": "old_gender", - "region": "old_region", - "photo": "old_photo", - "public_account": "usrAccount", - }, sql.NullString{String: "新昵称", Valid: true}, sql.NullString{String: "file_avatar_1", Valid: true}, + public := profilePublicFromFields(sql.NullString{String: "新昵称", Valid: true}, sql.NullString{String: "file_avatar_1", Valid: true}, sql.NullString{String: "新签名", Valid: true}, sql.NullString{String: "male", Valid: true}, sql.NullString{String: "上海", Valid: true}, sql.NullString{String: "usrProfile", Valid: true}).(map[string]any) @@ -53,12 +42,7 @@ func TestProfilePublicFromFields(t *testing.T) { t.Fatalf("profile public not overlaid: %#v", public) } - public = profilePublicFromFields(map[string]any{ - "fn": "旧昵称", - "avatar_file_id": "old_avatar", - "public_account": "usrAccount", - "signature": "旧签名", - }, sql.NullString{Valid: true}, sql.NullString{Valid: true}, sql.NullString{Valid: true}, + public = profilePublicFromFields(sql.NullString{Valid: true}, sql.NullString{Valid: true}, sql.NullString{Valid: true}, sql.NullString{Valid: true}, sql.NullString{Valid: true}, sql.NullString{Valid: true}).(map[string]any) for _, key := range []string{"fn", "name", "nickname", "display_name", "avatar_file_id", "photo", "avatar", "signature", "gender", "region", "public_account"} { if _, ok := public[key]; ok { diff --git a/server/internal_admin_query_resources.go b/server/internal_admin_query_resources.go index b9c47d3..e6e6d2d 100644 --- a/server/internal_admin_query_resources.go +++ b/server/internal_admin_query_resources.go @@ -326,16 +326,11 @@ func internalAdminQuerySpec(resource string) (*internalQuerySpec, bool) { func usersInternalQuerySpec() *internalQuerySpec { return &internalQuerySpec{ - Select: `SELECT CAST(u.id AS CHAR) AS user_id,u.account_no AS display_no,u.username,u.phone,u.state,u.stateat AS state_at,u.access,u.lastseen AS last_seen,u.useragent AS user_agent,u.public,u.trusted, + Select: `SELECT CAST(u.id AS CHAR) AS user_id,u.account_no AS display_no,u.username,u.phone,u.state,u.stateat AS state_at,u.access,u.lastseen AS last_seen,u.useragent AS user_agent,u.trusted, p.nickname,p.public_account,p.gender,p.region,p.avatar_file_id,COALESCE(p.friend_add_enabled,1) AS friend_add_enabled, COALESCE( NULLIF(avatar.location,''), NULLIF(CASE WHEN p.avatar_file_id REGEXP '^(https?://|data:)' THEN p.avatar_file_id ELSE '' END,''), - NULLIF(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.avatar')),''), - NULLIF(CASE WHEN JSON_TYPE(JSON_EXTRACT(u.public,'$.photo'))='STRING' THEN JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.photo')) ELSE '' END,''), - CASE WHEN JSON_TYPE(JSON_EXTRACT(u.public,'$.photo'))='OBJECT' AND NULLIF(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.photo.data')),'') IS NOT NULL - THEN CONCAT('data:image/',COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.photo.type')),''),'jpeg'),';base64,',JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.photo.data'))) ELSE NULL END, - NULLIF(CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.avatar_file_id')) REGEXP '^(https?://|data:)' THEN JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.avatar_file_id')) ELSE '' END,''), '' ) AS avatar, p.signature,sec.auth_version,sec.password_updated_at,u.createdat AS created_at,u.updatedat AS updated_at, diff --git a/server/internal_api.go b/server/internal_api.go index 2f5d231..be3f227 100644 --- a/server/internal_api.go +++ b/server/internal_api.go @@ -523,7 +523,7 @@ func serveInternalGroupMembersQuery(wrt http.ResponseWriter, req *http.Request, return } spec := &internalQuerySpec{ - Select: `SELECT gm.id,gm.topic,gm.user_id,u.state,u.public,p.nickname,p.public_account,s.modewant AS mode_want,s.modegiven AS mode_given,s.recvseqid AS recv_seq_id,s.readseqid AS read_seq_id,s.delid AS del_id, + Select: `SELECT gm.id,gm.topic,gm.user_id,u.state,p.nickname,p.public_account,p.avatar_file_id,p.signature,p.gender,p.region,s.modewant AS mode_want,s.modegiven AS mode_given,s.recvseqid AS recv_seq_id,s.readseqid AS read_seq_id,s.delid AS del_id, gm.role,gm.member_status,gm.nickname AS group_nickname,gm.pinned,gm.mute_notifications,gm.join_source,gm.joined_by,gm.joined_at,gm.left_at,gm.removed_by,gm.version,gm.created_at,gm.updated_at`, From: "FROM im_group_members AS gm LEFT JOIN im_users AS u ON u.id=gm.user_id LEFT JOIN im_user_profiles AS p ON p.user_id=gm.user_id LEFT JOIN im_subscriptions AS s ON s.topic=gm.topic AND s.userid=gm.user_id", DefaultSort: "ORDER BY gm.id DESC",