32
32
SynapseError ,
33
33
)
34
34
from synapse .storage .databases .main .media_repository import LocalMedia , RemoteMedia
35
- from synapse .types import JsonDict , Requester , UserID , create_requester
35
+ from synapse .types import JsonDict , JsonValue , Requester , UserID , create_requester
36
36
from synapse .util .caches .descriptors import cached
37
37
from synapse .util .stringutils import parse_and_validate_mxc_uri
38
38
43
43
44
44
MAX_DISPLAYNAME_LEN = 256
45
45
MAX_AVATAR_URL_LEN = 1000
46
+ # Field name length is specced at 255 bytes.
47
+ MAX_CUSTOM_FIELD_LEN = 255
46
48
47
49
48
50
class ProfileHandler :
@@ -90,7 +92,15 @@ async def get_profile(self, user_id: str, ignore_backoff: bool = True) -> JsonDi
90
92
91
93
if self .hs .is_mine (target_user ):
92
94
profileinfo = await self .store .get_profileinfo (target_user )
93
- if profileinfo .display_name is None and profileinfo .avatar_url is None :
95
+ extra_fields = {}
96
+ if self .hs .config .experimental .msc4133_enabled :
97
+ extra_fields = await self .store .get_profile_fields (target_user )
98
+
99
+ if (
100
+ profileinfo .display_name is None
101
+ and profileinfo .avatar_url is None
102
+ and not extra_fields
103
+ ):
94
104
raise SynapseError (404 , "Profile was not found" , Codes .NOT_FOUND )
95
105
96
106
# Do not include display name or avatar if unset.
@@ -99,6 +109,9 @@ async def get_profile(self, user_id: str, ignore_backoff: bool = True) -> JsonDi
99
109
ret [ProfileFields .DISPLAYNAME ] = profileinfo .display_name
100
110
if profileinfo .avatar_url is not None :
101
111
ret [ProfileFields .AVATAR_URL ] = profileinfo .avatar_url
112
+ if extra_fields :
113
+ ret .update (extra_fields )
114
+
102
115
return ret
103
116
else :
104
117
try :
@@ -403,6 +416,110 @@ async def check_avatar_size_and_mime_type(self, mxc: str) -> bool:
403
416
404
417
return True
405
418
419
+ async def get_profile_field (
420
+ self , target_user : UserID , field_name : str
421
+ ) -> JsonValue :
422
+ """
423
+ Fetch a user's profile from the database for local users and over federation
424
+ for remote users.
425
+
426
+ Args:
427
+ target_user: The user ID to fetch the profile for.
428
+ field_name: The field to fetch the profile for.
429
+
430
+ Returns:
431
+ The value for the profile field or None if the field does not exist.
432
+ """
433
+ if self .hs .is_mine (target_user ):
434
+ try :
435
+ field_value = await self .store .get_profile_field (
436
+ target_user , field_name
437
+ )
438
+ except StoreError as e :
439
+ if e .code == 404 :
440
+ raise SynapseError (404 , "Profile was not found" , Codes .NOT_FOUND )
441
+ raise
442
+
443
+ return field_value
444
+ else :
445
+ try :
446
+ result = await self .federation .make_query (
447
+ destination = target_user .domain ,
448
+ query_type = "profile" ,
449
+ args = {"user_id" : target_user .to_string (), "field" : field_name },
450
+ ignore_backoff = True ,
451
+ )
452
+ except RequestSendFailed as e :
453
+ raise SynapseError (502 , "Failed to fetch profile" ) from e
454
+ except HttpResponseException as e :
455
+ raise e .to_synapse_error ()
456
+
457
+ return result .get (field_name )
458
+
459
+ async def set_profile_field (
460
+ self ,
461
+ target_user : UserID ,
462
+ requester : Requester ,
463
+ field_name : str ,
464
+ new_value : JsonValue ,
465
+ by_admin : bool = False ,
466
+ deactivation : bool = False ,
467
+ ) -> None :
468
+ """Set a new profile field for a user.
469
+
470
+ Args:
471
+ target_user: the user whose profile is to be changed.
472
+ requester: The user attempting to make this change.
473
+ field_name: The name of the profile field to update.
474
+ new_value: The new field value for this user.
475
+ by_admin: Whether this change was made by an administrator.
476
+ deactivation: Whether this change was made while deactivating the user.
477
+ """
478
+ if not self .hs .is_mine (target_user ):
479
+ raise SynapseError (400 , "User is not hosted on this homeserver" )
480
+
481
+ if not by_admin and target_user != requester .user :
482
+ raise AuthError (403 , "Cannot set another user's profile" )
483
+
484
+ await self .store .set_profile_field (target_user , field_name , new_value )
485
+
486
+ # Custom fields do not propagate into the user directory *or* rooms.
487
+ profile = await self .store .get_profileinfo (target_user )
488
+ await self ._third_party_rules .on_profile_update (
489
+ target_user .to_string (), profile , by_admin , deactivation
490
+ )
491
+
492
+ async def delete_profile_field (
493
+ self ,
494
+ target_user : UserID ,
495
+ requester : Requester ,
496
+ field_name : str ,
497
+ by_admin : bool = False ,
498
+ deactivation : bool = False ,
499
+ ) -> None :
500
+ """Delete a field from a user's profile.
501
+
502
+ Args:
503
+ target_user: the user whose profile is to be changed.
504
+ requester: The user attempting to make this change.
505
+ field_name: The name of the profile field to remove.
506
+ by_admin: Whether this change was made by an administrator.
507
+ deactivation: Whether this change was made while deactivating the user.
508
+ """
509
+ if not self .hs .is_mine (target_user ):
510
+ raise SynapseError (400 , "User is not hosted on this homeserver" )
511
+
512
+ if not by_admin and target_user != requester .user :
513
+ raise AuthError (400 , "Cannot set another user's profile" )
514
+
515
+ await self .store .delete_profile_field (target_user , field_name )
516
+
517
+ # Custom fields do not propagate into the user directory *or* rooms.
518
+ profile = await self .store .get_profileinfo (target_user )
519
+ await self ._third_party_rules .on_profile_update (
520
+ target_user .to_string (), profile , by_admin , deactivation
521
+ )
522
+
406
523
async def on_profile_query (self , args : JsonDict ) -> JsonDict :
407
524
"""Handles federation profile query requests."""
408
525
@@ -419,13 +536,24 @@ async def on_profile_query(self, args: JsonDict) -> JsonDict:
419
536
420
537
just_field = args .get ("field" , None )
421
538
422
- response = {}
539
+ response : JsonDict = {}
423
540
try :
424
- if just_field is None or just_field == "displayname" :
541
+ if just_field is None or just_field == ProfileFields . DISPLAYNAME :
425
542
response ["displayname" ] = await self .store .get_profile_displayname (user )
426
543
427
- if just_field is None or just_field == "avatar_url" :
544
+ if just_field is None or just_field == ProfileFields . AVATAR_URL :
428
545
response ["avatar_url" ] = await self .store .get_profile_avatar_url (user )
546
+
547
+ if self .hs .config .experimental .msc4133_enabled :
548
+ if just_field is None :
549
+ response .update (await self .store .get_profile_fields (user ))
550
+ elif just_field not in (
551
+ ProfileFields .DISPLAYNAME ,
552
+ ProfileFields .AVATAR_URL ,
553
+ ):
554
+ response [just_field ] = await self .store .get_profile_field (
555
+ user , just_field
556
+ )
429
557
except StoreError as e :
430
558
if e .code == 404 :
431
559
raise SynapseError (404 , "Profile was not found" , Codes .NOT_FOUND )
0 commit comments