Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 03caba6

Browse files
authored
Improve the modules doc (#10758)
* Split up the documentation in several files rather than one huge one * Add examples for each callback category * Other niceties like fixing #10632 * Add titles to callbacks so they're easier to find in the navigation panels and link to
1 parent 01df612 commit 03caba6

10 files changed

+537
-404
lines changed

changelog.d/10758.doc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Split up the modules documentation and add examples for module developers.

docs/SUMMARY.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@
3737
- [URL Previews](development/url_previews.md)
3838
- [User Directory](user_directory.md)
3939
- [Message Retention Policies](message_retention_policies.md)
40-
- [Pluggable Modules](modules.md)
41-
- [Third Party Rules]()
42-
- [Spam Checker](spam_checker.md)
43-
- [Presence Router](presence_router_module.md)
44-
- [Media Storage Providers]()
40+
- [Pluggable Modules](modules/index.md)
41+
- [Writing a module](modules/writing_a_module.md)
42+
- [Spam checker callbacks](modules/spam_checker_callbacks.md)
43+
- [Third-party rules callbacks](modules/third_party_rules_callbacks.md)
44+
- [Presence router callbacks](modules/presence_router_callbacks.md)
45+
- [Account validity callbacks](modules/account_validity_callbacks.md)
46+
- [Porting a legacy module to the new interface](modules/porting_legacy_module.md)
4547
- [Workers](workers.md)
4648
- [Using `synctl` with Workers](synctl_workers.md)
4749
- [Systemd](systemd-with-workers/README.md)

docs/modules.md

Lines changed: 0 additions & 399 deletions
This file was deleted.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Account validity callbacks
2+
3+
Account validity callbacks allow module developers to add extra steps to verify the
4+
validity on an account, i.e. see if a user can be granted access to their account on the
5+
Synapse instance. Account validity callbacks can be registered using the module API's
6+
`register_account_validity_callbacks` method.
7+
8+
The available account validity callbacks are:
9+
10+
### `is_user_expired`
11+
12+
```python
13+
async def is_user_expired(user: str) -> Optional[bool]
14+
```
15+
16+
Called when processing any authenticated request (except for logout requests). The module
17+
can return a `bool` to indicate whether the user has expired and should be locked out of
18+
their account, or `None` if the module wasn't able to figure it out. The user is
19+
represented by their Matrix user ID (e.g. `@alice:example.com`).
20+
21+
If the module returns `True`, the current request will be denied with the error code
22+
`ORG_MATRIX_EXPIRED_ACCOUNT` and the HTTP status code 403. Note that this doesn't
23+
invalidate the user's access token.
24+
25+
### `on_user_registration`
26+
27+
```python
28+
async def on_user_registration(user: str) -> None
29+
```
30+
31+
Called after successfully registering a user, in case the module needs to perform extra
32+
operations to keep track of them. (e.g. add them to a database table). The user is
33+
represented by their Matrix user ID.

docs/modules/index.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Modules
2+
3+
Synapse supports extending its functionality by configuring external modules.
4+
5+
## Using modules
6+
7+
To use a module on Synapse, add it to the `modules` section of the configuration file:
8+
9+
```yaml
10+
modules:
11+
- module: my_super_module.MySuperClass
12+
config:
13+
do_thing: true
14+
- module: my_other_super_module.SomeClass
15+
config: {}
16+
```
17+
18+
Each module is defined by a path to a Python class as well as a configuration. This
19+
information for a given module should be available in the module's own documentation.
20+
21+
**Note**: When using third-party modules, you effectively allow someone else to run
22+
custom code on your Synapse homeserver. Server admins are encouraged to verify the
23+
provenance of the modules they use on their homeserver and make sure the modules aren't
24+
running malicious code on their instance.
25+
26+
Also note that we are currently in the process of migrating module interfaces to this
27+
system. While some interfaces might be compatible with it, others still require
28+
configuring modules in another part of Synapse's configuration file.
29+
30+
Currently, only the following pre-existing interfaces are compatible with this new system:
31+
32+
* spam checker
33+
* third-party rules
34+
* presence router

docs/modules/porting_legacy_module.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Porting an existing module that uses the old interface
2+
3+
In order to port a module that uses Synapse's old module interface, its author needs to:
4+
5+
* ensure the module's callbacks are all asynchronous.
6+
* register their callbacks using one or more of the `register_[...]_callbacks` methods
7+
from the `ModuleApi` class in the module's `__init__` method (see [this section](writing_a_module.html#registering-a-callback)
8+
for more info).
9+
10+
Additionally, if the module is packaged with an additional web resource, the module
11+
should register this resource in its `__init__` method using the `register_web_resource`
12+
method from the `ModuleApi` class (see [this section](writing_a_module.html#registering-a-web-resource) for
13+
more info).
14+
15+
The module's author should also update any example in the module's configuration to only
16+
use the new `modules` section in Synapse's configuration file (see [this section](index.html#using-modules)
17+
for more info).
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Presence router callbacks
2+
3+
Presence router callbacks allow module developers to specify additional users (local or remote)
4+
to receive certain presence updates from local users. Presence router callbacks can be
5+
registered using the module API's `register_presence_router_callbacks` method.
6+
7+
## Callbacks
8+
9+
The available presence router callbacks are:
10+
11+
### `get_users_for_states`
12+
13+
```python
14+
async def get_users_for_states(
15+
state_updates: Iterable["synapse.api.UserPresenceState"],
16+
) -> Dict[str, Set["synapse.api.UserPresenceState"]]
17+
```
18+
**Requires** `get_interested_users` to also be registered
19+
20+
Called when processing updates to the presence state of one or more users. This callback can
21+
be used to instruct the server to forward that presence state to specific users. The module
22+
must return a dictionary that maps from Matrix user IDs (which can be local or remote) to the
23+
`UserPresenceState` changes that they should be forwarded.
24+
25+
Synapse will then attempt to send the specified presence updates to each user when possible.
26+
27+
### `get_interested_users`
28+
29+
```python
30+
async def get_interested_users(
31+
user_id: str
32+
) -> Union[Set[str], "synapse.module_api.PRESENCE_ALL_USERS"]
33+
```
34+
**Requires** `get_users_for_states` to also be registered
35+
36+
Called when determining which users someone should be able to see the presence state of. This
37+
callback should return complementary results to `get_users_for_state` or the presence information
38+
may not be properly forwarded.
39+
40+
The callback is given the Matrix user ID for a local user that is requesting presence data and
41+
should return the Matrix user IDs of the users whose presence state they are allowed to
42+
query. The returned users can be local or remote.
43+
44+
Alternatively the callback can return `synapse.module_api.PRESENCE_ALL_USERS`
45+
to indicate that the user should receive updates from all known users.
46+
47+
## Example
48+
49+
The example below is a module that implements both presence router callbacks, and ensures
50+
that `@alice:example.org` receives all presence updates from `@bob:example.com` and
51+
`@charlie:somewhere.org`, regardless of whether Alice shares a room with any of them.
52+
53+
```python
54+
from typing import Dict, Iterable, Set, Union
55+
56+
from synapse.module_api import ModuleApi
57+
58+
59+
class CustomPresenceRouter:
60+
def __init__(self, config: dict, api: ModuleApi):
61+
self.api = api
62+
63+
self.api.register_presence_router_callbacks(
64+
get_users_for_states=self.get_users_for_states,
65+
get_interested_users=self.get_interested_users,
66+
)
67+
68+
async def get_users_for_states(
69+
self,
70+
state_updates: Iterable["synapse.api.UserPresenceState"],
71+
) -> Dict[str, Set["synapse.api.UserPresenceState"]]:
72+
res = {}
73+
for update in state_updates:
74+
if (
75+
update.user_id == "@bob:example.com"
76+
or update.user_id == "@charlie:somewhere.org"
77+
):
78+
res.setdefault("@alice:example.com", set()).add(update)
79+
80+
return res
81+
82+
async def get_interested_users(
83+
self,
84+
user_id: str,
85+
) -> Union[Set[str], "synapse.module_api.PRESENCE_ALL_USERS"]:
86+
if user_id == "@alice:example.com":
87+
return {"@bob:example.com", "@charlie:somewhere.org"}
88+
89+
return set()
90+
```
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Spam checker callbacks
2+
3+
Spam checker callbacks allow module developers to implement spam mitigation actions for
4+
Synapse instances. Spam checker callbacks can be registered using the module API's
5+
`register_spam_checker_callbacks` method.
6+
7+
## Callbacks
8+
9+
The available spam checker callbacks are:
10+
11+
### `check_event_for_spam`
12+
13+
```python
14+
async def check_event_for_spam(event: "synapse.events.EventBase") -> Union[bool, str]
15+
```
16+
17+
Called when receiving an event from a client or via federation. The module can return
18+
either a `bool` to indicate whether the event must be rejected because of spam, or a `str`
19+
to indicate the event must be rejected because of spam and to give a rejection reason to
20+
forward to clients.
21+
22+
### `user_may_invite`
23+
24+
```python
25+
async def user_may_invite(inviter: str, invitee: str, room_id: str) -> bool
26+
```
27+
28+
Called when processing an invitation. The module must return a `bool` indicating whether
29+
the inviter can invite the invitee to the given room. Both inviter and invitee are
30+
represented by their Matrix user ID (e.g. `@alice:example.com`).
31+
32+
### `user_may_create_room`
33+
34+
```python
35+
async def user_may_create_room(user: str) -> bool
36+
```
37+
38+
Called when processing a room creation request. The module must return a `bool` indicating
39+
whether the given user (represented by their Matrix user ID) is allowed to create a room.
40+
41+
### `user_may_create_room_alias`
42+
43+
```python
44+
async def user_may_create_room_alias(user: str, room_alias: "synapse.types.RoomAlias") -> bool
45+
```
46+
47+
Called when trying to associate an alias with an existing room. The module must return a
48+
`bool` indicating whether the given user (represented by their Matrix user ID) is allowed
49+
to set the given alias.
50+
51+
### `user_may_publish_room`
52+
53+
```python
54+
async def user_may_publish_room(user: str, room_id: str) -> bool
55+
```
56+
57+
Called when trying to publish a room to the homeserver's public rooms directory. The
58+
module must return a `bool` indicating whether the given user (represented by their
59+
Matrix user ID) is allowed to publish the given room.
60+
61+
### `check_username_for_spam`
62+
63+
```python
64+
async def check_username_for_spam(user_profile: Dict[str, str]) -> bool
65+
```
66+
67+
Called when computing search results in the user directory. The module must return a
68+
`bool` indicating whether the given user profile can appear in search results. The profile
69+
is represented as a dictionary with the following keys:
70+
71+
* `user_id`: The Matrix ID for this user.
72+
* `display_name`: The user's display name.
73+
* `avatar_url`: The `mxc://` URL to the user's avatar.
74+
75+
The module is given a copy of the original dictionary, so modifying it from within the
76+
module cannot modify a user's profile when included in user directory search results.
77+
78+
### `check_registration_for_spam`
79+
80+
```python
81+
async def check_registration_for_spam(
82+
email_threepid: Optional[dict],
83+
username: Optional[str],
84+
request_info: Collection[Tuple[str, str]],
85+
auth_provider_id: Optional[str] = None,
86+
) -> "synapse.spam_checker_api.RegistrationBehaviour"
87+
```
88+
89+
Called when registering a new user. The module must return a `RegistrationBehaviour`
90+
indicating whether the registration can go through or must be denied, or whether the user
91+
may be allowed to register but will be shadow banned.
92+
93+
The arguments passed to this callback are:
94+
95+
* `email_threepid`: The email address used for registering, if any.
96+
* `username`: The username the user would like to register. Can be `None`, meaning that
97+
Synapse will generate one later.
98+
* `request_info`: A collection of tuples, which first item is a user agent, and which
99+
second item is an IP address. These user agents and IP addresses are the ones that were
100+
used during the registration process.
101+
* `auth_provider_id`: The identifier of the SSO authentication provider, if any.
102+
103+
### `check_media_file_for_spam`
104+
105+
```python
106+
async def check_media_file_for_spam(
107+
file_wrapper: "synapse.rest.media.v1.media_storage.ReadableFileWrapper",
108+
file_info: "synapse.rest.media.v1._base.FileInfo",
109+
) -> bool
110+
```
111+
112+
Called when storing a local or remote file. The module must return a boolean indicating
113+
whether the given file can be stored in the homeserver's media store.
114+
115+
## Example
116+
117+
The example below is a module that implements the spam checker callback
118+
`check_event_for_spam` to deny any message sent by users whose Matrix user IDs are
119+
mentioned in a configured list, and registers a web resource to the path
120+
`/_synapse/client/list_spam_checker/is_evil` that returns a JSON object indicating
121+
whether the provided user appears in that list.
122+
123+
```python
124+
import json
125+
from typing import Union
126+
127+
from twisted.web.resource import Resource
128+
from twisted.web.server import Request
129+
130+
from synapse.module_api import ModuleApi
131+
132+
133+
class IsUserEvilResource(Resource):
134+
def __init__(self, config):
135+
super(IsUserEvilResource, self).__init__()
136+
self.evil_users = config.get("evil_users") or []
137+
138+
def render_GET(self, request: Request):
139+
user = request.args.get(b"user")[0]
140+
request.setHeader(b"Content-Type", b"application/json")
141+
return json.dumps({"evil": user in self.evil_users})
142+
143+
144+
class ListSpamChecker:
145+
def __init__(self, config: dict, api: ModuleApi):
146+
self.api = api
147+
self.evil_users = config.get("evil_users") or []
148+
149+
self.api.register_spam_checker_callbacks(
150+
check_event_for_spam=self.check_event_for_spam,
151+
)
152+
153+
self.api.register_web_resource(
154+
path="/_synapse/client/list_spam_checker/is_evil",
155+
resource=IsUserEvilResource(config),
156+
)
157+
158+
async def check_event_for_spam(self, event: "synapse.events.EventBase") -> Union[bool, str]:
159+
return event.sender not in self.evil_users
160+
```

0 commit comments

Comments
 (0)