Compare commits
4 Commits
devon/ssex
...
hs/as-prof
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7aaa267b54 | ||
|
|
13ffee45ce | ||
|
|
5abb4cf7a6 | ||
|
|
c213505b8c |
@@ -88,6 +88,7 @@ class ApplicationService:
|
||||
supports_ephemeral: bool = False,
|
||||
msc3202_transaction_extensions: bool = False,
|
||||
msc4190_device_management: bool = False,
|
||||
supports_profile_lookup: bool = False,
|
||||
):
|
||||
self.token = token
|
||||
self.url = (
|
||||
@@ -102,6 +103,7 @@ class ApplicationService:
|
||||
self.id = id
|
||||
self.ip_range_whitelist = ip_range_whitelist
|
||||
self.supports_ephemeral = supports_ephemeral
|
||||
self.supports_profile_lookup = supports_profile_lookup
|
||||
self.msc3202_transaction_extensions = msc3202_transaction_extensions
|
||||
self.msc4190_device_management = msc4190_device_management
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ from synapse.logging import opentracing
|
||||
from synapse.metrics import SERVER_NAME_LABEL
|
||||
from synapse.types import DeviceListUpdates, JsonDict, JsonMapping, ThirdPartyInstanceID
|
||||
from synapse.util.caches.response_cache import ResponseCache
|
||||
from synapse.types import (
|
||||
UserID,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
@@ -259,6 +262,53 @@ class ApplicationServiceApi(SimpleHttpClient):
|
||||
logger.warning("query_3pe to %s threw exception %s", service.url, ex)
|
||||
return []
|
||||
|
||||
async def query_profile(
|
||||
self,
|
||||
service: "ApplicationService",
|
||||
user_id: str,
|
||||
from_user_id: Optional[UserID] = None,
|
||||
key: Optional[str] = None,
|
||||
origin_server: Optional[str] = None,
|
||||
) -> Optional[JsonDict]:
|
||||
if service.url is None:
|
||||
return None
|
||||
|
||||
# This is required by the configuration.
|
||||
assert service.hs_token is not None
|
||||
|
||||
try:
|
||||
args = {}
|
||||
if self.config.use_appservice_legacy_authorization:
|
||||
args["access_token"] = service.hs_token
|
||||
if from_user_id:
|
||||
args["from_user_id"] = from_user_id.to_string()
|
||||
if origin_server:
|
||||
args["origin_server"] = origin_server
|
||||
|
||||
url = f"{service.url}{APP_SERVICE_PREFIX}/profile/{urllib.parse.quote(user_id)}"
|
||||
|
||||
if key:
|
||||
url += f"/{urllib.parse.quote(key)}"
|
||||
|
||||
response = await self.get_json(
|
||||
url,
|
||||
args,
|
||||
headers=self._get_headers(service),
|
||||
)
|
||||
if key:
|
||||
if key in response:
|
||||
return {key: response[key]}
|
||||
else:
|
||||
raise Exception(f"Missing {key} in response to profile request")
|
||||
return response
|
||||
except CodeMessageException as e:
|
||||
if e.code == 404:
|
||||
return None
|
||||
logger.warning("query_user to %s received %s", service.url, e.code)
|
||||
except Exception as ex:
|
||||
logger.warning("query_user to %s threw exception %s", service.url, ex)
|
||||
return None
|
||||
|
||||
async def get_3pe_protocol(
|
||||
self, service: "ApplicationService", protocol: str
|
||||
) -> Optional[JsonDict]:
|
||||
|
||||
@@ -170,6 +170,7 @@ def _load_appservice(
|
||||
ip_range_whitelist = IPSet(as_info.get("ip_range_whitelist"))
|
||||
|
||||
supports_ephemeral = as_info.get("de.sorunome.msc2409.push_ephemeral", False)
|
||||
lookup_profiles = as_info.get("uk.half-shot.lookup_profile", False)
|
||||
|
||||
# Opt-in flag for the MSC3202-specific transactional behaviour.
|
||||
# When enabled, appservice transactions contain the following information:
|
||||
@@ -205,6 +206,7 @@ def _load_appservice(
|
||||
rate_limited=rate_limited,
|
||||
ip_range_whitelist=ip_range_whitelist,
|
||||
supports_ephemeral=supports_ephemeral,
|
||||
supports_profile_lookup=lookup_profiles,
|
||||
msc3202_transaction_extensions=msc3202_transaction_extensions,
|
||||
msc4190_device_management=msc4190_enabled,
|
||||
)
|
||||
|
||||
@@ -736,6 +736,27 @@ class ApplicationServicesHandler:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def query_profile(
|
||||
self, user_id: str, from_user_id: Optional[UserID] = None, key: Optional[str] = None, origin_server: Optional[str] = None
|
||||
) -> Optional[JsonDict]:
|
||||
"""Check if any application service knows this user_id exists.
|
||||
|
||||
Args:
|
||||
user_id: The user to query if they exist on any AS.
|
||||
Returns:
|
||||
True if this user exists on at least one application service.
|
||||
"""
|
||||
user_query_services = self._get_services_for_user(user_id=user_id)
|
||||
accumulated_profile = {}
|
||||
for user_service in user_query_services:
|
||||
if user_service.supports_profile_lookup:
|
||||
profile = await self.appservice_api.query_profile(
|
||||
user_service, user_id, from_user_id, key, origin_server
|
||||
)
|
||||
if profile:
|
||||
accumulated_profile.update(profile)
|
||||
return accumulated_profile
|
||||
|
||||
async def query_room_alias_exists(
|
||||
self, room_alias: RoomAlias
|
||||
) -> Optional[RoomAliasMapping]:
|
||||
|
||||
@@ -492,6 +492,10 @@ class EventCreationHandler:
|
||||
self.store = hs.get_datastores().main
|
||||
self._storage_controllers = hs.get_storage_controllers()
|
||||
self.state = hs.get_state_handler()
|
||||
self.clock = hs.get_clock()
|
||||
self.validator = EventValidator()
|
||||
self.event_builder_factory = hs.get_event_builder_factory()
|
||||
self.server_name = hs.hostname
|
||||
self.profile_handler = hs.get_profile_handler()
|
||||
self.notifier = hs.get_notifier()
|
||||
self.config = hs.config
|
||||
|
||||
@@ -77,7 +77,14 @@ class ProfileHandler:
|
||||
|
||||
self._third_party_rules = hs.get_module_api_callbacks().third_party_event_rules
|
||||
|
||||
async def get_profile(self, user_id: str, ignore_backoff: bool = True) -> JsonDict:
|
||||
self._as = hs.get_application_service_handler()
|
||||
|
||||
async def get_profile(
|
||||
self,
|
||||
user_id: str,
|
||||
ignore_backoff: bool = True,
|
||||
from_user_id: Optional[UserID] = None,
|
||||
) -> JsonDict:
|
||||
"""
|
||||
Get a user's profile as a JSON dictionary.
|
||||
|
||||
@@ -90,6 +97,7 @@ class ProfileHandler:
|
||||
fields, if set. For remote queries it may contain arbitrary information.
|
||||
"""
|
||||
target_user = UserID.from_string(user_id)
|
||||
ret = {}
|
||||
|
||||
if self.hs.is_mine(target_user):
|
||||
profileinfo = await self.store.get_profileinfo(target_user)
|
||||
@@ -103,15 +111,12 @@ class ProfileHandler:
|
||||
raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
|
||||
|
||||
# Do not include display name or avatar if unset.
|
||||
ret = {}
|
||||
if profileinfo.display_name is not None:
|
||||
ret[ProfileFields.DISPLAYNAME] = profileinfo.display_name
|
||||
if profileinfo.avatar_url is not None:
|
||||
ret[ProfileFields.AVATAR_URL] = profileinfo.avatar_url
|
||||
if extra_fields:
|
||||
ret.update(extra_fields)
|
||||
|
||||
return ret
|
||||
else:
|
||||
try:
|
||||
result = await self.federation.make_query(
|
||||
@@ -120,7 +125,7 @@ class ProfileHandler:
|
||||
args={"user_id": user_id},
|
||||
ignore_backoff=ignore_backoff,
|
||||
)
|
||||
return result
|
||||
ret = result
|
||||
except RequestSendFailed as e:
|
||||
raise SynapseError(502, "Failed to fetch profile") from e
|
||||
except HttpResponseException as e:
|
||||
@@ -133,7 +138,17 @@ class ProfileHandler:
|
||||
raise SynapseError(502, "Failed to fetch profile")
|
||||
raise e.to_synapse_error()
|
||||
|
||||
async def get_displayname(self, target_user: UserID) -> Optional[str]:
|
||||
# Check whether the appservice has any information about the user.
|
||||
|
||||
logger.info(f"query_profile {user_id} {from_user_id}")
|
||||
as_profile = await self._as.query_profile(user_id, from_user_id)
|
||||
ret.update(as_profile)
|
||||
|
||||
return ret
|
||||
|
||||
async def get_displayname(
|
||||
self, target_user: UserID, from_user_id: Optional[UserID] = None
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Fetch a user's display name from their profile.
|
||||
|
||||
@@ -143,6 +158,14 @@ class ProfileHandler:
|
||||
Returns:
|
||||
The user's display name or None if unset.
|
||||
"""
|
||||
|
||||
# Check whether the appservice has any information about the user.
|
||||
as_profile = await self._as.query_profile(
|
||||
target_user.to_string(), from_user_id, "displayname"
|
||||
)
|
||||
if "displayname" in as_profile:
|
||||
return as_profile.displayname
|
||||
|
||||
if self.hs.is_mine(target_user):
|
||||
try:
|
||||
displayname = await self.store.get_profile_displayname(target_user)
|
||||
@@ -238,7 +261,9 @@ class ProfileHandler:
|
||||
if propagate:
|
||||
await self._update_join_states(requester, target_user)
|
||||
|
||||
async def get_avatar_url(self, target_user: UserID) -> Optional[str]:
|
||||
async def get_avatar_url(
|
||||
self, target_user: UserID, from_user_id: Optional[UserID]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Fetch a user's avatar URL from their profile.
|
||||
|
||||
@@ -248,6 +273,14 @@ class ProfileHandler:
|
||||
Returns:
|
||||
The user's avatar URL or None if unset.
|
||||
"""
|
||||
|
||||
# Check whether the appservice has any information about the user.
|
||||
as_profile = await self._as.query_profile(
|
||||
target_user.to_string(), from_user_id, "avatar_url"
|
||||
)
|
||||
if "avatar_url" in as_profile:
|
||||
return as_profile.avatar_url
|
||||
|
||||
if self.hs.is_mine(target_user):
|
||||
try:
|
||||
avatar_url = await self.store.get_profile_avatar_url(target_user)
|
||||
@@ -416,7 +449,10 @@ class ProfileHandler:
|
||||
return True
|
||||
|
||||
async def get_profile_field(
|
||||
self, target_user: UserID, field_name: str
|
||||
self,
|
||||
target_user: UserID,
|
||||
field_name: str,
|
||||
from_user_id: Optional[UserID] = None,
|
||||
) -> JsonValue:
|
||||
"""
|
||||
Fetch a user's profile from the database for local users and over federation
|
||||
@@ -429,6 +465,15 @@ class ProfileHandler:
|
||||
Returns:
|
||||
The value for the profile field or None if the field does not exist.
|
||||
"""
|
||||
|
||||
logger.info(f"get_profile_field {field_name} {from_user_id}")
|
||||
# Check whether the appservice has any information about the user.
|
||||
as_profile = await self._as.query_profile(
|
||||
target_user.to_string(), from_user_id, field_name
|
||||
)
|
||||
if field_name in as_profile:
|
||||
return as_profile[field_name]
|
||||
|
||||
if self.hs.is_mine(target_user):
|
||||
try:
|
||||
field_value = await self.store.get_profile_field(
|
||||
@@ -533,7 +578,15 @@ class ProfileHandler:
|
||||
if not self.hs.is_mine(user):
|
||||
raise SynapseError(400, "User is not hosted on this homeserver")
|
||||
|
||||
just_field = args.get("field", None)
|
||||
just_field = args.get("field")
|
||||
origin_server = args.get("origin")
|
||||
|
||||
assert origin_server
|
||||
|
||||
# Check whether the appservice has any information about the user.
|
||||
as_profile = await self._as.query_profile(user.to_string(), key=just_field, origin_server=origin_server)
|
||||
if just_field and just_field in as_profile:
|
||||
return as_profile[just_field]
|
||||
|
||||
response: JsonDict = {}
|
||||
try:
|
||||
@@ -564,6 +617,8 @@ class ProfileHandler:
|
||||
raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
|
||||
raise
|
||||
|
||||
response.update(as_profile)
|
||||
|
||||
return response
|
||||
|
||||
async def _update_join_states(
|
||||
|
||||
@@ -43,7 +43,7 @@ import attr
|
||||
from twisted.web.iweb import IRequest
|
||||
from twisted.web.server import Request
|
||||
|
||||
from synapse.api.constants import LoginType, ProfileFields
|
||||
from synapse.api.constants import LoginType
|
||||
from synapse.api.errors import Codes, NotFoundError, RedirectException, SynapseError
|
||||
from synapse.config.sso import SsoAttributeRequirement
|
||||
from synapse.handlers.register import init_counters_for_auth_provider
|
||||
@@ -813,9 +813,9 @@ class SsoHandler:
|
||||
upload_name = "sso_avatar_" + hashlib.sha256(picture.read()).hexdigest()
|
||||
|
||||
# bail if user already has the same avatar
|
||||
profile = await self._profile_handler.get_profile(user_id)
|
||||
if ProfileFields.AVATAR_URL in profile:
|
||||
avatar_url_parts = profile[ProfileFields.AVATAR_URL].split("/")
|
||||
avatar_url = await self._profile_handler.get_avatar_url(user_id)
|
||||
if avatar_url:
|
||||
avatar_url_parts = avatar_url.split("/")
|
||||
server_name = avatar_url_parts[-2]
|
||||
media_id = avatar_url_parts[-1]
|
||||
if self._is_mine_server_name(server_name):
|
||||
|
||||
@@ -1199,7 +1199,7 @@ class ModuleApi:
|
||||
try:
|
||||
# Try to fetch the user's profile.
|
||||
profile = await self._hs.get_profile_handler().get_profile(
|
||||
target_user_id.to_string(),
|
||||
target_user_id.to_string(), requester.user
|
||||
)
|
||||
except SynapseError as e:
|
||||
# If the profile couldn't be found, use default values.
|
||||
|
||||
@@ -134,6 +134,7 @@ class Mailer:
|
||||
self.macaroon_gen = self.hs.get_macaroon_generator()
|
||||
self.state_handler = self.hs.get_state_handler()
|
||||
self._storage_controllers = hs.get_storage_controllers()
|
||||
self._profile_handler = hs.get_profile_handler()
|
||||
self.app_name = app_name
|
||||
self.email_subjects: EmailSubjectConfig = hs.config.email.email_subjects
|
||||
|
||||
@@ -296,7 +297,7 @@ class Mailer:
|
||||
state_by_room = {}
|
||||
|
||||
try:
|
||||
user_display_name = await self.store.get_profile_displayname(
|
||||
user_display_name = await self._profile_handler.get_displayname(
|
||||
UserID.from_string(user_id)
|
||||
)
|
||||
if user_display_name is None:
|
||||
|
||||
@@ -84,7 +84,7 @@ class ProfileRestServlet(RestServlet):
|
||||
user = UserID.from_string(user_id)
|
||||
await self.profile_handler.check_profile_query_allowed(user, requester_user)
|
||||
|
||||
ret = await self.profile_handler.get_profile(user_id)
|
||||
ret = await self.profile_handler.get_profile(user_id, requester_user)
|
||||
|
||||
return 200, ret
|
||||
|
||||
@@ -113,11 +113,8 @@ class ProfileFieldRestServlet(RestServlet):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, user_id: str, field_name: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester_user = None
|
||||
|
||||
if self.hs.config.server.require_auth_for_profile_requests:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
requester_user = requester.user
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
requester_user = requester.user
|
||||
|
||||
if not UserID.is_valid(user_id):
|
||||
raise SynapseError(
|
||||
@@ -137,14 +134,20 @@ class ProfileFieldRestServlet(RestServlet):
|
||||
)
|
||||
|
||||
user = UserID.from_string(user_id)
|
||||
await self.profile_handler.check_profile_query_allowed(user, requester_user)
|
||||
await self.profile_handler.check_profile_query_allowed(user, requester_user if self.hs.config.server.require_auth_for_profile_requests else None)
|
||||
|
||||
if field_name == ProfileFields.DISPLAYNAME:
|
||||
field_value: JsonValue = await self.profile_handler.get_displayname(user)
|
||||
field_value: JsonValue = await self.profile_handler.get_displayname(
|
||||
user, requester_user
|
||||
)
|
||||
elif field_name == ProfileFields.AVATAR_URL:
|
||||
field_value = await self.profile_handler.get_avatar_url(user)
|
||||
field_value = await self.profile_handler.get_avatar_url(
|
||||
user, requester_user
|
||||
)
|
||||
else:
|
||||
field_value = await self.profile_handler.get_profile_field(user, field_name)
|
||||
field_value = await self.profile_handler.get_profile_field(
|
||||
user, field_name, requester_user
|
||||
)
|
||||
|
||||
return 200, {field_name: field_value}
|
||||
|
||||
@@ -272,9 +275,11 @@ class ProfileFieldRestServlet(RestServlet):
|
||||
|
||||
|
||||
class UnstableProfileFieldRestServlet(ProfileFieldRestServlet):
|
||||
re.compile(
|
||||
r"^/_matrix/client/unstable/uk\.tcpip\.msc4133/profile/(?P<user_id>[^/]*)/(?P<field_name>[^/]*)"
|
||||
)
|
||||
PATTERNS = [
|
||||
re.compile(
|
||||
r"^/_matrix/client/unstable/uk\.tcpip\.msc4133/profile/(?P<user_id>[^/]*)/(?P<field_name>[^/]*)"
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
|
||||
Reference in New Issue
Block a user