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.
 
 
 
 
 
 

224 line
8.0 KiB

  1. # Copyright 2021 The Matrix.org Foundation C.I.C.
  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. from typing import (
  16. TYPE_CHECKING,
  17. Awaitable,
  18. Callable,
  19. Dict,
  20. Iterable,
  21. List,
  22. Optional,
  23. Set,
  24. Union,
  25. )
  26. from synapse.api.presence import UserPresenceState
  27. from synapse.util.async_helpers import maybe_awaitable
  28. if TYPE_CHECKING:
  29. from synapse.server import HomeServer
  30. GET_USERS_FOR_STATES_CALLBACK = Callable[
  31. [Iterable[UserPresenceState]], Awaitable[Dict[str, Set[UserPresenceState]]]
  32. ]
  33. GET_INTERESTED_USERS_CALLBACK = Callable[
  34. [str], Awaitable[Union[Set[str], "PresenceRouter.ALL_USERS"]]
  35. ]
  36. logger = logging.getLogger(__name__)
  37. def load_legacy_presence_router(hs: "HomeServer"):
  38. """Wrapper that loads a presence router module configured using the old
  39. configuration, and registers the hooks they implement.
  40. """
  41. if hs.config.presence_router_module_class is None:
  42. return
  43. module = hs.config.presence_router_module_class
  44. config = hs.config.presence_router_config
  45. api = hs.get_module_api()
  46. presence_router = module(config=config, module_api=api)
  47. # The known hooks. If a module implements a method which name appears in this set,
  48. # we'll want to register it.
  49. presence_router_methods = {
  50. "get_users_for_states",
  51. "get_interested_users",
  52. }
  53. # All methods that the module provides should be async, but this wasn't enforced
  54. # in the old module system, so we wrap them if needed
  55. def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]:
  56. # f might be None if the callback isn't implemented by the module. In this
  57. # case we don't want to register a callback at all so we return None.
  58. if f is None:
  59. return None
  60. def run(*args, **kwargs):
  61. # mypy doesn't do well across function boundaries so we need to tell it
  62. # f is definitely not None.
  63. assert f is not None
  64. return maybe_awaitable(f(*args, **kwargs))
  65. return run
  66. # Register the hooks through the module API.
  67. hooks = {
  68. hook: async_wrapper(getattr(presence_router, hook, None))
  69. for hook in presence_router_methods
  70. }
  71. api.register_presence_router_callbacks(**hooks)
  72. class PresenceRouter:
  73. """
  74. A module that the homeserver will call upon to help route user presence updates to
  75. additional destinations.
  76. """
  77. ALL_USERS = "ALL"
  78. def __init__(self, hs: "HomeServer"):
  79. # Initially there are no callbacks
  80. self._get_users_for_states_callbacks: List[GET_USERS_FOR_STATES_CALLBACK] = []
  81. self._get_interested_users_callbacks: List[GET_INTERESTED_USERS_CALLBACK] = []
  82. def register_presence_router_callbacks(
  83. self,
  84. get_users_for_states: Optional[GET_USERS_FOR_STATES_CALLBACK] = None,
  85. get_interested_users: Optional[GET_INTERESTED_USERS_CALLBACK] = None,
  86. ):
  87. # PresenceRouter modules are required to implement both of these methods
  88. # or neither of them as they are assumed to act in a complementary manner
  89. paired_methods = [get_users_for_states, get_interested_users]
  90. if paired_methods.count(None) == 1:
  91. raise RuntimeError(
  92. "PresenceRouter modules must register neither or both of the paired callbacks: "
  93. "[get_users_for_states, get_interested_users]"
  94. )
  95. # Append the methods provided to the lists of callbacks
  96. if get_users_for_states is not None:
  97. self._get_users_for_states_callbacks.append(get_users_for_states)
  98. if get_interested_users is not None:
  99. self._get_interested_users_callbacks.append(get_interested_users)
  100. async def get_users_for_states(
  101. self,
  102. state_updates: Iterable[UserPresenceState],
  103. ) -> Dict[str, Set[UserPresenceState]]:
  104. """
  105. Given an iterable of user presence updates, determine where each one
  106. needs to go.
  107. Args:
  108. state_updates: An iterable of user presence state updates.
  109. Returns:
  110. A dictionary of user_id -> set of UserPresenceState, indicating which
  111. presence updates each user should receive.
  112. """
  113. # Bail out early if we don't have any callbacks to run.
  114. if len(self._get_users_for_states_callbacks) == 0:
  115. # Don't include any extra destinations for presence updates
  116. return {}
  117. users_for_states = {}
  118. # run all the callbacks for get_users_for_states and combine the results
  119. for callback in self._get_users_for_states_callbacks:
  120. try:
  121. result = await callback(state_updates)
  122. except Exception as e:
  123. logger.warning("Failed to run module API callback %s: %s", callback, e)
  124. continue
  125. if not isinstance(result, Dict):
  126. logger.warning(
  127. "Wrong type returned by module API callback %s: %s, expected Dict",
  128. callback,
  129. result,
  130. )
  131. continue
  132. for key, new_entries in result.items():
  133. if not isinstance(new_entries, Set):
  134. logger.warning(
  135. "Wrong type returned by module API callback %s: %s, expected Set",
  136. callback,
  137. new_entries,
  138. )
  139. break
  140. users_for_states.setdefault(key, set()).update(new_entries)
  141. return users_for_states
  142. async def get_interested_users(self, user_id: str) -> Union[Set[str], ALL_USERS]:
  143. """
  144. Retrieve a list of users that `user_id` is interested in receiving the
  145. presence of. This will be in addition to those they share a room with.
  146. Optionally, the object PresenceRouter.ALL_USERS can be returned to indicate
  147. that this user should receive all incoming local and remote presence updates.
  148. Note that this method will only be called for local users, but can return users
  149. that are local or remote.
  150. Args:
  151. user_id: A user requesting presence updates.
  152. Returns:
  153. A set of user IDs to return presence updates for, or ALL_USERS to return all
  154. known updates.
  155. """
  156. # Bail out early if we don't have any callbacks to run.
  157. if len(self._get_interested_users_callbacks) == 0:
  158. # Don't report any additional interested users
  159. return set()
  160. interested_users = set()
  161. # run all the callbacks for get_interested_users and combine the results
  162. for callback in self._get_interested_users_callbacks:
  163. try:
  164. result = await callback(user_id)
  165. except Exception as e:
  166. logger.warning("Failed to run module API callback %s: %s", callback, e)
  167. continue
  168. # If one of the callbacks returns ALL_USERS then we can stop calling all
  169. # of the other callbacks, since the set of interested_users is already as
  170. # large as it can possibly be
  171. if result == PresenceRouter.ALL_USERS:
  172. return PresenceRouter.ALL_USERS
  173. if not isinstance(result, Set):
  174. logger.warning(
  175. "Wrong type returned by module API callback %s: %s, expected set",
  176. callback,
  177. result,
  178. )
  179. continue
  180. # Add the new interested users to the set
  181. interested_users.update(result)
  182. return interested_users