package main import ( "encoding/json" "hash/fnv" "strings" "github.com/tinode/chat/server/store" "github.com/tinode/chat/server/store/types" ) const ( imAssetsConfigGroup = "im_assets" imDefaultMaleAvatarURLsConfig = "default_user_avatar_male_urls" imDefaultFemaleAvatarURLsConfig = "default_user_avatar_female_urls" ) func applyDefaultUserAvatar(data map[string]any, userID, gender string) { if data == nil { return } 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 return } // 裸文件 ID:统一补一个可直接下载的 avatar_url。 // 下载走 /v0/file/s/{file_id},自定义头像的 ACL 对登录用户放行(见 adapter.FileUserHasAccess)。 if avatar != "" && !types.ParseUid(avatar).IsZero() { data["avatar_url"] = "/v0/file/s/" + 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 { if store.ThirdPartyConfig == nil { return "" } values, err := store.ThirdPartyConfig.Get(imAssetsConfigGroup, imDefaultMaleAvatarURLsConfig, imDefaultFemaleAvatarURLsConfig) if err != nil { return "" } key := imDefaultMaleAvatarURLsConfig if strings.EqualFold(strings.TrimSpace(gender), "female") { key = imDefaultFemaleAvatarURLsConfig } urls := parseDefaultAvatarURLs(values[key]) if len(urls) == 0 && key == imDefaultFemaleAvatarURLsConfig { urls = parseDefaultAvatarURLs(values[imDefaultMaleAvatarURLsConfig]) } if len(urls) == 0 { return "" } return urls[defaultAvatarIndex(userID, len(urls))] } func parseDefaultAvatarURLs(raw string) []string { var urls []string if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &urls); err != nil { return nil } out := urls[:0] for _, url := range urls { if url = strings.TrimSpace(url); url != "" { out = append(out, url) } } return out } func defaultAvatarIndex(userID string, count int) int { if count <= 1 { return 0 } uid := types.ParseUserId(strings.TrimSpace(userID)) if uid.IsZero() { uid = types.ParseUid(strings.TrimSpace(userID)) } if !uid.IsZero() { return int(uint64(uid) % uint64(count)) } h := fnv.New32a() _, _ = h.Write([]byte(userID)) return int(h.Sum32() % uint32(count)) }