fix: 统一用户资料投影并放开头像访问
This commit is contained in:
@@ -2439,6 +2439,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
||||
|
||||
- 查询 `GET /v1/im/me/profile` 返回 `avatar_file_id,nickname,birth_date,signature,gender,region,email,public_account,version,field_rules`;`email` 当前仅为资料字段,不用于登录或找回密码。
|
||||
- 更新统一使用 `PATCH /v1/im/me/profile`,仅提交变更字段并带 `version`;服务端更新后同步 MeetingIM `me.public` 并推送资料事件。
|
||||
- 头像、昵称、签名、性别、地区、公开账号等资料字段以 `im_user_profiles` 为业务真源;`im_users.public` 和 `im_subscriptions.public` 只作为服务端生成的展示投影/缓存,前端不得依赖它们作为可编辑源。
|
||||
- 头像先以 `purpose=user_avatar` 上传再 PATCH;任何字段的可见性由隐私策略裁剪。
|
||||
|
||||
### 10.3 设置昵称.png
|
||||
|
||||
+2
-2
@@ -508,11 +508,11 @@ end_reason, created_at, updated_at
|
||||
|
||||
### 14.1 个人资料
|
||||
|
||||
基础展示资料可以同步到 User Public,但可索引、可审计或敏感字段应进入独立表。
|
||||
个人资料以 `im_user_profiles` 为唯一业务真源;User Public 与 `im_subscriptions.public` 仅作为 MeetingIM 协议展示投影/缓存,由服务端在资料变更后同步刷新,业务接口不得把这些投影当作可写真源。
|
||||
|
||||
| 字段 | 存储建议 |
|
||||
| --- | --- |
|
||||
| 头像、昵称、签名 | User Public + `im_user_profiles` |
|
||||
| 头像、昵称、签名 | `im_user_profiles`;User Public / Subscription Public 仅保留服务端生成的展示投影 |
|
||||
| 生日、地区、性别 | `im_user_profiles`,按隐私设置返回 |
|
||||
| 注册手机号 | `im_users.phone`,仅账号安全流程内部使用 |
|
||||
| 邮箱 | `im_user_profiles.email`,仅作为个人资料,不作为认证凭证 |
|
||||
|
||||
@@ -165,6 +165,21 @@ func openMySQLDB(config configType, dsn, dbName string) (*sqlx.DB, string, error
|
||||
return db, dbName, nil
|
||||
}
|
||||
|
||||
func (a *adapter) HealthCheck(ctx context.Context) error {
|
||||
if a.db == nil {
|
||||
return errors.New("mysql database is not open")
|
||||
}
|
||||
if err := a.db.PingContext(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if a.registryDB != nil {
|
||||
if err := a.registryDB.PingContext(ctx); err != nil {
|
||||
return errors.New("tenant registry mysql: " + err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open initializes database session
|
||||
func (a *adapter) Open(jsonconfig json.RawMessage) error {
|
||||
if a.db != nil {
|
||||
@@ -2490,7 +2505,8 @@ 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,
|
||||
p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.updated_at AS profile_updated_at
|
||||
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=? AND u.state!=?`, store.DecodeUid(uid), t.StateDeleted)
|
||||
if err == nil {
|
||||
@@ -2523,7 +2539,8 @@ func (a *adapter) UserGetAll(ids ...t.Uid) ([]t.User, error) {
|
||||
}
|
||||
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,
|
||||
p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.updated_at AS profile_updated_at
|
||||
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!=?`, uids, t.StateDeleted)
|
||||
rows, err := a.db.QueryxContext(ctx, a.db.Rebind(q), uids...)
|
||||
@@ -3259,7 +3276,8 @@ 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,
|
||||
p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.updated_at AS profile_updated_at
|
||||
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 (?)`
|
||||
q, args, _ = sqlx.In(q, usrq)
|
||||
if !keepDeleted {
|
||||
@@ -5165,6 +5183,12 @@ func (a *adapter) FileUserHasAccess(fid string, uid t.Uid) (bool, error) {
|
||||
SELECT 1 FROM im_filemsglinks AS fml
|
||||
WHERE fml.fileid=fu.id AND fml.userid=?
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM im_filemsglinks AS fml
|
||||
INNER JOIN im_friendships AS fs
|
||||
ON fs.user_id=? AND fs.peer_id=fml.userid AND fs.status=?
|
||||
WHERE fml.fileid=fu.id AND fml.userid IS NOT NULL
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM im_filemsglinks AS fml
|
||||
INNER JOIN im_subscriptions AS s
|
||||
@@ -5172,6 +5196,12 @@ func (a *adapter) FileUserHasAccess(fid string, uid t.Uid) (bool, error) {
|
||||
WHERE fml.fileid=fu.id AND fml.topic IS NOT NULL
|
||||
AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM im_filemsglinks AS fml
|
||||
INNER JOIN im_group_members AS gm
|
||||
ON gm.topic=fml.topic AND gm.user_id=? AND gm.member_status=?
|
||||
WHERE fml.fileid=fu.id AND fml.topic IS NOT NULL
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM im_filemsglinks AS fml
|
||||
INNER JOIN im_messages AS m
|
||||
@@ -5211,7 +5241,8 @@ func (a *adapter) FileUserHasAccess(fid string, uid t.Uid) (bool, error) {
|
||||
)
|
||||
)
|
||||
) LIMIT 1`,
|
||||
store.DecodeUid(fileID), t.UploadCompleted, store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid),
|
||||
store.DecodeUid(fileID), t.UploadCompleted, store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid), t.RelationshipFriend,
|
||||
store.DecodeUid(uid), store.DecodeUid(uid), t.GroupMemberStatusActive, store.DecodeUid(uid), store.DecodeUid(uid),
|
||||
store.DecodeUid(uid), fid, fid,
|
||||
fid, t.MeetingStateCanceled, store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid))
|
||||
if err != nil {
|
||||
|
||||
@@ -54,3 +54,21 @@ func TestMessageSearchDoesNotClearScannedHead(t *testing.T) {
|
||||
t.Fatal("message search must not clear Head after sqlx StructScan has already decoded it")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTenantFileAccessIncludesAvatarAudienceRules(t *testing.T) {
|
||||
source, err := os.ReadFile("tenant_adapter.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := string(source)
|
||||
for _, want := range []string{
|
||||
"INNER JOIN im_friendships AS fs",
|
||||
"fs.user_id=? AND fs.peer_id=fml.userid AND fs.status=?",
|
||||
"INNER JOIN im_group_members AS gm",
|
||||
"gm.topic=fml.topic AND gm.user_id=? AND gm.member_status=?",
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("file ACL must include avatar audience rule %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,22 +48,26 @@ func stampTenantUser(id t.TenantID, user *t.User) *t.User {
|
||||
}
|
||||
|
||||
type profileUserRow struct {
|
||||
TenantID t.TenantID `db:"tenant_id"`
|
||||
ID string `db:"id"`
|
||||
Username string
|
||||
CreatedAt time.Time `db:"createdat"`
|
||||
UpdatedAt time.Time `db:"updatedat"`
|
||||
State t.ObjState `db:"state"`
|
||||
StateAt *time.Time `db:"stateat"`
|
||||
Access t.DefaultAccess
|
||||
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"`
|
||||
ProfileUpdatedAt sql.NullTime `db:"profile_updated_at"`
|
||||
TenantID t.TenantID `db:"tenant_id"`
|
||||
ID string `db:"id"`
|
||||
Username string
|
||||
CreatedAt time.Time `db:"createdat"`
|
||||
UpdatedAt time.Time `db:"updatedat"`
|
||||
State t.ObjState `db:"state"`
|
||||
StateAt *time.Time `db:"stateat"`
|
||||
Access t.DefaultAccess
|
||||
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"`
|
||||
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"`
|
||||
ProfileUpdatedAt sql.NullTime `db:"profile_updated_at"`
|
||||
}
|
||||
|
||||
func (row profileUserRow) effectiveUpdatedAt() time.Time {
|
||||
@@ -81,7 +85,7 @@ func (row profileUserRow) lastSeen() *time.Time {
|
||||
}
|
||||
|
||||
func (row profileUserRow) profilePublic() any {
|
||||
return profilePublicFromFields(common.FromJSON(row.Public), row.ProfileNickname, row.ProfileAvatarFileID)
|
||||
return profilePublicFromFields(common.FromJSON(row.Public), row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount)
|
||||
}
|
||||
|
||||
func profileUserFromRow(row profileUserRow) t.User {
|
||||
@@ -106,7 +110,7 @@ func profileUserFromRow(row profileUserRow) t.User {
|
||||
return user
|
||||
}
|
||||
|
||||
func profilePublicFromFields(base any, nickname, avatarFileID sql.NullString) any {
|
||||
func profilePublicFromFields(base any, nickname, avatarFileID, signature, gender, region, publicAccount sql.NullString) any {
|
||||
public := publicMap(base)
|
||||
clearProfilePublicFields(public)
|
||||
if nickname.Valid {
|
||||
@@ -123,11 +127,31 @@ func profilePublicFromFields(base any, nickname, avatarFileID sql.NullString) an
|
||||
public["avatar_file_id"] = value
|
||||
}
|
||||
}
|
||||
if signature.Valid {
|
||||
if value := strings.TrimSpace(signature.String); value != "" {
|
||||
public["signature"] = value
|
||||
}
|
||||
}
|
||||
if gender.Valid {
|
||||
if value := strings.TrimSpace(gender.String); value != "" {
|
||||
public["gender"] = value
|
||||
}
|
||||
}
|
||||
if region.Valid {
|
||||
if value := strings.TrimSpace(region.String); value != "" {
|
||||
public["region"] = value
|
||||
}
|
||||
}
|
||||
if publicAccount.Valid {
|
||||
if value := strings.TrimSpace(publicAccount.String); value != "" {
|
||||
public["public_account"] = value
|
||||
}
|
||||
}
|
||||
return public
|
||||
}
|
||||
|
||||
func clearProfilePublicFields(public map[string]any) {
|
||||
for _, key := range []string{"display_name", "nickname", "fn", "name", "avatar_file_id", "photo", "avatar"} {
|
||||
for _, key := range []string{"display_name", "nickname", "fn", "name", "avatar_file_id", "photo", "avatar", "signature", "gender", "region", "public_account"} {
|
||||
delete(public, key)
|
||||
}
|
||||
}
|
||||
@@ -335,7 +359,8 @@ 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,
|
||||
p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.updated_at AS profile_updated_at
|
||||
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=? AND u.state!=?`,
|
||||
id, decodeUID(uid), t.StateDeleted)
|
||||
@@ -995,7 +1020,8 @@ 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,
|
||||
p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.updated_at AS profile_updated_at
|
||||
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.tenant_id=? AND u.id IN (?)`
|
||||
q, args, _ = sqlx.In(q, id, usrq)
|
||||
if !keepDeleted {
|
||||
@@ -7821,6 +7847,12 @@ func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid)
|
||||
SELECT 1 FROM im_filemsglinks AS fml
|
||||
WHERE fml.tenant_id=fu.tenant_id AND fml.fileid=fu.id AND fml.userid=?
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM im_filemsglinks AS fml
|
||||
INNER JOIN im_friendships AS fs
|
||||
ON fs.user_id=? AND fs.peer_id=fml.userid AND fs.status=?
|
||||
WHERE fml.tenant_id=fu.tenant_id AND fml.fileid=fu.id AND fml.userid IS NOT NULL
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM im_filemsglinks AS fml
|
||||
INNER JOIN im_subscriptions AS s
|
||||
@@ -7828,6 +7860,12 @@ func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid)
|
||||
WHERE fml.tenant_id=fu.tenant_id AND fml.fileid=fu.id AND fml.topic IS NOT NULL
|
||||
AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM im_filemsglinks AS fml
|
||||
INNER JOIN im_group_members AS gm
|
||||
ON gm.topic=fml.topic AND gm.user_id=? AND gm.member_status=?
|
||||
WHERE fml.tenant_id=fu.tenant_id AND fml.fileid=fu.id AND fml.topic IS NOT NULL
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM im_filemsglinks AS fml
|
||||
INNER JOIN im_messages AS m
|
||||
@@ -7851,7 +7889,8 @@ func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid)
|
||||
AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0
|
||||
)
|
||||
) LIMIT 1`,
|
||||
id, decodeUID(fileID), t.UploadCompleted, decodeUID(uid), decodeUID(uid), decodeUID(uid), decodeUID(uid), decodeUID(uid),
|
||||
id, decodeUID(fileID), t.UploadCompleted, decodeUID(uid), decodeUID(uid), decodeUID(uid), t.RelationshipFriend,
|
||||
decodeUID(uid), decodeUID(uid), t.GroupMemberStatusActive, decodeUID(uid), decodeUID(uid),
|
||||
decodeUID(uid), fid, fid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -9397,7 +9436,8 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT fs.*,u.public,u.trusted,
|
||||
p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.updated_at AS profile_updated_at
|
||||
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
|
||||
JOIN im_users AS u ON u.id=fs.peer_id
|
||||
LEFT JOIN im_user_profiles AS p ON p.user_id=u.id
|
||||
@@ -9411,19 +9451,23 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init
|
||||
nextCursor := ""
|
||||
for rows.Next() {
|
||||
row := struct {
|
||||
ID int64
|
||||
UserID int64 `db:"user_id"`
|
||||
PeerID int64 `db:"peer_id"`
|
||||
Status string
|
||||
Remark string
|
||||
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"`
|
||||
ProfileUpdatedAt sql.NullTime `db:"profile_updated_at"`
|
||||
ID int64
|
||||
UserID int64 `db:"user_id"`
|
||||
PeerID int64 `db:"peer_id"`
|
||||
Status string
|
||||
Remark string
|
||||
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"`
|
||||
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"`
|
||||
ProfileUpdatedAt sql.NullTime `db:"profile_updated_at"`
|
||||
}{}
|
||||
if err = rows.StructScan(&row); err != nil {
|
||||
return nil, "", err
|
||||
@@ -9447,7 +9491,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),
|
||||
Public: profilePublicFromFields(common.FromJSON(row.Public), row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount),
|
||||
Trusted: common.FromJSON(row.Trusted),
|
||||
},
|
||||
})
|
||||
@@ -9799,7 +9843,8 @@ func (a *adapter) TenantUserSearch(id t.TenantID, viewer t.Uid, queryText, searc
|
||||
}
|
||||
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,
|
||||
profile.nickname AS profile_nickname,profile.avatar_file_id AS profile_avatar_file_id,profile.updated_at AS profile_updated_at
|
||||
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, " ")+`
|
||||
WHERE `+strings.Join(where, " AND ")+` ORDER BY u.id ASC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
|
||||
@@ -34,16 +34,22 @@ func TestProfilePublicFromFields(t *testing.T) {
|
||||
"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}).(map[string]any)
|
||||
}, 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)
|
||||
|
||||
for _, key := range []string{"fn", "name", "nickname", "display_name"} {
|
||||
if public[key] != "新昵称" {
|
||||
t.Fatalf("nickname field %s not overlaid: %#v", key, public)
|
||||
}
|
||||
}
|
||||
if public["avatar_file_id"] != "file_avatar_1" || public["public_account"] != "usrAccount" {
|
||||
if public["avatar_file_id"] != "file_avatar_1" || public["signature"] != "新签名" || public["gender"] != "male" ||
|
||||
public["region"] != "上海" || public["public_account"] != "usrProfile" {
|
||||
t.Fatalf("profile public not overlaid: %#v", public)
|
||||
}
|
||||
|
||||
@@ -51,15 +57,14 @@ func TestProfilePublicFromFields(t *testing.T) {
|
||||
"fn": "旧昵称",
|
||||
"avatar_file_id": "old_avatar",
|
||||
"public_account": "usrAccount",
|
||||
}, 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": "旧签名",
|
||||
}, 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 {
|
||||
t.Fatalf("legacy profile field %s should not be read from im_users.public: %#v", key, public)
|
||||
}
|
||||
}
|
||||
if public["public_account"] != "usrAccount" {
|
||||
t.Fatalf("unrelated public field changed: %#v", public)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileSeedFromPublic(t *testing.T) {
|
||||
|
||||
@@ -3109,6 +3109,14 @@ func serveIMMeProfile(wrt http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.AvatarFileID != nil {
|
||||
if avatar := strings.TrimSpace(*body.AvatarFileID); avatar != "" {
|
||||
if err := store.Files.LinkAttachments(principal.TenantID, principal.Uid.UserId(), types.ZeroUid, []string{avatar}); err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
profile, err := store.Profiles.Update(principal.TenantID, principal.Uid, types.UserProfilePatch{
|
||||
AvatarFileID: body.AvatarFileID,
|
||||
Nickname: body.Nickname,
|
||||
@@ -4603,6 +4611,11 @@ func serveIMGroupCreate(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
if settings.AvatarFileID != "" {
|
||||
if err := store.Files.LinkAttachments(principal.TenantID, topicName, types.ZeroUid, []string{settings.AvatarFileID}); err != nil {
|
||||
logs.Warn.Printf("group[%s]: failed to link avatar attachment: %v", topicName, err)
|
||||
}
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
created, err := store.Topics.Get(principal.TenantID, topicName)
|
||||
if err != nil {
|
||||
@@ -4897,6 +4910,13 @@ func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.AvatarFileID != nil {
|
||||
if avatar := strings.TrimSpace(*body.AvatarFileID); avatar != "" {
|
||||
if err := store.Files.LinkAttachments(principal.TenantID, topic, types.ZeroUid, []string{avatar}); err != nil {
|
||||
logs.Warn.Printf("group[%s]: failed to link avatar attachment: %v", topic, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
updated, err := store.Topics.Get(principal.TenantID, topic)
|
||||
if err != nil {
|
||||
|
||||
+79
-6
@@ -2834,7 +2834,66 @@ func (profileMapper) Update(tenantID types.TenantID, uid types.Uid, patch types.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantUserProfileUpdate(tenantID, uid, patch)
|
||||
profile, err := tenantAdp.TenantUserProfileUpdate(tenantID, uid, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refreshUserProfileProjections(tenantID, uid, profile)
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func refreshUserProfileProjections(tenantID types.TenantID, uid types.Uid, profile *types.UserProfile) {
|
||||
_ = DeleteCachedObject(tenantID, cacheNamespaceUser, uid.String())
|
||||
if profile == nil {
|
||||
return
|
||||
}
|
||||
public := userProfilePublicProjection(profile)
|
||||
cursor := ""
|
||||
for {
|
||||
items, nextCursor, err := Relations.ListFriends(tenantID, uid, "", "", cursor, 100)
|
||||
if err != nil {
|
||||
logs.Warn.Printf("profile %s: refresh friend subscription projections failed: %v", uid, err)
|
||||
return
|
||||
}
|
||||
for _, item := range items {
|
||||
if item.Friendship.Topic == "" || item.Friendship.PeerID.IsZero() {
|
||||
continue
|
||||
}
|
||||
if err := Subs.Update(tenantID, item.Friendship.Topic, item.Friendship.PeerID, map[string]any{"Public": public}); err != nil {
|
||||
logs.Warn.Printf("profile %s: refresh subscription %s/%s public failed: %v", uid, item.Friendship.Topic, item.Friendship.PeerID, err)
|
||||
}
|
||||
}
|
||||
if nextCursor == "" {
|
||||
return
|
||||
}
|
||||
cursor = nextCursor
|
||||
}
|
||||
}
|
||||
|
||||
func userProfilePublicProjection(profile *types.UserProfile) map[string]any {
|
||||
public := make(map[string]any)
|
||||
nickname := strings.TrimSpace(profile.Nickname)
|
||||
if nickname != "" {
|
||||
for _, key := range []string{"display_name", "nickname", "fn", "name"} {
|
||||
public[key] = nickname
|
||||
}
|
||||
}
|
||||
if value := strings.TrimSpace(profile.AvatarFileID); value != "" {
|
||||
public["avatar_file_id"] = value
|
||||
}
|
||||
if value := strings.TrimSpace(profile.Signature); value != "" {
|
||||
public["signature"] = value
|
||||
}
|
||||
if value := strings.TrimSpace(profile.Gender); value != "" {
|
||||
public["gender"] = value
|
||||
}
|
||||
if value := strings.TrimSpace(profile.Region); value != "" {
|
||||
public["region"] = value
|
||||
}
|
||||
if value := strings.TrimSpace(profile.PublicAccount); value != "" {
|
||||
public["public_account"] = value
|
||||
}
|
||||
return public
|
||||
}
|
||||
|
||||
func (profileMapper) GetPrivacy(tenantID types.TenantID, uid types.Uid) (*types.UserPrivacySettings, error) {
|
||||
@@ -4155,19 +4214,33 @@ func (fileMapper) DeleteUnused(tenantID types.TenantID, olderThan time.Time, lim
|
||||
// LinkAttachments connects earlier uploaded attachments to a message or topic to prevent it
|
||||
// from being garbage collected.
|
||||
func (fileMapper) LinkAttachments(tenantID types.TenantID, topic string, msgId types.Uid, attachments []string) error {
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Convert attachment URLs to file IDs.
|
||||
var fids []string
|
||||
for _, url := range attachments {
|
||||
if fid := mediaHandler.GetIdFromUrl(url); !fid.IsZero() {
|
||||
raw := strings.TrimSpace(url)
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
if mediaHandler != nil {
|
||||
if fid := mediaHandler.GetIdFromUrl(raw); !fid.IsZero() {
|
||||
fids = append(fids, fid.String())
|
||||
continue
|
||||
}
|
||||
}
|
||||
if fid := media.GetIdFromUrl(raw, ""); !fid.IsZero() {
|
||||
fids = append(fids, fid.String())
|
||||
continue
|
||||
}
|
||||
if fid := types.ParseUid(raw); !fid.IsZero() {
|
||||
fids = append(fids, fid.String())
|
||||
}
|
||||
}
|
||||
|
||||
if len(fids) > 0 {
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userId := types.ZeroUid
|
||||
if topic != "" && types.GetTopicCat(topic) == types.TopicCatMe {
|
||||
userId = types.ParseUserId(topic)
|
||||
|
||||
Reference in New Issue
Block a user