This repository has been archived by the owner on Apr 26, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Add a Synapse Module for configuring presence update routing #9491
Merged
Merged
Changes from 32 commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
de8c33e
Add documentation for implementing a PresenceRouter module
anoadragon453 09eb6fd
Clean up presence config, allow specifying a presence_router module
anoadragon453 5751f6d
Add a built-in PresenceRouter class
anoadragon453 3600d63
Modify `get_interested_parties` and `get_interested_remotes` to query…
anoadragon453 f62e385
Add a func to ModuleApi to send all local online user presence to a s…
anoadragon453 2a0c785
Update PresenceHandler to call PresenceRouter methods when applicable
anoadragon453 08f39cf
Update method calls to thread presence_router through to presence han…
anoadragon453 5c5eb45
Add tests for PresenceRouter and new module_api method
anoadragon453 ff6d051
Changelog
anoadragon453 b67b071
Remove Literal as it's broken on py35-olddeps
anoadragon453 41f9cd1
Social distancing for example module
anoadragon453 5fc716c
Specify PresenceRouter class method definitions up front
anoadragon453 a2a60e0
Add docstring to (and remove useless arg from) method
anoadragon453 997a81b
Raise if required PresenceRouter methods are not implemented
anoadragon453 41537b9
Note that AS's are not currently supported
anoadragon453 4fcd817
Don't just blindly copy our PoC code
anoadragon453 744bb53
typo fix
anoadragon453 6daf640
Add a test for sending all local online presence over federation
anoadragon453 5e2a047
Fix burst federation presence sending implementation
anoadragon453 37d30d7
Fix cyclic dependency import
anoadragon453 1dfc8cc
Only load federation_sender where it's needed in ModuleApi
anoadragon453 42a7db2
Merge branch 'develop' of github.com:matrix-org/synapse into anoa/pre…
anoadragon453 23c5b93
presence != federation
anoadragon453 7c1eedb
Merge branch 'develop' of github.com:matrix-org/synapse into anoa/pre…
anoadragon453 5549f52
Make "ALL" return value a constant
anoadragon453 6fae9f3
Wording fixes
anoadragon453 2f16ed0
Refactor _filter_all_presence_updates_for_user; always filter through PR
anoadragon453 36a7bd2
Make ModuleApi._send_full_presence_to_local_users private
anoadragon453 9186191
Add a couple tests for send_local_online_presence_to w/o a PresenceRo…
anoadragon453 ac4b0ff
Move PresenceRouter module_api test to test_presence_router
anoadragon453 6e42612
Refactor PresenceRouter tests
anoadragon453 25de4c1
Sort some eyes
anoadragon453 a1a52f4
Some clarifications to the module documentation
anoadragon453 e538126
Clarify text in a comment and docstring
anoadragon453 9ddbaa8
Merge branch 'develop' of github.com:matrix-org/synapse into anoa/pre…
anoadragon453 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add a Synapse module for routing presence updates between users. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
# Presence Router Module | ||
|
||
Synapse supports configuring a module that can specify additional users | ||
(local or remote) to should receive certain presence updates from local | ||
users. | ||
|
||
Note that routing presence via an Application Service transactions is not | ||
currently supported. | ||
|
||
The presence routing module is implemented as a Python class, which will be imported by | ||
the running Synapse. | ||
|
||
## Python Presence Router Class | ||
|
||
The Python class is instantiated with two objects: | ||
|
||
* A configuration object of some type (see below). | ||
* An instance of `synapse.module_api.ModuleApi`. | ||
|
||
It then implements methods related to presence routing. | ||
|
||
Note that one method of `ModuleApi` that may be useful is: | ||
|
||
```python | ||
async def ModuleApi.send_local_online_presence_to(users: Iterable[str]) -> None | ||
``` | ||
|
||
which can be given a list of local or remote MXIDs to broadcast known, online user | ||
presence to (for those users that the receiving user is considered interested in). | ||
It does not include state for users who are currently offline, and it can only be | ||
called on workers that support sending federation. | ||
|
||
### Module structure | ||
|
||
Below is a list of possible methods that can be implemented, and whether they are | ||
required. | ||
|
||
#### `parse_config` | ||
|
||
```python | ||
def parse_config(config_dict: dict) -> Any | ||
``` | ||
**Required.** A static method that is passed a dictionary of config options, and | ||
should return a validated config object. This method is described further in | ||
[Configuration](#configuration). | ||
|
||
#### `get_users_for_states` | ||
|
||
```python | ||
async def get_users_for_states( | ||
self, | ||
state_updates: Iterable[UserPresenceState], | ||
) -> Dict[str, Set[UserPresenceState]]: | ||
``` | ||
|
||
**Required.** An asynchronous method that is passed an iterable of user presence | ||
state. This method can determine whether a given presence update should be sent to certain | ||
users. It does this by returning a dictionary with keys representing local or remote | ||
Matrix User IDs, and values being a python set | ||
of `synapse.handlers.presence.UserPresenceState` instances. | ||
|
||
Synapse will then attempt to send the specified presence updates to each user when | ||
possible. | ||
|
||
#### `get_interested_users` | ||
|
||
```python | ||
async def get_interested_users(self, user_id: str) -> Union[Set[str], str] | ||
``` | ||
|
||
**Required.** An asynchronous method that is passed a single Matrix User ID. This | ||
method is expected to return the users that the passed in user may be interested in the | ||
presence of, in addition to users they share a room with. This may be local or remote | ||
users. It does so by returning a python set of Matrix User IDs, or the object | ||
`synapse.events.presence_router.PresenceRouter.ALL_USERS` to indicate that the passed | ||
user should receive presence information for *all* known users. | ||
|
||
For clarity, if the user `@alice:example.org` is passed to this method, and the Set | ||
`{"@bob:example.com", "@charlie:somewhere.org"}` is returned, this signifies that Alice | ||
should receive presence updates sent by Bob and Charlie, regardless of whether these | ||
users share a room. | ||
|
||
### Example | ||
anoadragon453 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Below is an example implementation of a presence router class. | ||
|
||
```python | ||
from typing import Dict, Iterable, Set, Union | ||
from synapse.events.presence_router import PresenceRouter | ||
from synapse.handlers.presence import UserPresenceState | ||
from synapse.module_api import ModuleApi | ||
|
||
class PresenceRouterConfig: | ||
def __init__(self): | ||
# Config options with their defaults | ||
# A list of users to always send all user presence updates to | ||
self.always_send_to_users = [] # type: List[str] | ||
|
||
# A list of users to ignore presence updates for. Does not affect | ||
# shared-room presence relationships | ||
self.blacklisted_users = [] # type: List[str] | ||
|
||
class ExamplePresenceRouter: | ||
"""An example implementation of synapse.presence_router.PresenceRouter. | ||
Supports routing all presence to a configured set of users, or a subset | ||
of presence from certain users to members of certain rooms. | ||
|
||
Args: | ||
config: A configuration object. | ||
module_api: An instance of Synapse's ModuleApi. | ||
""" | ||
def __init__(self, config: PresenceRouterConfig, module_api: ModuleApi): | ||
self._config = config | ||
self._module_api = module_api | ||
|
||
@staticmethod | ||
def parse_config(config_dict: dict) -> PresenceRouterConfig: | ||
"""Parse a configuration dictionary from the homeserver config, do | ||
some validation and return a typed PresenceRouterConfig. | ||
|
||
Args: | ||
config_dict: The configuration dictionary. | ||
|
||
Returns: | ||
A validated config object. | ||
""" | ||
# Initialise a typed config object | ||
config = PresenceRouterConfig() | ||
always_send_to_users = config_dict.get("always_send_to_users") | ||
blacklisted_users = config_dict.get("blacklisted_users") | ||
|
||
# Do some validation of config options... otherwise raise a | ||
# synapse.config.ConfigError. | ||
config.always_send_to_users = always_send_to_users | ||
config.blacklisted_users = blacklisted_users | ||
|
||
return config | ||
|
||
async def get_users_for_states( | ||
self, | ||
state_updates: Iterable[UserPresenceState], | ||
) -> Dict[str, Set[UserPresenceState]]: | ||
"""Given an iterable of user presence updates, determine where each one | ||
needs to go. Returned results will not affect presence updates that are | ||
sent between users who share a room. | ||
|
||
Args: | ||
state_updates: An iterable of user presence state updates. | ||
|
||
Returns: | ||
A dictionary of user_id -> set of UserPresenceState that the user should | ||
receive. | ||
""" | ||
destination_users = {} # type: Dict[str, Set[UserPresenceState] | ||
|
||
# Ignore any updates for blacklisted users | ||
desired_updates = set() | ||
for update in state_updates: | ||
if update.state_key not in self._config.blacklisted_users: | ||
desired_updates.add(update) | ||
|
||
# Send all presence updates to specific users | ||
for user_id in self._config.always_send_to_users: | ||
destination_users[user_id] = desired_updates | ||
|
||
return destination_users | ||
|
||
async def get_interested_users( | ||
self, | ||
user_id: str, | ||
) -> Union[Set[str], PresenceRouter.ALL_USERS]: | ||
""" | ||
Retrieve a list of users that `user_id` is interested in receiving the | ||
presence of. This will be in addition to those they share a room with. | ||
Optionally, the object PresenceRouter.ALL_USERS can be returned to indicate | ||
that this user should receive all incoming local and remote presence updates. | ||
|
||
Note that this method will only be called for local users. | ||
|
||
Args: | ||
user_id: A user requesting presence updates. | ||
|
||
Returns: | ||
A set of user IDs to return additional presence updates for, or | ||
PresenceRouter.ALL_USERS to return presence updates for all other users. | ||
""" | ||
if user_id in self._config.always_send_to_users: | ||
return PresenceRouter.ALL_USERS | ||
|
||
return set() | ||
anoadragon453 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
#### A note on `get_users_for_states` and `get_interested_users` | ||
|
||
Both of these methods are effectively two different sides of the same coin. The logic | ||
regarding which users should receive updates for other users should be the same | ||
between them. | ||
|
||
`get_users_for_states` is called when presence updates come in from either federation | ||
or local users, and is used to either direct local presence to remote users, or to | ||
wake up the sync streams of local users to collect remote presence. | ||
|
||
In contrast, `get_interested_users` is used to determine the users that presence should | ||
be fetched for when a local user is syncing. This presence is then retrieved, before | ||
being fed through `get_users_for_states` once again, with only the syncing user's | ||
routing information pulled from the resulting dictionary. | ||
|
||
Their routing logic should thus line up, else you may run into unintended behaviour. | ||
|
||
## Configuration | ||
|
||
Once you've crafted your module and installed it into the same Python environment as | ||
Synapse, amend your homeserver config file with the following. | ||
|
||
```yaml | ||
presence: | ||
routing_module: | ||
module: my_module.ExamplePresenceRouter | ||
config: | ||
# Any configuration options for your module. The below is an example. | ||
# of setting options for ExamplePresenceRouter. | ||
always_send_to_users: ["@presence_gobbler:example.org"] | ||
blacklisted_users: | ||
- "@alice:example.com" | ||
- "@bob:example.com" | ||
... | ||
``` | ||
|
||
The contents of `config` will be passed as a Python dictionary to the static | ||
`parse_config` method of your class. The object returned by this method will | ||
then be passed to the `__init__` method of your module as `config`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was asked today about how one would implement this module for an application service, instead of a user calling
/sync
, and had a bit of a look through the code. I noticed a couple of things:There's some code that determines whether an application service is interested in a user or a room. This is used to check whether we need to consider sending an event linked to a room/user to the application service. We don't hook into this yet. I suppose we could add a call to
get_interested_users
here and call it with theuser_id
argument set to the Application Service'ssender_localpart
user?get_users_for_states
would still be called to filter presence updates once an application service had been declared interested.This could also be worked around by having the AS expand their namespace regexes, thus making them interested in a wide range of users (potentially all), but I think that's a bit hacky.
The
ModuleApi.send_local_online_presence_to
method is currently only set up to modify/sync
responses. I know we're trying to discourage AS users from syncing. So we probably need to modify the method to check if the user is a known application servicesender_localpart
, and if so send a burst of all current user presence states in a transaction.I'll also add some tests for application services to verify what does and doesn't already work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After testing locally appservices do not currently receive presence updates.
However in the interest of not complicating this PR further, I'm going to leave a note about this and aim to fix it in a future PR.