profile.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014-2016 OpenMarket Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. from six import raise_from
  17. from twisted.internet import defer
  18. from synapse.api.errors import (
  19. AuthError,
  20. Codes,
  21. HttpResponseException,
  22. RequestSendFailed,
  23. StoreError,
  24. SynapseError,
  25. )
  26. from synapse.metrics.background_process_metrics import run_as_background_process
  27. from synapse.types import UserID, get_domain_from_id
  28. from ._base import BaseHandler
  29. logger = logging.getLogger(__name__)
  30. MAX_DISPLAYNAME_LEN = 100
  31. MAX_AVATAR_URL_LEN = 1000
  32. class BaseProfileHandler(BaseHandler):
  33. """Handles fetching and updating user profile information.
  34. BaseProfileHandler can be instantiated directly on workers and will
  35. delegate to master when necessary. The master process should use the
  36. subclass MasterProfileHandler
  37. """
  38. def __init__(self, hs):
  39. super(BaseProfileHandler, self).__init__(hs)
  40. self.federation = hs.get_federation_client()
  41. hs.get_federation_registry().register_query_handler(
  42. "profile", self.on_profile_query
  43. )
  44. self.user_directory_handler = hs.get_user_directory_handler()
  45. @defer.inlineCallbacks
  46. def get_profile(self, user_id):
  47. target_user = UserID.from_string(user_id)
  48. if self.hs.is_mine(target_user):
  49. try:
  50. displayname = yield self.store.get_profile_displayname(
  51. target_user.localpart
  52. )
  53. avatar_url = yield self.store.get_profile_avatar_url(
  54. target_user.localpart
  55. )
  56. except StoreError as e:
  57. if e.code == 404:
  58. raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
  59. raise
  60. return {"displayname": displayname, "avatar_url": avatar_url}
  61. else:
  62. try:
  63. result = yield self.federation.make_query(
  64. destination=target_user.domain,
  65. query_type="profile",
  66. args={"user_id": user_id},
  67. ignore_backoff=True,
  68. )
  69. return result
  70. except RequestSendFailed as e:
  71. raise_from(SynapseError(502, "Failed to fetch profile"), e)
  72. except HttpResponseException as e:
  73. raise e.to_synapse_error()
  74. @defer.inlineCallbacks
  75. def get_profile_from_cache(self, user_id):
  76. """Get the profile information from our local cache. If the user is
  77. ours then the profile information will always be corect. Otherwise,
  78. it may be out of date/missing.
  79. """
  80. target_user = UserID.from_string(user_id)
  81. if self.hs.is_mine(target_user):
  82. try:
  83. displayname = yield self.store.get_profile_displayname(
  84. target_user.localpart
  85. )
  86. avatar_url = yield self.store.get_profile_avatar_url(
  87. target_user.localpart
  88. )
  89. except StoreError as e:
  90. if e.code == 404:
  91. raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
  92. raise
  93. return {"displayname": displayname, "avatar_url": avatar_url}
  94. else:
  95. profile = yield self.store.get_from_remote_profile_cache(user_id)
  96. return profile or {}
  97. @defer.inlineCallbacks
  98. def get_displayname(self, target_user):
  99. if self.hs.is_mine(target_user):
  100. try:
  101. displayname = yield self.store.get_profile_displayname(
  102. target_user.localpart
  103. )
  104. except StoreError as e:
  105. if e.code == 404:
  106. raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
  107. raise
  108. return displayname
  109. else:
  110. try:
  111. result = yield self.federation.make_query(
  112. destination=target_user.domain,
  113. query_type="profile",
  114. args={"user_id": target_user.to_string(), "field": "displayname"},
  115. ignore_backoff=True,
  116. )
  117. except RequestSendFailed as e:
  118. raise_from(SynapseError(502, "Failed to fetch profile"), e)
  119. except HttpResponseException as e:
  120. raise e.to_synapse_error()
  121. return result["displayname"]
  122. @defer.inlineCallbacks
  123. def set_displayname(self, target_user, requester, new_displayname, by_admin=False):
  124. """Set the displayname of a user
  125. Args:
  126. target_user (UserID): the user whose displayname is to be changed.
  127. requester (Requester): The user attempting to make this change.
  128. new_displayname (str): The displayname to give this user.
  129. by_admin (bool): Whether this change was made by an administrator.
  130. """
  131. if not self.hs.is_mine(target_user):
  132. raise SynapseError(400, "User is not hosted on this Home Server")
  133. if not by_admin and target_user != requester.user:
  134. raise AuthError(400, "Cannot set another user's displayname")
  135. if len(new_displayname) > MAX_DISPLAYNAME_LEN:
  136. raise SynapseError(
  137. 400, "Displayname is too long (max %i)" % (MAX_DISPLAYNAME_LEN,)
  138. )
  139. if new_displayname == "":
  140. new_displayname = None
  141. yield self.store.set_profile_displayname(target_user.localpart, new_displayname)
  142. if self.hs.config.user_directory_search_all_users:
  143. profile = yield self.store.get_profileinfo(target_user.localpart)
  144. yield self.user_directory_handler.handle_local_profile_change(
  145. target_user.to_string(), profile
  146. )
  147. yield self._update_join_states(requester, target_user)
  148. @defer.inlineCallbacks
  149. def get_avatar_url(self, target_user):
  150. if self.hs.is_mine(target_user):
  151. try:
  152. avatar_url = yield self.store.get_profile_avatar_url(
  153. target_user.localpart
  154. )
  155. except StoreError as e:
  156. if e.code == 404:
  157. raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
  158. raise
  159. return avatar_url
  160. else:
  161. try:
  162. result = yield self.federation.make_query(
  163. destination=target_user.domain,
  164. query_type="profile",
  165. args={"user_id": target_user.to_string(), "field": "avatar_url"},
  166. ignore_backoff=True,
  167. )
  168. except RequestSendFailed as e:
  169. raise_from(SynapseError(502, "Failed to fetch profile"), e)
  170. except HttpResponseException as e:
  171. raise e.to_synapse_error()
  172. return result["avatar_url"]
  173. @defer.inlineCallbacks
  174. def set_avatar_url(self, target_user, requester, new_avatar_url, by_admin=False):
  175. """target_user is the user whose avatar_url is to be changed;
  176. auth_user is the user attempting to make this change."""
  177. if not self.hs.is_mine(target_user):
  178. raise SynapseError(400, "User is not hosted on this Home Server")
  179. if not by_admin and target_user != requester.user:
  180. raise AuthError(400, "Cannot set another user's avatar_url")
  181. if len(new_avatar_url) > MAX_AVATAR_URL_LEN:
  182. raise SynapseError(
  183. 400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,)
  184. )
  185. yield self.store.set_profile_avatar_url(target_user.localpart, new_avatar_url)
  186. if self.hs.config.user_directory_search_all_users:
  187. profile = yield self.store.get_profileinfo(target_user.localpart)
  188. yield self.user_directory_handler.handle_local_profile_change(
  189. target_user.to_string(), profile
  190. )
  191. yield self._update_join_states(requester, target_user)
  192. @defer.inlineCallbacks
  193. def on_profile_query(self, args):
  194. user = UserID.from_string(args["user_id"])
  195. if not self.hs.is_mine(user):
  196. raise SynapseError(400, "User is not hosted on this Home Server")
  197. just_field = args.get("field", None)
  198. response = {}
  199. try:
  200. if just_field is None or just_field == "displayname":
  201. response["displayname"] = yield self.store.get_profile_displayname(
  202. user.localpart
  203. )
  204. if just_field is None or just_field == "avatar_url":
  205. response["avatar_url"] = yield self.store.get_profile_avatar_url(
  206. user.localpart
  207. )
  208. except StoreError as e:
  209. if e.code == 404:
  210. raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
  211. raise
  212. return response
  213. @defer.inlineCallbacks
  214. def _update_join_states(self, requester, target_user):
  215. if not self.hs.is_mine(target_user):
  216. return
  217. yield self.ratelimit(requester)
  218. room_ids = yield self.store.get_rooms_for_user(target_user.to_string())
  219. for room_id in room_ids:
  220. handler = self.hs.get_room_member_handler()
  221. try:
  222. # Assume the target_user isn't a guest,
  223. # because we don't let guests set profile or avatar data.
  224. yield handler.update_membership(
  225. requester,
  226. target_user,
  227. room_id,
  228. "join", # We treat a profile update like a join.
  229. ratelimit=False, # Try to hide that these events aren't atomic.
  230. )
  231. except Exception as e:
  232. logger.warn(
  233. "Failed to update join event for room %s - %s", room_id, str(e)
  234. )
  235. @defer.inlineCallbacks
  236. def check_profile_query_allowed(self, target_user, requester=None):
  237. """Checks whether a profile query is allowed. If the
  238. 'require_auth_for_profile_requests' config flag is set to True and a
  239. 'requester' is provided, the query is only allowed if the two users
  240. share a room.
  241. Args:
  242. target_user (UserID): The owner of the queried profile.
  243. requester (None|UserID): The user querying for the profile.
  244. Raises:
  245. SynapseError(403): The two users share no room, or ne user couldn't
  246. be found to be in any room the server is in, and therefore the query
  247. is denied.
  248. """
  249. # Implementation of MSC1301: don't allow looking up profiles if the
  250. # requester isn't in the same room as the target. We expect requester to
  251. # be None when this function is called outside of a profile query, e.g.
  252. # when building a membership event. In this case, we must allow the
  253. # lookup.
  254. if not self.hs.config.require_auth_for_profile_requests or not requester:
  255. return
  256. # Always allow the user to query their own profile.
  257. if target_user.to_string() == requester.to_string():
  258. return
  259. try:
  260. requester_rooms = yield self.store.get_rooms_for_user(requester.to_string())
  261. target_user_rooms = yield self.store.get_rooms_for_user(
  262. target_user.to_string()
  263. )
  264. # Check if the room lists have no elements in common.
  265. if requester_rooms.isdisjoint(target_user_rooms):
  266. raise SynapseError(403, "Profile isn't available", Codes.FORBIDDEN)
  267. except StoreError as e:
  268. if e.code == 404:
  269. # This likely means that one of the users doesn't exist,
  270. # so we act as if we couldn't find the profile.
  271. raise SynapseError(403, "Profile isn't available", Codes.FORBIDDEN)
  272. raise
  273. class MasterProfileHandler(BaseProfileHandler):
  274. PROFILE_UPDATE_MS = 60 * 1000
  275. PROFILE_UPDATE_EVERY_MS = 24 * 60 * 60 * 1000
  276. def __init__(self, hs):
  277. super(MasterProfileHandler, self).__init__(hs)
  278. assert hs.config.worker_app is None
  279. self.clock.looping_call(
  280. self._start_update_remote_profile_cache, self.PROFILE_UPDATE_MS
  281. )
  282. def _start_update_remote_profile_cache(self):
  283. return run_as_background_process(
  284. "Update remote profile", self._update_remote_profile_cache
  285. )
  286. @defer.inlineCallbacks
  287. def _update_remote_profile_cache(self):
  288. """Called periodically to check profiles of remote users we haven't
  289. checked in a while.
  290. """
  291. entries = yield self.store.get_remote_profile_cache_entries_that_expire(
  292. last_checked=self.clock.time_msec() - self.PROFILE_UPDATE_EVERY_MS
  293. )
  294. for user_id, displayname, avatar_url in entries:
  295. is_subscribed = yield self.store.is_subscribed_remote_profile_for_user(
  296. user_id
  297. )
  298. if not is_subscribed:
  299. yield self.store.maybe_delete_remote_profile_cache(user_id)
  300. continue
  301. try:
  302. profile = yield self.federation.make_query(
  303. destination=get_domain_from_id(user_id),
  304. query_type="profile",
  305. args={"user_id": user_id},
  306. ignore_backoff=True,
  307. )
  308. except Exception:
  309. logger.exception("Failed to get avatar_url")
  310. yield self.store.update_remote_profile_cache(
  311. user_id, displayname, avatar_url
  312. )
  313. continue
  314. new_name = profile.get("displayname")
  315. new_avatar = profile.get("avatar_url")
  316. # We always hit update to update the last_check timestamp
  317. yield self.store.update_remote_profile_cache(user_id, new_name, new_avatar)