|
| 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 | +``` |
0 commit comments