feat: expose account fields in user profiles

This commit is contained in:
richjohnrambo
2026-09-17 13:37:34 +08:00
parent 816be46e03
commit 734e042af6
3 changed files with 103 additions and 9 deletions
+43 -2
View File
@@ -3191,7 +3191,12 @@ func serveIMMeProfile(wrt http.ResponseWriter, req *http.Request) {
writeIMRelationError(wrt, req, err)
return
}
writeIMSuccess(wrt, req, userProfilePayload10(profile))
payload, err := currentUserProfilePayload(principal.TenantID, principal.Uid, profile)
if err != nil {
writeIMRelationError(wrt, req, err)
return
}
writeIMSuccess(wrt, req, payload)
case http.MethodPatch:
var body struct {
AvatarFileID *string `json:"avatar_file_id"`
@@ -3240,7 +3245,12 @@ func serveIMMeProfile(wrt http.ResponseWriter, req *http.Request) {
writeIMRelationError(wrt, req, err)
return
}
writeIMSuccess(wrt, req, userProfilePayload10(profile))
payload, err := currentUserProfilePayload(principal.TenantID, principal.Uid, profile)
if err != nil {
writeIMRelationError(wrt, req, err)
return
}
writeIMSuccess(wrt, req, payload)
default:
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
}
@@ -9799,6 +9809,21 @@ func userProfilePayload10(profile *types.UserProfile) map[string]any {
return data
}
func currentUserProfilePayload(tenantID types.TenantID, uid types.Uid, profile *types.UserProfile) (map[string]any, error) {
user, err := store.Users.Get(tenantID, uid)
if err != nil {
return nil, err
}
data := userProfilePayload10(profile)
data["phone_mask"] = ""
data["username"] = ""
if user != nil {
data["phone_mask"] = maskIMProfilePhone(user.Phone)
data["username"] = user.Username
}
return data, nil
}
func validIMSecurityChallengeScene(scene string) bool {
switch strings.TrimSpace(scene) {
case "set_independent_password", "enable_youth_mode", "close_youth_mode", "reset_independent_password",
@@ -10280,6 +10305,7 @@ func blockListItemPayload(item types.BlockListItem) map[string]any {
func userProfilePayload(user *types.User, ctx *types.RelationshipContext) map[string]any {
data := userSummaryPayload(user, ctx.Remark)
data["username"] = user.Username
data["relationship"] = ctx.Relationship
data["topic"] = ctx.Topic
data["capabilities"] = map[string]any{
@@ -10303,6 +10329,7 @@ func userProfilePayload(user *types.User, ctx *types.RelationshipContext) map[st
func blockedUserProfilePayload(user *types.User, ctx *types.RelationshipContext) map[string]any {
data := userSummaryPayload(user, ctx.Remark)
data["username"] = user.Username
data["relationship"] = ctx.Relationship
data["topic"] = ctx.Topic
data["capabilities"] = map[string]any{
@@ -11365,6 +11392,20 @@ func maskIMPhone(phone string) string {
return phone[:4] + "****" + phone[len(phone)-3:]
}
func maskIMProfilePhone(phone string) string {
phone = strings.TrimSpace(phone)
if strings.HasPrefix(phone, "+86") {
phone = strings.TrimPrefix(phone, "+86")
}
if phone == "" {
return ""
}
if len(phone) <= 7 {
return phone
}
return phone[:3] + "****" + phone[len(phone)-4:]
}
func randomIMNumericCode(length int) (string, error) {
max := big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(length)), nil)
n, err := rand.Int(rand.Reader, max)
+56 -3
View File
@@ -3821,6 +3821,11 @@ func TestServeIMMeProfile(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
expectIMBearer(t, ctrl, "token-1", 7, 42)
oldUsers := store.Users
users := mock_store.NewMockUsersPersistenceInterface(ctrl)
users.EXPECT().Get(types.TenantID(7), types.Uid(42)).Return(&types.User{Phone: "+8613800138000", Username: "gcharles7"}, nil)
store.Users = users
defer func() { store.Users = oldUsers }()
oldProfiles := store.Profiles
profiles := &testProfilesStore{}
store.Profiles = profiles
@@ -3835,11 +3840,57 @@ func TestServeIMMeProfile(t *testing.T) {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if data["nickname"] != "新昵称" || data["email"] != "me@example.com" || data["version"].(float64) != 2 {
if data["nickname"] != "新昵称" || data["email"] != "me@example.com" || data["version"].(float64) != 2 ||
data["phone_mask"] != "138****8000" || data["username"] != "gcharles7" {
t.Fatalf("unexpected profile: %#v", data)
}
}
func TestServeIMMeProfileGetPhoneMask(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
expectIMBearer(t, ctrl, "token-1", 7, 42)
oldUsers := store.Users
users := mock_store.NewMockUsersPersistenceInterface(ctrl)
users.EXPECT().Get(types.TenantID(7), types.Uid(42)).Return(&types.User{Phone: "+8613600003333", Username: "gcharles7"}, nil)
store.Users = users
defer func() { store.Users = oldUsers }()
oldProfiles := store.Profiles
store.Profiles = &testProfilesStore{}
defer func() { store.Profiles = oldProfiles }()
req := httptest.NewRequest(http.MethodGet, "/v1/im/me/profile", nil)
req.Header.Set("Authorization", "Bearer token-1")
rec := httptest.NewRecorder()
serveIMMeProfile(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if data["phone_mask"] != "136****3333" || data["username"] != "gcharles7" {
t.Fatalf("unexpected account fields: %#v body=%s", data, rec.Body.String())
}
}
func TestMaskIMProfilePhone(t *testing.T) {
for _, tc := range []struct {
name string
phone string
want string
}{
{name: "empty", phone: "", want: ""},
{name: "mainland e164", phone: "+8613600003333", want: "136****3333"},
{name: "mainland local", phone: "13600003333", want: "136****3333"},
} {
t.Run(tc.name, func(t *testing.T) {
if got := maskIMProfilePhone(tc.phone); got != tc.want {
t.Fatalf("maskIMProfilePhone(%q)=%q, want %q", tc.phone, got, tc.want)
}
})
}
}
func TestServeIMMePrivacy(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -8907,6 +8958,7 @@ func TestServeIMUserProfile(t *testing.T) {
users.EXPECT().Get(types.TenantID(7), peer).Return(&types.User{
ObjHeader: types.ObjHeader{Id: peer.String()},
TenantID: 7,
Username: "alice7",
Public: map[string]any{"display_name": "Alice", "nickname": "Alice", "avatar_file_id": "file-1", "signature": "hello"},
}, nil)
store.Users = users
@@ -8936,7 +8988,7 @@ func TestServeIMUserProfile(t *testing.T) {
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if data["relationship"] != types.RelationshipFriend || data["signature"] != "hello" || data["topic"] == "" {
if data["relationship"] != types.RelationshipFriend || data["signature"] != "hello" || data["topic"] == "" || data["username"] != "alice7" {
t.Fatalf("unexpected profile response: %#v", data)
}
if data["display_name"] != "老同事" || data["remark"] != "老同事" {
@@ -8959,6 +9011,7 @@ func TestServeIMUserProfileBlockedByMeReturnsMinimalProfile(t *testing.T) {
users.EXPECT().Get(types.TenantID(7), peer).Return(&types.User{
ObjHeader: types.ObjHeader{Id: peer.String()},
TenantID: 7,
Username: "alice7",
Public: map[string]any{
"display_name": "Alice",
"nickname": "Alice",
@@ -8996,7 +9049,7 @@ func TestServeIMUserProfileBlockedByMeReturnsMinimalProfile(t *testing.T) {
}
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if data["relationship"] != types.RelationshipBlockedByMe || data["display_name"] != "老同事" ||
data["avatar_file_id"] != "file-1" || data["topic"] == "" {
data["avatar_file_id"] != "file-1" || data["topic"] == "" || data["username"] != "alice7" {
t.Fatalf("unexpected blocked profile response: %#v", data)
}
capabilities := data["capabilities"].(map[string]any)