You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

258 lines
8.3 KiB

  1. # Copyright 2015, 2016 OpenMarket Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import logging
  15. import urllib
  16. from typing import TYPE_CHECKING, List, Optional, Tuple
  17. from prometheus_client import Counter
  18. from synapse.api.constants import EventTypes, ThirdPartyEntityKind
  19. from synapse.api.errors import CodeMessageException
  20. from synapse.events import EventBase
  21. from synapse.events.utils import serialize_event
  22. from synapse.http.client import SimpleHttpClient
  23. from synapse.types import JsonDict, ThirdPartyInstanceID
  24. from synapse.util.caches.response_cache import ResponseCache
  25. if TYPE_CHECKING:
  26. from synapse.appservice import ApplicationService
  27. logger = logging.getLogger(__name__)
  28. sent_transactions_counter = Counter(
  29. "synapse_appservice_api_sent_transactions",
  30. "Number of /transactions/ requests sent",
  31. ["service"],
  32. )
  33. failed_transactions_counter = Counter(
  34. "synapse_appservice_api_failed_transactions",
  35. "Number of /transactions/ requests that failed to send",
  36. ["service"],
  37. )
  38. sent_events_counter = Counter(
  39. "synapse_appservice_api_sent_events", "Number of events sent to the AS", ["service"]
  40. )
  41. HOUR_IN_MS = 60 * 60 * 1000
  42. APP_SERVICE_PREFIX = "/_matrix/app/unstable"
  43. def _is_valid_3pe_metadata(info):
  44. if "instances" not in info:
  45. return False
  46. if not isinstance(info["instances"], list):
  47. return False
  48. return True
  49. def _is_valid_3pe_result(r, field):
  50. if not isinstance(r, dict):
  51. return False
  52. for k in (field, "protocol"):
  53. if k not in r:
  54. return False
  55. if not isinstance(r[k], str):
  56. return False
  57. if "fields" not in r:
  58. return False
  59. fields = r["fields"]
  60. if not isinstance(fields, dict):
  61. return False
  62. return True
  63. class ApplicationServiceApi(SimpleHttpClient):
  64. """This class manages HS -> AS communications, including querying and
  65. pushing.
  66. """
  67. def __init__(self, hs):
  68. super().__init__(hs)
  69. self.clock = hs.get_clock()
  70. self.protocol_meta_cache = ResponseCache(
  71. hs.get_clock(), "as_protocol_meta", timeout_ms=HOUR_IN_MS
  72. ) # type: ResponseCache[Tuple[str, str]]
  73. async def query_user(self, service, user_id):
  74. if service.url is None:
  75. return False
  76. uri = service.url + ("/users/%s" % urllib.parse.quote(user_id))
  77. try:
  78. response = await self.get_json(uri, {"access_token": service.hs_token})
  79. if response is not None: # just an empty json object
  80. return True
  81. except CodeMessageException as e:
  82. if e.code == 404:
  83. return False
  84. logger.warning("query_user to %s received %s", uri, e.code)
  85. except Exception as ex:
  86. logger.warning("query_user to %s threw exception %s", uri, ex)
  87. return False
  88. async def query_alias(self, service, alias):
  89. if service.url is None:
  90. return False
  91. uri = service.url + ("/rooms/%s" % urllib.parse.quote(alias))
  92. try:
  93. response = await self.get_json(uri, {"access_token": service.hs_token})
  94. if response is not None: # just an empty json object
  95. return True
  96. except CodeMessageException as e:
  97. logger.warning("query_alias to %s received %s", uri, e.code)
  98. if e.code == 404:
  99. return False
  100. except Exception as ex:
  101. logger.warning("query_alias to %s threw exception %s", uri, ex)
  102. return False
  103. async def query_3pe(self, service, kind, protocol, fields):
  104. if kind == ThirdPartyEntityKind.USER:
  105. required_field = "userid"
  106. elif kind == ThirdPartyEntityKind.LOCATION:
  107. required_field = "alias"
  108. else:
  109. raise ValueError("Unrecognised 'kind' argument %r to query_3pe()", kind)
  110. if service.url is None:
  111. return []
  112. uri = "%s%s/thirdparty/%s/%s" % (
  113. service.url,
  114. APP_SERVICE_PREFIX,
  115. kind,
  116. urllib.parse.quote(protocol),
  117. )
  118. try:
  119. response = await self.get_json(uri, fields)
  120. if not isinstance(response, list):
  121. logger.warning(
  122. "query_3pe to %s returned an invalid response %r", uri, response
  123. )
  124. return []
  125. ret = []
  126. for r in response:
  127. if _is_valid_3pe_result(r, field=required_field):
  128. ret.append(r)
  129. else:
  130. logger.warning(
  131. "query_3pe to %s returned an invalid result %r", uri, r
  132. )
  133. return ret
  134. except Exception as ex:
  135. logger.warning("query_3pe to %s threw exception %s", uri, ex)
  136. return []
  137. async def get_3pe_protocol(
  138. self, service: "ApplicationService", protocol: str
  139. ) -> Optional[JsonDict]:
  140. if service.url is None:
  141. return {}
  142. async def _get() -> Optional[JsonDict]:
  143. uri = "%s%s/thirdparty/protocol/%s" % (
  144. service.url,
  145. APP_SERVICE_PREFIX,
  146. urllib.parse.quote(protocol),
  147. )
  148. try:
  149. info = await self.get_json(uri)
  150. if not _is_valid_3pe_metadata(info):
  151. logger.warning(
  152. "query_3pe_protocol to %s did not return a valid result", uri
  153. )
  154. return None
  155. for instance in info.get("instances", []):
  156. network_id = instance.get("network_id", None)
  157. if network_id is not None:
  158. instance["instance_id"] = ThirdPartyInstanceID(
  159. service.id, network_id
  160. ).to_string()
  161. return info
  162. except Exception as ex:
  163. logger.warning("query_3pe_protocol to %s threw exception %s", uri, ex)
  164. return None
  165. key = (service.id, protocol)
  166. return await self.protocol_meta_cache.wrap(key, _get)
  167. async def push_bulk(
  168. self,
  169. service: "ApplicationService",
  170. events: List[EventBase],
  171. ephemeral: List[JsonDict],
  172. txn_id: Optional[int] = None,
  173. ):
  174. if service.url is None:
  175. return True
  176. events = self._serialize(service, events)
  177. if txn_id is None:
  178. logger.warning(
  179. "push_bulk: Missing txn ID sending events to %s", service.url
  180. )
  181. txn_id = 0
  182. uri = service.url + ("/transactions/%s" % urllib.parse.quote(str(txn_id)))
  183. # Never send ephemeral events to appservices that do not support it
  184. if service.supports_ephemeral:
  185. body = {"events": events, "de.sorunome.msc2409.ephemeral": ephemeral}
  186. else:
  187. body = {"events": events}
  188. try:
  189. await self.put_json(
  190. uri=uri,
  191. json_body=body,
  192. args={"access_token": service.hs_token},
  193. )
  194. sent_transactions_counter.labels(service.id).inc()
  195. sent_events_counter.labels(service.id).inc(len(events))
  196. return True
  197. except CodeMessageException as e:
  198. logger.warning("push_bulk to %s received %s", uri, e.code)
  199. except Exception as ex:
  200. logger.warning("push_bulk to %s threw exception %s", uri, ex)
  201. failed_transactions_counter.labels(service.id).inc()
  202. return False
  203. def _serialize(self, service, events):
  204. time_now = self.clock.time_msec()
  205. return [
  206. serialize_event(
  207. e,
  208. time_now,
  209. as_client_event=True,
  210. is_invite=(
  211. e.type == EventTypes.Member
  212. and e.membership == "invite"
  213. and service.is_interested_in_user(e.state_key)
  214. ),
  215. )
  216. for e in events
  217. ]