From 48ca3ae68445abca22ede649a06ebffdfebd6ad6 Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Thu, 24 Sep 2026 10:59:43 +0800 Subject: [PATCH] fix: align member meta default avatars --- server/default_avatar.go | 52 ++++++++++++++++++++++++++++++++-- server/default_avatar_test.go | 53 +++++++++++++++++++++++++++++++++-- server/im_http.go | 3 ++ server/topic.go | 6 +++- 4 files changed, 108 insertions(+), 6 deletions(-) diff --git a/server/default_avatar.go b/server/default_avatar.go index aef045c..997407e 100644 --- a/server/default_avatar.go +++ b/server/default_avatar.go @@ -16,12 +16,60 @@ const ( ) func applyDefaultUserAvatar(data map[string]any, userID, gender string) { - if data == nil || strings.TrimSpace(stringFromAny(data["avatar_file_id"])) != "" { + if data == nil { return } - if avatarURL := defaultUserAvatarURL(userID, gender); avatarURL != "" { + if strings.TrimSpace(stringFromAny(data["avatar_file_id"])) == "" { + avatarURL := defaultUserAvatarURL(userID, gender) + if avatarURL == "" { + return + } data["avatar_file_id"] = avatarURL } + applyAvatarURL(data) +} + +func applyAvatarURL(data map[string]any) { + if data == nil || strings.TrimSpace(stringFromAny(data["avatar_url"])) != "" { + return + } + avatar := strings.TrimSpace(stringFromAny(data["avatar_file_id"])) + if isAvatarURLValue(avatar) { + data["avatar_url"] = avatar + } +} + +func publicWithDefaultUserAvatar(public any, userID string) any { + data := publicToMap(public) + gender := stringFromAny(data["gender"]) + applyDefaultUserAvatar(data, userID, gender) + return data +} + +func publicToMap(public any) map[string]any { + switch value := public.(type) { + case nil: + return map[string]any{} + case map[string]any: + out := make(map[string]any, len(value)+1) + for k, v := range value { + out[k] = v + } + return out + case types.KVMap: + out := make(map[string]any, len(value)+1) + for k, v := range value { + out[k] = v + } + return out + default: + return map[string]any{} + } +} + +func isAvatarURLValue(value string) bool { + return strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") || + strings.HasPrefix(value, "data:") || strings.HasPrefix(value, "/") } func defaultUserAvatarURL(userID, gender string) string { diff --git a/server/default_avatar_test.go b/server/default_avatar_test.go index d1fbf6e..b301242 100644 --- a/server/default_avatar_test.go +++ b/server/default_avatar_test.go @@ -21,8 +21,8 @@ func (s defaultAvatarConfigStore) Get(_ string, keys ...string) (map[string]stri func TestUserSummaryPayloadAppliesDefaultAvatarByGenderAndUserID(t *testing.T) { oldConfig := store.ThirdPartyConfig store.ThirdPartyConfig = defaultAvatarConfigStore{ - imDefaultMaleAvatarURLsConfig: `["m0","m1","m2","m3","m4"]`, - imDefaultFemaleAvatarURLsConfig: `["f0","f1","f2","f3","f4"]`, + imDefaultMaleAvatarURLsConfig: `["https://oss.example.com/m0.png","https://oss.example.com/m1.png","https://oss.example.com/m2.png","https://oss.example.com/m3.png","https://oss.example.com/m4.png"]`, + imDefaultFemaleAvatarURLsConfig: `["https://oss.example.com/f0.png","https://oss.example.com/f1.png","https://oss.example.com/f2.png","https://oss.example.com/f3.png","https://oss.example.com/f4.png"]`, } defer func() { store.ThirdPartyConfig = oldConfig }() @@ -36,10 +36,13 @@ func TestUserSummaryPayloadAppliesDefaultAvatarByGenderAndUserID(t *testing.T) { if gender == "female" { wantPrefix = "f" } - want := fmt.Sprintf("%s%d", wantPrefix, uint64(uid)%5) + want := fmt.Sprintf("https://oss.example.com/%s%d.png", wantPrefix, uint64(uid)%5) if payload["avatar_file_id"] != want { t.Fatalf("gender=%s avatar=%v want %s", gender, payload["avatar_file_id"], want) } + if payload["avatar_url"] != want { + t.Fatalf("gender=%s avatar_url=%v want %s", gender, payload["avatar_url"], want) + } } } @@ -57,4 +60,48 @@ func TestUserSummaryPayloadKeepsUploadedAvatar(t *testing.T) { if payload["avatar_file_id"] != "uploaded.png" { t.Fatalf("avatar=%v", payload["avatar_file_id"]) } + if _, ok := payload["avatar_url"]; ok { + t.Fatalf("non-url uploaded avatar must not be exposed as avatar_url: %#v", payload) + } +} + +func TestPublicWithDefaultUserAvatarMatchesHTTPSummary(t *testing.T) { + oldConfig := store.ThirdPartyConfig + store.ThirdPartyConfig = defaultAvatarConfigStore{ + imDefaultMaleAvatarURLsConfig: `["https://oss.example.com/m0.png","https://oss.example.com/m1.png"]`, + imDefaultFemaleAvatarURLsConfig: `["https://oss.example.com/f0.png","https://oss.example.com/f1.png"]`, + } + defer func() { store.ThirdPartyConfig = oldConfig }() + + uid := types.Uid(11) + public := publicWithDefaultUserAvatar(map[string]any{"display_name": "Alice", "gender": "female"}, uid.UserId()).(map[string]any) + want := "https://oss.example.com/f1.png" + if public["avatar_file_id"] != want || public["avatar_url"] != want { + t.Fatalf("default avatar not applied to protocol public: %#v", public) + } + + summary := userSummaryPayload(&types.User{ + ObjHeader: types.ObjHeader{Id: uid.String()}, + Public: map[string]any{"display_name": "Alice", "gender": "female"}, + }, "") + if summary["avatar_file_id"] != public["avatar_file_id"] || summary["avatar_url"] != public["avatar_url"] { + t.Fatalf("protocol public and HTTP summary diverged: public=%#v summary=%#v", public, summary) + } +} + +func TestPublicWithDefaultUserAvatarKeepsExistingAvatarURL(t *testing.T) { + oldConfig := store.ThirdPartyConfig + store.ThirdPartyConfig = defaultAvatarConfigStore{ + imDefaultMaleAvatarURLsConfig: `["https://oss.example.com/default.png"]`, + } + defer func() { store.ThirdPartyConfig = oldConfig }() + + public := publicWithDefaultUserAvatar(types.KVMap{ + "display_name": "Bob", + "avatar_file_id": "file_1", + "avatar_url": "https://oss.example.com/file_1.png", + }, types.Uid(20).UserId()).(map[string]any) + if public["avatar_file_id"] != "file_1" || public["avatar_url"] != "https://oss.example.com/file_1.png" { + t.Fatalf("existing avatar should be kept: %#v", public) + } } diff --git a/server/im_http.go b/server/im_http.go index b945cd0..239104a 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -9143,6 +9143,9 @@ func copyUserPublicSummary(data map[string]any, public map[string]any) { break } } + if value, ok := public["avatar_url"]; ok && strings.TrimSpace(fmt.Sprint(value)) != "" { + data["avatar_url"] = value + } } func displayInitial(value any) string { diff --git a/server/topic.go b/server/topic.go index 96b13fb..81b4aff 100644 --- a/server/topic.go +++ b/server/topic.go @@ -3374,7 +3374,11 @@ func (t *Topic) replyGetSub(sess *Session, asUid types.Uid, authLevel auth.Level // Returning public and private only if they have changed since ifModified if sendPubPriv { // 'sub' has nil 'public'/'trusted' in P2P topics which is OK. - mts.Public = sub.GetPublic() + if t.cat == types.TopicCatGrp { + mts.Public = publicWithDefaultUserAvatar(sub.GetPublic(), uid.UserId()) + } else { + mts.Public = sub.GetPublic() + } mts.Trusted = sub.GetTrusted() // Reporting 'private' only if it's user's own subscription. if uid == asUid {