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

Commit 1b3e398

Browse files
authored
Standardise the module interface (#10062)
This PR adds a common configuration section for all modules (see docs). These modules are then loaded at startup by the homeserver. Modules register their hooks and web resources using the new `register_[...]_callbacks` and `register_web_resource` methods of the module API.
1 parent 91fa9cc commit 1b3e398

23 files changed

+769
-188
lines changed

UPGRADE.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,23 @@ for example:
8585
wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb
8686
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
8787
88+
Upgrading to v1.37.0
89+
====================
90+
91+
Deprecation of the current spam checker interface
92+
-------------------------------------------------
93+
94+
The current spam checker interface is deprecated in favour of a new generic modules system.
95+
Authors of spam checker modules can refer to `this documentation <https://matrix-org.github.io/synapse/develop/modules.html#porting-an-existing-module-that-uses-the-old-interface>`_
96+
to update their modules. Synapse administrators can refer to `this documentation <https://matrix-org.github.io/synapse/develop/modules.html#using-modules>`_
97+
to update their configuration once the modules they are using have been updated.
98+
99+
We plan to remove support for the current spam checker interface in August 2021.
100+
101+
More module interfaces will be ported over to this new generic system in future versions
102+
of Synapse.
103+
104+
88105
Upgrading to v1.34.0
89106
====================
90107

changelog.d/10062.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Standardised the module interface.

changelog.d/10062.removal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/master/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system.

docs/SUMMARY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
- [URL Previews](url_previews.md)
3636
- [User Directory](user_directory.md)
3737
- [Message Retention Policies](message_retention_policies.md)
38-
- [Pluggable Modules]()
38+
- [Pluggable Modules](modules.md)
3939
- [Third Party Rules]()
4040
- [Spam Checker](spam_checker.md)
4141
- [Presence Router](presence_router_module.md)

docs/modules.md

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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. Currently, only the
29+
spam checker interface is compatible with this new system.
30+
31+
## Writing a module
32+
33+
A module is a Python class that uses Synapse's module API to interact with the
34+
homeserver. It can register callbacks that Synapse will call on specific operations, as
35+
well as web resources to attach to Synapse's web server.
36+
37+
When instantiated, a module is given its parsed configuration as well as an instance of
38+
the `synapse.module_api.ModuleApi` class. The configuration is a dictionary, and is
39+
either the output of the module's `parse_config` static method (see below), or the
40+
configuration associated with the module in Synapse's configuration file.
41+
42+
See the documentation for the `ModuleApi` class
43+
[here](https://github.com/matrix-org/synapse/blob/master/synapse/module_api/__init__.py).
44+
45+
### Handling the module's configuration
46+
47+
A module can implement the following static method:
48+
49+
```python
50+
@staticmethod
51+
def parse_config(config: dict) -> dict
52+
```
53+
54+
This method is given a dictionary resulting from parsing the YAML configuration for the
55+
module. It may modify it (for example by parsing durations expressed as strings (e.g.
56+
"5d") into milliseconds, etc.), and return the modified dictionary. It may also verify
57+
that the configuration is correct, and raise an instance of
58+
`synapse.module_api.errors.ConfigError` if not.
59+
60+
### Registering a web resource
61+
62+
Modules can register web resources onto Synapse's web server using the following module
63+
API method:
64+
65+
```python
66+
def ModuleApi.register_web_resource(path: str, resource: IResource)
67+
```
68+
69+
The path is the full absolute path to register the resource at. For example, if you
70+
register a resource for the path `/_synapse/client/my_super_module/say_hello`, Synapse
71+
will serve it at `http(s)://[HS_URL]/_synapse/client/my_super_module/say_hello`. Note
72+
that Synapse does not allow registering resources for several sub-paths in the `/_matrix`
73+
namespace (such as anything under `/_matrix/client` for example). It is strongly
74+
recommended that modules register their web resources under the `/_synapse/client`
75+
namespace.
76+
77+
The provided resource is a Python class that implements Twisted's [IResource](https://twistedmatrix.com/documents/current/api/twisted.web.resource.IResource.html)
78+
interface (such as [Resource](https://twistedmatrix.com/documents/current/api/twisted.web.resource.Resource.html)).
79+
80+
Only one resource can be registered for a given path. If several modules attempt to
81+
register a resource for the same path, the module that appears first in Synapse's
82+
configuration file takes priority.
83+
84+
Modules **must** register their web resources in their `__init__` method.
85+
86+
### Registering a callback
87+
88+
Modules can use Synapse's module API to register callbacks. Callbacks are functions that
89+
Synapse will call when performing specific actions. Callbacks must be asynchronous, and
90+
are split in categories. A single module may implement callbacks from multiple categories,
91+
and is under no obligation to implement all callbacks from the categories it registers
92+
callbacks for.
93+
94+
#### Spam checker callbacks
95+
96+
To register one of the callbacks described in this section, a module needs to use the
97+
module API's `register_spam_checker_callbacks` method. The callback functions are passed
98+
to `register_spam_checker_callbacks` as keyword arguments, with the callback name as the
99+
argument name and the function as its value. This is demonstrated in the example below.
100+
101+
The available spam checker callbacks are:
102+
103+
```python
104+
def check_event_for_spam(event: "synapse.events.EventBase") -> Union[bool, str]
105+
```
106+
107+
Called when receiving an event from a client or via federation. The module can return
108+
either a `bool` to indicate whether the event must be rejected because of spam, or a `str`
109+
to indicate the event must be rejected because of spam and to give a rejection reason to
110+
forward to clients.
111+
112+
```python
113+
def user_may_invite(inviter: str, invitee: str, room_id: str) -> bool
114+
```
115+
116+
Called when processing an invitation. The module must return a `bool` indicating whether
117+
the inviter can invite the invitee to the given room. Both inviter and invitee are
118+
represented by their Matrix user ID (i.e. `@alice:example.com`).
119+
120+
```python
121+
def user_may_create_room(user: str) -> bool
122+
```
123+
124+
Called when processing a room creation request. The module must return a `bool` indicating
125+
whether the given user (represented by their Matrix user ID) is allowed to create a room.
126+
127+
```python
128+
def user_may_create_room_alias(user: str, room_alias: "synapse.types.RoomAlias") -> bool
129+
```
130+
131+
Called when trying to associate an alias with an existing room. The module must return a
132+
`bool` indicating whether the given user (represented by their Matrix user ID) is allowed
133+
to set the given alias.
134+
135+
```python
136+
def user_may_publish_room(user: str, room_id: str) -> bool
137+
```
138+
139+
Called when trying to publish a room to the homeserver's public rooms directory. The
140+
module must return a `bool` indicating whether the given user (represented by their
141+
Matrix user ID) is allowed to publish the given room.
142+
143+
```python
144+
def check_username_for_spam(user_profile: Dict[str, str]) -> bool
145+
```
146+
147+
Called when computing search results in the user directory. The module must return a
148+
`bool` indicating whether the given user profile can appear in search results. The profile
149+
is represented as a dictionary with the following keys:
150+
151+
* `user_id`: The Matrix ID for this user.
152+
* `display_name`: The user's display name.
153+
* `avatar_url`: The `mxc://` URL to the user's avatar.
154+
155+
The module is given a copy of the original dictionary, so modifying it from within the
156+
module cannot modify a user's profile when included in user directory search results.
157+
158+
```python
159+
def check_registration_for_spam(
160+
email_threepid: Optional[dict],
161+
username: Optional[str],
162+
request_info: Collection[Tuple[str, str]],
163+
auth_provider_id: Optional[str] = None,
164+
) -> "synapse.spam_checker_api.RegistrationBehaviour"
165+
```
166+
167+
Called when registering a new user. The module must return a `RegistrationBehaviour`
168+
indicating whether the registration can go through or must be denied, or whether the user
169+
may be allowed to register but will be shadow banned.
170+
171+
The arguments passed to this callback are:
172+
173+
* `email_threepid`: The email address used for registering, if any.
174+
* `username`: The username the user would like to register. Can be `None`, meaning that
175+
Synapse will generate one later.
176+
* `request_info`: A collection of tuples, which first item is a user agent, and which
177+
second item is an IP address. These user agents and IP addresses are the ones that were
178+
used during the registration process.
179+
* `auth_provider_id`: The identifier of the SSO authentication provider, if any.
180+
181+
```python
182+
def check_media_file_for_spam(
183+
file_wrapper: "synapse.rest.media.v1.media_storage.ReadableFileWrapper",
184+
file_info: "synapse.rest.media.v1._base.FileInfo"
185+
) -> bool
186+
```
187+
188+
Called when storing a local or remote file. The module must return a boolean indicating
189+
whether the given file can be stored in the homeserver's media store.
190+
191+
### Porting an existing module that uses the old interface
192+
193+
In order to port a module that uses Synapse's old module interface, its author needs to:
194+
195+
* ensure the module's callbacks are all asynchronous.
196+
* register their callbacks using one or more of the `register_[...]_callbacks` methods
197+
from the `ModuleApi` class in the module's `__init__` method (see [this section](#registering-a-web-resource)
198+
for more info).
199+
200+
Additionally, if the module is packaged with an additional web resource, the module
201+
should register this resource in its `__init__` method using the `register_web_resource`
202+
method from the `ModuleApi` class (see [this section](#registering-a-web-resource) for
203+
more info).
204+
205+
The module's author should also update any example in the module's configuration to only
206+
use the new `modules` section in Synapse's configuration file (see [this section](#using-modules)
207+
for more info).
208+
209+
### Example
210+
211+
The example below is a module that implements the spam checker callback
212+
`user_may_create_room` to deny room creation to user `@evilguy:example.com`, and registers
213+
a web resource to the path `/_synapse/client/demo/hello` that returns a JSON object.
214+
215+
```python
216+
import json
217+
218+
from twisted.web.resource import Resource
219+
from twisted.web.server import Request
220+
221+
from synapse.module_api import ModuleApi
222+
223+
224+
class DemoResource(Resource):
225+
def __init__(self, config):
226+
super(DemoResource, self).__init__()
227+
self.config = config
228+
229+
def render_GET(self, request: Request):
230+
name = request.args.get(b"name")[0]
231+
request.setHeader(b"Content-Type", b"application/json")
232+
return json.dumps({"hello": name})
233+
234+
235+
class DemoModule:
236+
def __init__(self, config: dict, api: ModuleApi):
237+
self.config = config
238+
self.api = api
239+
240+
self.api.register_web_resource(
241+
path="/_synapse/client/demo/hello",
242+
resource=DemoResource(self.config),
243+
)
244+
245+
self.api.register_spam_checker_callbacks(
246+
user_may_create_room=self.user_may_create_room,
247+
)
248+
249+
@staticmethod
250+
def parse_config(config):
251+
return config
252+
253+
async def user_may_create_room(self, user: str) -> bool:
254+
if user == "@evilguy:example.com":
255+
return False
256+
257+
return True
258+
```

docs/sample_config.yaml

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@
3131
#
3232
# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html
3333

34+
35+
## Modules ##
36+
37+
# Server admins can expand Synapse's functionality with external modules.
38+
#
39+
# See https://matrix-org.github.io/synapse/develop/modules.html for more
40+
# documentation on how to configure or create custom modules for Synapse.
41+
#
42+
modules:
43+
# - module: my_super_module.MySuperClass
44+
# config:
45+
# do_thing: true
46+
# - module: my_other_super_module.SomeClass
47+
# config: {}
48+
49+
3450
## Server ##
3551

3652
# The public-facing domain of the server
@@ -2491,19 +2507,6 @@ push:
24912507
#group_unread_count_by_room: false
24922508

24932509

2494-
# Spam checkers are third-party modules that can block specific actions
2495-
# of local users, such as creating rooms and registering undesirable
2496-
# usernames, as well as remote users by redacting incoming events.
2497-
#
2498-
spam_checker:
2499-
#- module: "my_custom_project.SuperSpamChecker"
2500-
# config:
2501-
# example_option: 'things'
2502-
#- module: "some_other_project.BadEventStopper"
2503-
# config:
2504-
# example_stop_events_from: ['@bad:example.com']
2505-
2506-
25072510
## Rooms ##
25082511

25092512
# Controls whether locally-created rooms should be end-to-end encrypted by

docs/spam_checker.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
**Note: this page of the Synapse documentation is now deprecated. For up to date
2+
documentation on setting up or writing a spam checker module, please see
3+
[this page](https://matrix-org.github.io/synapse/develop/modules.html).**
4+
15
# Handling spam in Synapse
26

37
Synapse has support to customize spam checking behavior. It can plug into a

synapse/app/_base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from synapse.app.phone_stats_home import start_phone_stats_home
3636
from synapse.config.homeserver import HomeServerConfig
3737
from synapse.crypto import context_factory
38+
from synapse.events.spamcheck import load_legacy_spam_checkers
3839
from synapse.logging.context import PreserveLoggingContext
3940
from synapse.metrics.background_process_metrics import wrap_as_background_process
4041
from synapse.metrics.jemalloc import setup_jemalloc_stats
@@ -330,6 +331,14 @@ def run_sighup(*args, **kwargs):
330331
# Start the tracer
331332
synapse.logging.opentracing.init_tracer(hs) # type: ignore[attr-defined] # noqa
332333

334+
# Instantiate the modules so they can register their web resources to the module API
335+
# before we start the listeners.
336+
module_api = hs.get_module_api()
337+
for module, config in hs.config.modules.loaded_modules:
338+
module(config=config, api=module_api)
339+
340+
load_legacy_spam_checkers(hs)
341+
333342
# It is now safe to start your Synapse.
334343
hs.start_listening()
335344
hs.get_datastore().db_pool.start_profiling()

synapse/app/generic_worker.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,10 @@ def _listen_http(self, listener_config: ListenerConfig):
354354
if name == "replication":
355355
resources[REPLICATION_PREFIX] = ReplicationRestResource(self)
356356

357+
# Attach additional resources registered by modules.
358+
resources.update(self._module_web_resources)
359+
self._module_web_resources_consumed = True
360+
357361
root_resource = create_resource_tree(resources, OptionsResource())
358362

359363
_base.listen_tcp(

synapse/app/homeserver.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ def _listener_http(self, config: HomeServerConfig, listener_config: ListenerConf
124124
)
125125
resources[path] = resource
126126

127+
# Attach additional resources registered by modules.
128+
resources.update(self._module_web_resources)
129+
self._module_web_resources_consumed = True
130+
127131
# try to find something useful to redirect '/' to
128132
if WEB_CLIENT_PREFIX in resources:
129133
root_resource = RootOptionsRedirectResource(WEB_CLIENT_PREFIX)

0 commit comments

Comments
 (0)