diff --git a/server/db/mysql/adapter.go b/server/db/mysql/adapter.go index 697f9ce..a188e84 100644 --- a/server/db/mysql/adapter.go +++ b/server/db/mysql/adapter.go @@ -2630,10 +2630,11 @@ 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.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.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,pu.location AS profile_avatar_url,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 LEFT JOIN im_user_profiles AS p ON p.user_id=s.userid + LEFT JOIN im_public_uploads AS pu ON pu.file_id=p.avatar_file_id WHERE s.topic=?` args := []any{topic} if !keepDeleted { @@ -2686,21 +2687,21 @@ func (a *adapter) UsersForTopic(topic string, keepDeleted bool, opts *t.QueryOpt var lastSeen sql.NullTime var userAgent string var trusted any - var profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount sql.NullString + var profileNickname, profileAvatarFileID, profileAvatarURL, 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, &trusted, &lastSeen, &userAgent, &sub.Private, - &profileNickname, &profileAvatarFileID, &profileSignature, + &profileNickname, &profileAvatarFileID, &profileAvatarURL, &profileSignature, &profileGender, &profileRegion, &profilePublicAccount); err != nil { break } sub.User = common.EncodeUidString(sub.User).String() sub.Private = common.FromJSON(sub.Private) - sub.SetPublic(profilePublicFromFields(profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount)) + sub.SetPublic(profilePublicFromFieldsWithAvatarURL(profileNickname, profileAvatarFileID, profileAvatarURL, 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 6a5492f..fe80b01 100644 --- a/server/db/mysql/tenant_adapter.go +++ b/server/db/mysql/tenant_adapter.go @@ -62,6 +62,7 @@ type profileUserRow struct { Trusted any `db:"trusted"` ProfileNickname sql.NullString `db:"profile_nickname"` ProfileAvatarFileID sql.NullString `db:"profile_avatar_file_id"` + ProfileAvatarURL sql.NullString `db:"profile_avatar_url"` ProfileSignature sql.NullString `db:"profile_signature"` ProfileGender sql.NullString `db:"profile_gender"` ProfileRegion sql.NullString `db:"profile_region"` @@ -85,7 +86,7 @@ func (row profileUserRow) lastSeen() *time.Time { } func (row profileUserRow) profilePublic() any { - return profilePublicFromFields(row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount) + return profilePublicFromFieldsWithAvatarURL(row.ProfileNickname, row.ProfileAvatarFileID, row.ProfileAvatarURL, row.ProfileSignature, row.ProfileGender, row.ProfileRegion, row.ProfilePublicAccount) } func profileUserFromRow(row profileUserRow) t.User { @@ -111,6 +112,10 @@ func profileUserFromRow(row profileUserRow) t.User { } func profilePublicFromFields(nickname, avatarFileID, signature, gender, region, publicAccount sql.NullString) any { + return profilePublicFromFieldsWithAvatarURL(nickname, avatarFileID, sql.NullString{}, signature, gender, region, publicAccount) +} + +func profilePublicFromFieldsWithAvatarURL(nickname, avatarFileID, avatarURL, signature, gender, region, publicAccount sql.NullString) any { public := map[string]any{} if nickname.Valid { value := strings.TrimSpace(nickname.String) @@ -124,6 +129,9 @@ func profilePublicFromFields(nickname, avatarFileID, signature, gender, region, value := strings.TrimSpace(avatarFileID.String) if value != "" { public["avatar_file_id"] = value + if url := profileAvatarURL(value, avatarURL); url != "" { + public["avatar_url"] = url + } } } if signature.Valid { @@ -186,6 +194,20 @@ func sanitizeUserUpdate(update map[string]any) map[string]any { return update } +func profileAvatarURL(avatarFileID string, avatarURL sql.NullString) string { + if avatarURL.Valid { + if value := strings.TrimSpace(avatarURL.String); value != "" { + return value + } + } + value := strings.TrimSpace(avatarFileID) + if strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") || + strings.HasPrefix(value, "data:") || strings.HasPrefix(value, "/") { + return value + } + return "" +} + func profileSeedFromPublic(public any) (nickname, avatarFileID string) { values := publicMap(public) for _, key := range []string{"display_name", "nickname", "fn", "name"} { @@ -1302,10 +1324,11 @@ 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.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.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,pu.location AS profile_avatar_url,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 LEFT JOIN im_user_profiles AS p ON p.tenant_id=s.tenant_id AND p.user_id=s.userid + LEFT JOIN im_public_uploads AS pu ON pu.file_id=p.avatar_file_id WHERE s.tenant_id=? AND s.topic=?` q = tenantShardSQL(id, q) args := []any{id, topic} @@ -1347,17 +1370,17 @@ func (a *adapter) TenantUsersForTopic(id t.TenantID, topic string, keepDeleted b var sub t.Subscription var lastSeen sql.NullTime var trusted any - var profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount sql.NullString + var profileNickname, profileAvatarFileID, profileAvatarURL, 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, &trusted, &lastSeen, &userAgent, &sub.Private, - &profileNickname, &profileAvatarFileID, &profileSignature, + &profileNickname, &profileAvatarFileID, &profileAvatarURL, &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(profileNickname, profileAvatarFileID, profileSignature, profileGender, profileRegion, profilePublicAccount)) + sub.SetPublic(profilePublicFromFieldsWithAvatarURL(profileNickname, profileAvatarFileID, profileAvatarURL, profileSignature, profileGender, profileRegion, profilePublicAccount)) sub.SetTrusted(common.FromJSON(trusted)) sub.SetLastSeenAndUA(&lastSeen.Time, userAgent) subs = append(subs, sub) diff --git a/server/db/mysql/user_identity_test.go b/server/db/mysql/user_identity_test.go index 678e9fb..15adf3f 100644 --- a/server/db/mysql/user_identity_test.go +++ b/server/db/mysql/user_identity_test.go @@ -42,9 +42,22 @@ func TestProfilePublicFromFields(t *testing.T) { t.Fatalf("profile public not overlaid: %#v", public) } + public = profilePublicFromFieldsWithAvatarURL(sql.NullString{}, sql.NullString{String: "file_avatar_1", Valid: true}, + sql.NullString{String: "https://cdn.example.com/avatar.png", Valid: true}, sql.NullString{}, sql.NullString{}, + sql.NullString{}, sql.NullString{}).(map[string]any) + if public["avatar_file_id"] != "file_avatar_1" || public["avatar_url"] != "https://cdn.example.com/avatar.png" { + t.Fatalf("avatar url not overlaid: %#v", public) + } + + public = profilePublicFromFields(sql.NullString{}, sql.NullString{String: "https://cdn.example.com/default.png", Valid: true}, + sql.NullString{}, sql.NullString{}, sql.NullString{}, sql.NullString{}).(map[string]any) + if public["avatar_file_id"] != "https://cdn.example.com/default.png" || public["avatar_url"] != "https://cdn.example.com/default.png" { + t.Fatalf("absolute avatar value should be exposed as avatar_url: %#v", public) + } + 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"} { + for _, key := range []string{"fn", "name", "nickname", "display_name", "avatar_file_id", "avatar_url", "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) }