feat: apply configured default user avatars
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
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 || strings.TrimSpace(stringFromAny(data["avatar_file_id"])) != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if avatarURL := defaultUserAvatarURL(userID, gender); avatarURL != "" {
|
||||||
|
data["avatar_file_id"] = avatarURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tinode/chat/server/store"
|
||||||
|
"github.com/tinode/chat/server/store/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type defaultAvatarConfigStore map[string]string
|
||||||
|
|
||||||
|
func (s defaultAvatarConfigStore) Get(_ string, keys ...string) (map[string]string, error) {
|
||||||
|
out := make(map[string]string, len(keys))
|
||||||
|
for _, key := range keys {
|
||||||
|
out[key] = s[key]
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserSummaryPayloadAppliesDefaultAvatarByGenderAndUserID(t *testing.T) {
|
||||||
|
oldConfig := store.ThirdPartyConfig
|
||||||
|
store.ThirdPartyConfig = defaultAvatarConfigStore{
|
||||||
|
imDefaultMaleAvatarURLsConfig: `["m0","m1","m2","m3","m4"]`,
|
||||||
|
imDefaultFemaleAvatarURLsConfig: `["f0","f1","f2","f3","f4"]`,
|
||||||
|
}
|
||||||
|
defer func() { store.ThirdPartyConfig = oldConfig }()
|
||||||
|
|
||||||
|
for i, gender := range []string{"male", "female"} {
|
||||||
|
uid := types.Uid(10 + i)
|
||||||
|
payload := userSummaryPayload(&types.User{
|
||||||
|
ObjHeader: types.ObjHeader{Id: uid.String()},
|
||||||
|
Public: map[string]any{"display_name": "Alice", "gender": gender},
|
||||||
|
}, "")
|
||||||
|
wantPrefix := "m"
|
||||||
|
if gender == "female" {
|
||||||
|
wantPrefix = "f"
|
||||||
|
}
|
||||||
|
want := fmt.Sprintf("%s%d", wantPrefix, uint64(uid)%5)
|
||||||
|
if payload["avatar_file_id"] != want {
|
||||||
|
t.Fatalf("gender=%s avatar=%v want %s", gender, payload["avatar_file_id"], want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserSummaryPayloadKeepsUploadedAvatar(t *testing.T) {
|
||||||
|
oldConfig := store.ThirdPartyConfig
|
||||||
|
store.ThirdPartyConfig = defaultAvatarConfigStore{
|
||||||
|
imDefaultMaleAvatarURLsConfig: `["m0","m1","m2","m3","m4"]`,
|
||||||
|
}
|
||||||
|
defer func() { store.ThirdPartyConfig = oldConfig }()
|
||||||
|
|
||||||
|
payload := userSummaryPayload(&types.User{
|
||||||
|
ObjHeader: types.ObjHeader{Id: types.Uid(10).String()},
|
||||||
|
Public: map[string]any{"display_name": "Alice", "gender": "male", "avatar_file_id": "uploaded.png"},
|
||||||
|
}, "")
|
||||||
|
if payload["avatar_file_id"] != "uploaded.png" {
|
||||||
|
t.Fatalf("avatar=%v", payload["avatar_file_id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-2
@@ -3252,10 +3252,12 @@ func serveIMMe(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
writeIMRelationError(wrt, req, err)
|
writeIMRelationError(wrt, req, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
profilePayload := userProfilePayload10(profile)
|
||||||
|
applyDefaultUserAvatar(profilePayload, principal.Uid.UserId(), profile.Gender)
|
||||||
writeIMSuccess(wrt, req, map[string]any{
|
writeIMSuccess(wrt, req, map[string]any{
|
||||||
"user_id": principal.Uid.UserId(),
|
"user_id": principal.Uid.UserId(),
|
||||||
"username": account["username"],
|
"username": account["username"],
|
||||||
"profile": userProfilePayload10(profile),
|
"profile": profilePayload,
|
||||||
"profile_version": profile.Version,
|
"profile_version": profile.Version,
|
||||||
"realname": map[string]any{"status": "unknown"},
|
"realname": map[string]any{"status": "unknown"},
|
||||||
"youth_mode": map[string]any{"enabled": false},
|
"youth_mode": map[string]any{"enabled": false},
|
||||||
@@ -10104,6 +10106,7 @@ func currentUserProfilePayload(tenantID types.TenantID, uid types.Uid, profile *
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
data := userProfilePayload10(profile)
|
data := userProfilePayload10(profile)
|
||||||
|
applyDefaultUserAvatar(data, uid.UserId(), profile.Gender)
|
||||||
data["phone_mask"] = account["phone_mask"]
|
data["phone_mask"] = account["phone_mask"]
|
||||||
data["username"] = account["username"]
|
data["username"] = account["username"]
|
||||||
return data, nil
|
return data, nil
|
||||||
@@ -10721,10 +10724,14 @@ func userSummaryPayload(user *types.User, remark string) map[string]any {
|
|||||||
userID = user.Id
|
userID = user.Id
|
||||||
}
|
}
|
||||||
data := map[string]any{"user_id": userID}
|
data := map[string]any{"user_id": userID}
|
||||||
|
gender := ""
|
||||||
if public, ok := user.Public.(map[string]any); ok {
|
if public, ok := user.Public.(map[string]any); ok {
|
||||||
copyUserPublicSummary(data, public)
|
copyUserPublicSummary(data, public)
|
||||||
|
gender = stringFromAny(public["gender"])
|
||||||
} else if public, ok := user.Public.(types.KVMap); ok {
|
} else if public, ok := user.Public.(types.KVMap); ok {
|
||||||
copyUserPublicSummary(data, map[string]any(public))
|
publicMap := map[string]any(public)
|
||||||
|
copyUserPublicSummary(data, publicMap)
|
||||||
|
gender = stringFromAny(publicMap["gender"])
|
||||||
}
|
}
|
||||||
if remark != "" {
|
if remark != "" {
|
||||||
data["display_name"] = remark
|
data["display_name"] = remark
|
||||||
@@ -10732,6 +10739,7 @@ func userSummaryPayload(user *types.User, remark string) map[string]any {
|
|||||||
if _, ok := data["display_name"]; !ok {
|
if _, ok := data["display_name"]; !ok {
|
||||||
data["display_name"] = userID
|
data["display_name"] = userID
|
||||||
}
|
}
|
||||||
|
applyDefaultUserAvatar(data, userID, gender)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user