client.py 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014-2016 OpenMarket Ltd
  3. # Copyright 2018 New Vector Ltd
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import logging
  17. import urllib
  18. from typing import Any, Dict, Optional
  19. from twisted.internet import defer
  20. from synapse.api.constants import Membership
  21. from synapse.api.errors import Codes, HttpResponseException, SynapseError
  22. from synapse.api.urls import (
  23. FEDERATION_UNSTABLE_PREFIX,
  24. FEDERATION_V1_PREFIX,
  25. FEDERATION_V2_PREFIX,
  26. )
  27. from synapse.logging.utils import log_function
  28. logger = logging.getLogger(__name__)
  29. class TransportLayerClient(object):
  30. """Sends federation HTTP requests to other servers"""
  31. def __init__(self, hs):
  32. self.server_name = hs.hostname
  33. self.client = hs.get_http_client()
  34. @log_function
  35. def get_room_state_ids(self, destination, room_id, event_id):
  36. """ Requests all state for a given room from the given server at the
  37. given event. Returns the state's event_id's
  38. Args:
  39. destination (str): The host name of the remote homeserver we want
  40. to get the state from.
  41. context (str): The name of the context we want the state of
  42. event_id (str): The event we want the context at.
  43. Returns:
  44. Deferred: Results in a dict received from the remote homeserver.
  45. """
  46. logger.debug("get_room_state_ids dest=%s, room=%s", destination, room_id)
  47. path = _create_v1_path("/state_ids/%s", room_id)
  48. return self.client.get_json(
  49. destination,
  50. path=path,
  51. args={"event_id": event_id},
  52. try_trailing_slash_on_400=True,
  53. )
  54. @log_function
  55. def get_event(self, destination, event_id, timeout=None):
  56. """ Requests the pdu with give id and origin from the given server.
  57. Args:
  58. destination (str): The host name of the remote homeserver we want
  59. to get the state from.
  60. event_id (str): The id of the event being requested.
  61. timeout (int): How long to try (in ms) the destination for before
  62. giving up. None indicates no timeout.
  63. Returns:
  64. Deferred: Results in a dict received from the remote homeserver.
  65. """
  66. logger.debug("get_pdu dest=%s, event_id=%s", destination, event_id)
  67. path = _create_v1_path("/event/%s", event_id)
  68. return self.client.get_json(
  69. destination, path=path, timeout=timeout, try_trailing_slash_on_400=True
  70. )
  71. @log_function
  72. def backfill(self, destination, room_id, event_tuples, limit):
  73. """ Requests `limit` previous PDUs in a given context before list of
  74. PDUs.
  75. Args:
  76. dest (str)
  77. room_id (str)
  78. event_tuples (list)
  79. limit (int)
  80. Returns:
  81. Deferred: Results in a dict received from the remote homeserver.
  82. """
  83. logger.debug(
  84. "backfill dest=%s, room_id=%s, event_tuples=%r, limit=%s",
  85. destination,
  86. room_id,
  87. event_tuples,
  88. str(limit),
  89. )
  90. if not event_tuples:
  91. # TODO: raise?
  92. return
  93. path = _create_v1_path("/backfill/%s", room_id)
  94. args = {"v": event_tuples, "limit": [str(limit)]}
  95. return self.client.get_json(
  96. destination, path=path, args=args, try_trailing_slash_on_400=True
  97. )
  98. @defer.inlineCallbacks
  99. @log_function
  100. def send_transaction(self, transaction, json_data_callback=None):
  101. """ Sends the given Transaction to its destination
  102. Args:
  103. transaction (Transaction)
  104. Returns:
  105. Deferred: Succeeds when we get a 2xx HTTP response. The result
  106. will be the decoded JSON body.
  107. Fails with ``HTTPRequestException`` if we get an HTTP response
  108. code >= 300.
  109. Fails with ``NotRetryingDestination`` if we are not yet ready
  110. to retry this server.
  111. Fails with ``FederationDeniedError`` if this destination
  112. is not on our federation whitelist
  113. """
  114. logger.debug(
  115. "send_data dest=%s, txid=%s",
  116. transaction.destination,
  117. transaction.transaction_id,
  118. )
  119. if transaction.destination == self.server_name:
  120. raise RuntimeError("Transport layer cannot send to itself!")
  121. # FIXME: This is only used by the tests. The actual json sent is
  122. # generated by the json_data_callback.
  123. json_data = transaction.get_dict()
  124. path = _create_v1_path("/send/%s", transaction.transaction_id)
  125. response = yield self.client.put_json(
  126. transaction.destination,
  127. path=path,
  128. data=json_data,
  129. json_data_callback=json_data_callback,
  130. long_retries=True,
  131. backoff_on_404=True, # If we get a 404 the other side has gone
  132. try_trailing_slash_on_400=True,
  133. )
  134. return response
  135. @defer.inlineCallbacks
  136. @log_function
  137. def make_query(
  138. self, destination, query_type, args, retry_on_dns_fail, ignore_backoff=False
  139. ):
  140. path = _create_v1_path("/query/%s", query_type)
  141. content = yield self.client.get_json(
  142. destination=destination,
  143. path=path,
  144. args=args,
  145. retry_on_dns_fail=retry_on_dns_fail,
  146. timeout=10000,
  147. ignore_backoff=ignore_backoff,
  148. )
  149. return content
  150. @defer.inlineCallbacks
  151. @log_function
  152. def make_membership_event(self, destination, room_id, user_id, membership, params):
  153. """Asks a remote server to build and sign us a membership event
  154. Note that this does not append any events to any graphs.
  155. Args:
  156. destination (str): address of remote homeserver
  157. room_id (str): room to join/leave
  158. user_id (str): user to be joined/left
  159. membership (str): one of join/leave
  160. params (dict[str, str|Iterable[str]]): Query parameters to include in the
  161. request.
  162. Returns:
  163. Deferred: Succeeds when we get a 2xx HTTP response. The result
  164. will be the decoded JSON body (ie, the new event).
  165. Fails with ``HTTPRequestException`` if we get an HTTP response
  166. code >= 300.
  167. Fails with ``NotRetryingDestination`` if we are not yet ready
  168. to retry this server.
  169. Fails with ``FederationDeniedError`` if the remote destination
  170. is not in our federation whitelist
  171. """
  172. valid_memberships = {Membership.JOIN, Membership.LEAVE}
  173. if membership not in valid_memberships:
  174. raise RuntimeError(
  175. "make_membership_event called with membership='%s', must be one of %s"
  176. % (membership, ",".join(valid_memberships))
  177. )
  178. path = _create_v1_path("/make_%s/%s/%s", membership, room_id, user_id)
  179. ignore_backoff = False
  180. retry_on_dns_fail = False
  181. if membership == Membership.LEAVE:
  182. # we particularly want to do our best to send leave events. The
  183. # problem is that if it fails, we won't retry it later, so if the
  184. # remote server was just having a momentary blip, the room will be
  185. # out of sync.
  186. ignore_backoff = True
  187. retry_on_dns_fail = True
  188. content = yield self.client.get_json(
  189. destination=destination,
  190. path=path,
  191. args=params,
  192. retry_on_dns_fail=retry_on_dns_fail,
  193. timeout=20000,
  194. ignore_backoff=ignore_backoff,
  195. )
  196. return content
  197. @defer.inlineCallbacks
  198. @log_function
  199. def send_join_v1(self, destination, room_id, event_id, content):
  200. path = _create_v1_path("/send_join/%s/%s", room_id, event_id)
  201. response = yield self.client.put_json(
  202. destination=destination, path=path, data=content
  203. )
  204. return response
  205. @defer.inlineCallbacks
  206. @log_function
  207. def send_join_v2(self, destination, room_id, event_id, content):
  208. path = _create_v2_path("/send_join/%s/%s", room_id, event_id)
  209. response = yield self.client.put_json(
  210. destination=destination, path=path, data=content
  211. )
  212. return response
  213. @defer.inlineCallbacks
  214. @log_function
  215. def send_leave_v1(self, destination, room_id, event_id, content):
  216. path = _create_v1_path("/send_leave/%s/%s", room_id, event_id)
  217. response = yield self.client.put_json(
  218. destination=destination,
  219. path=path,
  220. data=content,
  221. # we want to do our best to send this through. The problem is
  222. # that if it fails, we won't retry it later, so if the remote
  223. # server was just having a momentary blip, the room will be out of
  224. # sync.
  225. ignore_backoff=True,
  226. )
  227. return response
  228. @defer.inlineCallbacks
  229. @log_function
  230. def send_leave_v2(self, destination, room_id, event_id, content):
  231. path = _create_v2_path("/send_leave/%s/%s", room_id, event_id)
  232. response = yield self.client.put_json(
  233. destination=destination,
  234. path=path,
  235. data=content,
  236. # we want to do our best to send this through. The problem is
  237. # that if it fails, we won't retry it later, so if the remote
  238. # server was just having a momentary blip, the room will be out of
  239. # sync.
  240. ignore_backoff=True,
  241. )
  242. return response
  243. @defer.inlineCallbacks
  244. @log_function
  245. def send_invite_v1(self, destination, room_id, event_id, content):
  246. path = _create_v1_path("/invite/%s/%s", room_id, event_id)
  247. response = yield self.client.put_json(
  248. destination=destination, path=path, data=content, ignore_backoff=True
  249. )
  250. return response
  251. @defer.inlineCallbacks
  252. @log_function
  253. def send_invite_v2(self, destination, room_id, event_id, content):
  254. path = _create_v2_path("/invite/%s/%s", room_id, event_id)
  255. response = yield self.client.put_json(
  256. destination=destination, path=path, data=content, ignore_backoff=True
  257. )
  258. return response
  259. @defer.inlineCallbacks
  260. @log_function
  261. def get_public_rooms(
  262. self,
  263. remote_server: str,
  264. limit: Optional[int] = None,
  265. since_token: Optional[str] = None,
  266. search_filter: Optional[Dict] = None,
  267. include_all_networks: bool = False,
  268. third_party_instance_id: Optional[str] = None,
  269. ):
  270. """Get the list of public rooms from a remote homeserver
  271. See synapse.federation.federation_client.FederationClient.get_public_rooms for
  272. more information.
  273. """
  274. if search_filter:
  275. # this uses MSC2197 (Search Filtering over Federation)
  276. path = _create_v1_path("/publicRooms")
  277. data = {
  278. "include_all_networks": "true" if include_all_networks else "false"
  279. } # type: Dict[str, Any]
  280. if third_party_instance_id:
  281. data["third_party_instance_id"] = third_party_instance_id
  282. if limit:
  283. data["limit"] = str(limit)
  284. if since_token:
  285. data["since"] = since_token
  286. data["filter"] = search_filter
  287. try:
  288. response = yield self.client.post_json(
  289. destination=remote_server, path=path, data=data, ignore_backoff=True
  290. )
  291. except HttpResponseException as e:
  292. if e.code == 403:
  293. raise SynapseError(
  294. 403,
  295. "You are not allowed to view the public rooms list of %s"
  296. % (remote_server,),
  297. errcode=Codes.FORBIDDEN,
  298. )
  299. raise
  300. else:
  301. path = _create_v1_path("/publicRooms")
  302. args = {
  303. "include_all_networks": "true" if include_all_networks else "false"
  304. } # type: Dict[str, Any]
  305. if third_party_instance_id:
  306. args["third_party_instance_id"] = (third_party_instance_id,)
  307. if limit:
  308. args["limit"] = [str(limit)]
  309. if since_token:
  310. args["since"] = [since_token]
  311. try:
  312. response = yield self.client.get_json(
  313. destination=remote_server, path=path, args=args, ignore_backoff=True
  314. )
  315. except HttpResponseException as e:
  316. if e.code == 403:
  317. raise SynapseError(
  318. 403,
  319. "You are not allowed to view the public rooms list of %s"
  320. % (remote_server,),
  321. errcode=Codes.FORBIDDEN,
  322. )
  323. raise
  324. return response
  325. @defer.inlineCallbacks
  326. @log_function
  327. def exchange_third_party_invite(self, destination, room_id, event_dict):
  328. path = _create_v1_path("/exchange_third_party_invite/%s", room_id)
  329. response = yield self.client.put_json(
  330. destination=destination, path=path, data=event_dict
  331. )
  332. return response
  333. @defer.inlineCallbacks
  334. @log_function
  335. def get_event_auth(self, destination, room_id, event_id):
  336. path = _create_v1_path("/event_auth/%s/%s", room_id, event_id)
  337. content = yield self.client.get_json(destination=destination, path=path)
  338. return content
  339. @defer.inlineCallbacks
  340. @log_function
  341. def query_client_keys(self, destination, query_content, timeout):
  342. """Query the device keys for a list of user ids hosted on a remote
  343. server.
  344. Request:
  345. {
  346. "device_keys": {
  347. "<user_id>": ["<device_id>"]
  348. }
  349. }
  350. Response:
  351. {
  352. "device_keys": {
  353. "<user_id>": {
  354. "<device_id>": {...}
  355. }
  356. },
  357. "master_key": {
  358. "<user_id>": {...}
  359. }
  360. },
  361. "self_signing_key": {
  362. "<user_id>": {...}
  363. }
  364. }
  365. Args:
  366. destination(str): The server to query.
  367. query_content(dict): The user ids to query.
  368. Returns:
  369. A dict containing device and cross-signing keys.
  370. """
  371. path = _create_v1_path("/user/keys/query")
  372. content = yield self.client.post_json(
  373. destination=destination, path=path, data=query_content, timeout=timeout
  374. )
  375. return content
  376. @defer.inlineCallbacks
  377. @log_function
  378. def query_user_devices(self, destination, user_id, timeout):
  379. """Query the devices for a user id hosted on a remote server.
  380. Response:
  381. {
  382. "stream_id": "...",
  383. "devices": [ { ... } ],
  384. "master_key": {
  385. "user_id": "<user_id>",
  386. "usage": [...],
  387. "keys": {...},
  388. "signatures": {
  389. "<user_id>": {...}
  390. }
  391. },
  392. "self_signing_key": {
  393. "user_id": "<user_id>",
  394. "usage": [...],
  395. "keys": {...},
  396. "signatures": {
  397. "<user_id>": {...}
  398. }
  399. }
  400. }
  401. Args:
  402. destination(str): The server to query.
  403. query_content(dict): The user ids to query.
  404. Returns:
  405. A dict containing device and cross-signing keys.
  406. """
  407. path = _create_v1_path("/user/devices/%s", user_id)
  408. content = yield self.client.get_json(
  409. destination=destination, path=path, timeout=timeout
  410. )
  411. return content
  412. @defer.inlineCallbacks
  413. @log_function
  414. def claim_client_keys(self, destination, query_content, timeout):
  415. """Claim one-time keys for a list of devices hosted on a remote server.
  416. Request:
  417. {
  418. "one_time_keys": {
  419. "<user_id>": {
  420. "<device_id>": "<algorithm>"
  421. }
  422. }
  423. }
  424. Response:
  425. {
  426. "device_keys": {
  427. "<user_id>": {
  428. "<device_id>": {
  429. "<algorithm>:<key_id>": "<key_base64>"
  430. }
  431. }
  432. }
  433. }
  434. Args:
  435. destination(str): The server to query.
  436. query_content(dict): The user ids to query.
  437. Returns:
  438. A dict containing the one-time keys.
  439. """
  440. path = _create_v1_path("/user/keys/claim")
  441. content = yield self.client.post_json(
  442. destination=destination, path=path, data=query_content, timeout=timeout
  443. )
  444. return content
  445. @defer.inlineCallbacks
  446. @log_function
  447. def get_missing_events(
  448. self,
  449. destination,
  450. room_id,
  451. earliest_events,
  452. latest_events,
  453. limit,
  454. min_depth,
  455. timeout,
  456. ):
  457. path = _create_v1_path("/get_missing_events/%s", room_id)
  458. content = yield self.client.post_json(
  459. destination=destination,
  460. path=path,
  461. data={
  462. "limit": int(limit),
  463. "min_depth": int(min_depth),
  464. "earliest_events": earliest_events,
  465. "latest_events": latest_events,
  466. },
  467. timeout=timeout,
  468. )
  469. return content
  470. @log_function
  471. def get_group_profile(self, destination, group_id, requester_user_id):
  472. """Get a group profile
  473. """
  474. path = _create_v1_path("/groups/%s/profile", group_id)
  475. return self.client.get_json(
  476. destination=destination,
  477. path=path,
  478. args={"requester_user_id": requester_user_id},
  479. ignore_backoff=True,
  480. )
  481. @log_function
  482. def update_group_profile(self, destination, group_id, requester_user_id, content):
  483. """Update a remote group profile
  484. Args:
  485. destination (str)
  486. group_id (str)
  487. requester_user_id (str)
  488. content (dict): The new profile of the group
  489. """
  490. path = _create_v1_path("/groups/%s/profile", group_id)
  491. return self.client.post_json(
  492. destination=destination,
  493. path=path,
  494. args={"requester_user_id": requester_user_id},
  495. data=content,
  496. ignore_backoff=True,
  497. )
  498. @log_function
  499. def get_group_summary(self, destination, group_id, requester_user_id):
  500. """Get a group summary
  501. """
  502. path = _create_v1_path("/groups/%s/summary", group_id)
  503. return self.client.get_json(
  504. destination=destination,
  505. path=path,
  506. args={"requester_user_id": requester_user_id},
  507. ignore_backoff=True,
  508. )
  509. @log_function
  510. def get_rooms_in_group(self, destination, group_id, requester_user_id):
  511. """Get all rooms in a group
  512. """
  513. path = _create_v1_path("/groups/%s/rooms", group_id)
  514. return self.client.get_json(
  515. destination=destination,
  516. path=path,
  517. args={"requester_user_id": requester_user_id},
  518. ignore_backoff=True,
  519. )
  520. def add_room_to_group(
  521. self, destination, group_id, requester_user_id, room_id, content
  522. ):
  523. """Add a room to a group
  524. """
  525. path = _create_v1_path("/groups/%s/room/%s", group_id, room_id)
  526. return self.client.post_json(
  527. destination=destination,
  528. path=path,
  529. args={"requester_user_id": requester_user_id},
  530. data=content,
  531. ignore_backoff=True,
  532. )
  533. def update_room_in_group(
  534. self, destination, group_id, requester_user_id, room_id, config_key, content
  535. ):
  536. """Update room in group
  537. """
  538. path = _create_v1_path(
  539. "/groups/%s/room/%s/config/%s", group_id, room_id, config_key
  540. )
  541. return self.client.post_json(
  542. destination=destination,
  543. path=path,
  544. args={"requester_user_id": requester_user_id},
  545. data=content,
  546. ignore_backoff=True,
  547. )
  548. def remove_room_from_group(self, destination, group_id, requester_user_id, room_id):
  549. """Remove a room from a group
  550. """
  551. path = _create_v1_path("/groups/%s/room/%s", group_id, room_id)
  552. return self.client.delete_json(
  553. destination=destination,
  554. path=path,
  555. args={"requester_user_id": requester_user_id},
  556. ignore_backoff=True,
  557. )
  558. @log_function
  559. def get_users_in_group(self, destination, group_id, requester_user_id):
  560. """Get users in a group
  561. """
  562. path = _create_v1_path("/groups/%s/users", group_id)
  563. return self.client.get_json(
  564. destination=destination,
  565. path=path,
  566. args={"requester_user_id": requester_user_id},
  567. ignore_backoff=True,
  568. )
  569. @log_function
  570. def get_invited_users_in_group(self, destination, group_id, requester_user_id):
  571. """Get users that have been invited to a group
  572. """
  573. path = _create_v1_path("/groups/%s/invited_users", group_id)
  574. return self.client.get_json(
  575. destination=destination,
  576. path=path,
  577. args={"requester_user_id": requester_user_id},
  578. ignore_backoff=True,
  579. )
  580. @log_function
  581. def accept_group_invite(self, destination, group_id, user_id, content):
  582. """Accept a group invite
  583. """
  584. path = _create_v1_path("/groups/%s/users/%s/accept_invite", group_id, user_id)
  585. return self.client.post_json(
  586. destination=destination, path=path, data=content, ignore_backoff=True
  587. )
  588. @log_function
  589. def join_group(self, destination, group_id, user_id, content):
  590. """Attempts to join a group
  591. """
  592. path = _create_v1_path("/groups/%s/users/%s/join", group_id, user_id)
  593. return self.client.post_json(
  594. destination=destination, path=path, data=content, ignore_backoff=True
  595. )
  596. @log_function
  597. def invite_to_group(
  598. self, destination, group_id, user_id, requester_user_id, content
  599. ):
  600. """Invite a user to a group
  601. """
  602. path = _create_v1_path("/groups/%s/users/%s/invite", group_id, user_id)
  603. return self.client.post_json(
  604. destination=destination,
  605. path=path,
  606. args={"requester_user_id": requester_user_id},
  607. data=content,
  608. ignore_backoff=True,
  609. )
  610. @log_function
  611. def invite_to_group_notification(self, destination, group_id, user_id, content):
  612. """Sent by group server to inform a user's server that they have been
  613. invited.
  614. """
  615. path = _create_v1_path("/groups/local/%s/users/%s/invite", group_id, user_id)
  616. return self.client.post_json(
  617. destination=destination, path=path, data=content, ignore_backoff=True
  618. )
  619. @log_function
  620. def remove_user_from_group(
  621. self, destination, group_id, requester_user_id, user_id, content
  622. ):
  623. """Remove a user fron a group
  624. """
  625. path = _create_v1_path("/groups/%s/users/%s/remove", group_id, user_id)
  626. return self.client.post_json(
  627. destination=destination,
  628. path=path,
  629. args={"requester_user_id": requester_user_id},
  630. data=content,
  631. ignore_backoff=True,
  632. )
  633. @log_function
  634. def remove_user_from_group_notification(
  635. self, destination, group_id, user_id, content
  636. ):
  637. """Sent by group server to inform a user's server that they have been
  638. kicked from the group.
  639. """
  640. path = _create_v1_path("/groups/local/%s/users/%s/remove", group_id, user_id)
  641. return self.client.post_json(
  642. destination=destination, path=path, data=content, ignore_backoff=True
  643. )
  644. @log_function
  645. def renew_group_attestation(self, destination, group_id, user_id, content):
  646. """Sent by either a group server or a user's server to periodically update
  647. the attestations
  648. """
  649. path = _create_v1_path("/groups/%s/renew_attestation/%s", group_id, user_id)
  650. return self.client.post_json(
  651. destination=destination, path=path, data=content, ignore_backoff=True
  652. )
  653. @log_function
  654. def update_group_summary_room(
  655. self, destination, group_id, user_id, room_id, category_id, content
  656. ):
  657. """Update a room entry in a group summary
  658. """
  659. if category_id:
  660. path = _create_v1_path(
  661. "/groups/%s/summary/categories/%s/rooms/%s",
  662. group_id,
  663. category_id,
  664. room_id,
  665. )
  666. else:
  667. path = _create_v1_path("/groups/%s/summary/rooms/%s", group_id, room_id)
  668. return self.client.post_json(
  669. destination=destination,
  670. path=path,
  671. args={"requester_user_id": user_id},
  672. data=content,
  673. ignore_backoff=True,
  674. )
  675. @log_function
  676. def delete_group_summary_room(
  677. self, destination, group_id, user_id, room_id, category_id
  678. ):
  679. """Delete a room entry in a group summary
  680. """
  681. if category_id:
  682. path = _create_v1_path(
  683. "/groups/%s/summary/categories/%s/rooms/%s",
  684. group_id,
  685. category_id,
  686. room_id,
  687. )
  688. else:
  689. path = _create_v1_path("/groups/%s/summary/rooms/%s", group_id, room_id)
  690. return self.client.delete_json(
  691. destination=destination,
  692. path=path,
  693. args={"requester_user_id": user_id},
  694. ignore_backoff=True,
  695. )
  696. @log_function
  697. def get_group_categories(self, destination, group_id, requester_user_id):
  698. """Get all categories in a group
  699. """
  700. path = _create_v1_path("/groups/%s/categories", group_id)
  701. return self.client.get_json(
  702. destination=destination,
  703. path=path,
  704. args={"requester_user_id": requester_user_id},
  705. ignore_backoff=True,
  706. )
  707. @log_function
  708. def get_group_category(self, destination, group_id, requester_user_id, category_id):
  709. """Get category info in a group
  710. """
  711. path = _create_v1_path("/groups/%s/categories/%s", group_id, category_id)
  712. return self.client.get_json(
  713. destination=destination,
  714. path=path,
  715. args={"requester_user_id": requester_user_id},
  716. ignore_backoff=True,
  717. )
  718. @log_function
  719. def update_group_category(
  720. self, destination, group_id, requester_user_id, category_id, content
  721. ):
  722. """Update a category in a group
  723. """
  724. path = _create_v1_path("/groups/%s/categories/%s", group_id, category_id)
  725. return self.client.post_json(
  726. destination=destination,
  727. path=path,
  728. args={"requester_user_id": requester_user_id},
  729. data=content,
  730. ignore_backoff=True,
  731. )
  732. @log_function
  733. def delete_group_category(
  734. self, destination, group_id, requester_user_id, category_id
  735. ):
  736. """Delete a category in a group
  737. """
  738. path = _create_v1_path("/groups/%s/categories/%s", group_id, category_id)
  739. return self.client.delete_json(
  740. destination=destination,
  741. path=path,
  742. args={"requester_user_id": requester_user_id},
  743. ignore_backoff=True,
  744. )
  745. @log_function
  746. def get_group_roles(self, destination, group_id, requester_user_id):
  747. """Get all roles in a group
  748. """
  749. path = _create_v1_path("/groups/%s/roles", group_id)
  750. return self.client.get_json(
  751. destination=destination,
  752. path=path,
  753. args={"requester_user_id": requester_user_id},
  754. ignore_backoff=True,
  755. )
  756. @log_function
  757. def get_group_role(self, destination, group_id, requester_user_id, role_id):
  758. """Get a roles info
  759. """
  760. path = _create_v1_path("/groups/%s/roles/%s", group_id, role_id)
  761. return self.client.get_json(
  762. destination=destination,
  763. path=path,
  764. args={"requester_user_id": requester_user_id},
  765. ignore_backoff=True,
  766. )
  767. @log_function
  768. def update_group_role(
  769. self, destination, group_id, requester_user_id, role_id, content
  770. ):
  771. """Update a role in a group
  772. """
  773. path = _create_v1_path("/groups/%s/roles/%s", group_id, role_id)
  774. return self.client.post_json(
  775. destination=destination,
  776. path=path,
  777. args={"requester_user_id": requester_user_id},
  778. data=content,
  779. ignore_backoff=True,
  780. )
  781. @log_function
  782. def delete_group_role(self, destination, group_id, requester_user_id, role_id):
  783. """Delete a role in a group
  784. """
  785. path = _create_v1_path("/groups/%s/roles/%s", group_id, role_id)
  786. return self.client.delete_json(
  787. destination=destination,
  788. path=path,
  789. args={"requester_user_id": requester_user_id},
  790. ignore_backoff=True,
  791. )
  792. @log_function
  793. def update_group_summary_user(
  794. self, destination, group_id, requester_user_id, user_id, role_id, content
  795. ):
  796. """Update a users entry in a group
  797. """
  798. if role_id:
  799. path = _create_v1_path(
  800. "/groups/%s/summary/roles/%s/users/%s", group_id, role_id, user_id
  801. )
  802. else:
  803. path = _create_v1_path("/groups/%s/summary/users/%s", group_id, user_id)
  804. return self.client.post_json(
  805. destination=destination,
  806. path=path,
  807. args={"requester_user_id": requester_user_id},
  808. data=content,
  809. ignore_backoff=True,
  810. )
  811. @log_function
  812. def set_group_join_policy(self, destination, group_id, requester_user_id, content):
  813. """Sets the join policy for a group
  814. """
  815. path = _create_v1_path("/groups/%s/settings/m.join_policy", group_id)
  816. return self.client.put_json(
  817. destination=destination,
  818. path=path,
  819. args={"requester_user_id": requester_user_id},
  820. data=content,
  821. ignore_backoff=True,
  822. )
  823. @log_function
  824. def delete_group_summary_user(
  825. self, destination, group_id, requester_user_id, user_id, role_id
  826. ):
  827. """Delete a users entry in a group
  828. """
  829. if role_id:
  830. path = _create_v1_path(
  831. "/groups/%s/summary/roles/%s/users/%s", group_id, role_id, user_id
  832. )
  833. else:
  834. path = _create_v1_path("/groups/%s/summary/users/%s", group_id, user_id)
  835. return self.client.delete_json(
  836. destination=destination,
  837. path=path,
  838. args={"requester_user_id": requester_user_id},
  839. ignore_backoff=True,
  840. )
  841. def bulk_get_publicised_groups(self, destination, user_ids):
  842. """Get the groups a list of users are publicising
  843. """
  844. path = _create_v1_path("/get_groups_publicised")
  845. content = {"user_ids": user_ids}
  846. return self.client.post_json(
  847. destination=destination, path=path, data=content, ignore_backoff=True
  848. )
  849. def get_room_complexity(self, destination, room_id):
  850. """
  851. Args:
  852. destination (str): The remote server
  853. room_id (str): The room ID to ask about.
  854. """
  855. path = _create_path(FEDERATION_UNSTABLE_PREFIX, "/rooms/%s/complexity", room_id)
  856. return self.client.get_json(destination=destination, path=path)
  857. def _create_path(federation_prefix, path, *args):
  858. """
  859. Ensures that all args are url encoded.
  860. """
  861. return federation_prefix + path % tuple(urllib.parse.quote(arg, "") for arg in args)
  862. def _create_v1_path(path, *args):
  863. """Creates a path against V1 federation API from the path template and
  864. args. Ensures that all args are url encoded.
  865. Example:
  866. _create_v1_path("/event/%s", event_id)
  867. Args:
  868. path (str): String template for the path
  869. args: ([str]): Args to insert into path. Each arg will be url encoded
  870. Returns:
  871. str
  872. """
  873. return _create_path(FEDERATION_V1_PREFIX, path, *args)
  874. def _create_v2_path(path, *args):
  875. """Creates a path against V2 federation API from the path template and
  876. args. Ensures that all args are url encoded.
  877. Example:
  878. _create_v2_path("/event/%s", event_id)
  879. Args:
  880. path (str): String template for the path
  881. args: ([str]): Args to insert into path. Each arg will be url encoded
  882. Returns:
  883. str
  884. """
  885. return _create_path(FEDERATION_V2_PREFIX, path, *args)