|
| 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_join_room` |
| 23 | + |
| 24 | +```python |
| 25 | +async def user_may_join_room(user: str, room: str, is_invited: bool) -> bool |
| 26 | +``` |
| 27 | + |
| 28 | +Called when a user is trying to join a room. The module must return a `bool` to indicate |
| 29 | +whether the user can join the room. The user is represented by their Matrix user ID (e.g. |
| 30 | +`@alice:example.com`) and the room is represented by its Matrix ID (e.g. |
| 31 | +`!room:example.com`). The module is also given a boolean to indicate whether the user |
| 32 | +currently has a pending invite in the room. |
| 33 | + |
| 34 | +This callback isn't called if the join is performed by a server administrator, or in the |
| 35 | +context of a room creation. |
| 36 | + |
| 37 | +### `user_may_invite` |
| 38 | + |
| 39 | +```python |
| 40 | +async def user_may_invite(inviter: str, invitee: str, room_id: str) -> bool |
| 41 | +``` |
| 42 | + |
| 43 | +Called when processing an invitation. The module must return a `bool` indicating whether |
| 44 | +the inviter can invite the invitee to the given room. Both inviter and invitee are |
| 45 | +represented by their Matrix user ID (e.g. `@alice:example.com`). |
| 46 | + |
| 47 | +### `user_may_send_3pid_invite` |
| 48 | + |
| 49 | +```python |
| 50 | +async def user_may_send_3pid_invite( |
| 51 | + inviter: str, |
| 52 | + medium: str, |
| 53 | + address: str, |
| 54 | + room_id: str, |
| 55 | +) -> bool |
| 56 | +``` |
| 57 | + |
| 58 | +Called when processing an invitation using a third-party identifier (also called a 3PID, |
| 59 | +e.g. an email address or a phone number). The module must return a `bool` indicating |
| 60 | +whether the inviter can invite the invitee to the given room. |
| 61 | + |
| 62 | +The inviter is represented by their Matrix user ID (e.g. `@alice:example.com`), and the |
| 63 | +invitee is represented by its medium (e.g. "email") and its address |
| 64 | +(e.g. `alice@example.com`). See [the Matrix specification](https://matrix.org/docs/spec/appendices#pid-types) |
| 65 | +for more information regarding third-party identifiers. |
| 66 | + |
| 67 | +For example, a call to this callback to send an invitation to the email address |
| 68 | +`alice@example.com` would look like this: |
| 69 | + |
| 70 | +```python |
| 71 | +await user_may_send_3pid_invite( |
| 72 | + "@bob:example.com", # The inviter's user ID |
| 73 | + "email", # The medium of the 3PID to invite |
| 74 | + "[email protected]", # The address of the 3PID to invite |
| 75 | + "!some_room:example.com", # The ID of the room to send the invite into |
| 76 | +) |
| 77 | +``` |
| 78 | + |
| 79 | +**Note**: If the third-party identifier is already associated with a matrix user ID, |
| 80 | +[`user_may_invite`](#user_may_invite) will be used instead. |
| 81 | + |
| 82 | +### `user_may_create_room` |
| 83 | + |
| 84 | +```python |
| 85 | +async def user_may_create_room(user: str) -> bool |
| 86 | +``` |
| 87 | + |
| 88 | +Called when processing a room creation request. The module must return a `bool` indicating |
| 89 | +whether the given user (represented by their Matrix user ID) is allowed to create a room. |
| 90 | + |
| 91 | +### `user_may_create_room_with_invites` |
| 92 | + |
| 93 | +```python |
| 94 | +async def user_may_create_room_with_invites( |
| 95 | + user: str, |
| 96 | + invites: List[str], |
| 97 | + threepid_invites: List[Dict[str, str]], |
| 98 | +) -> bool |
| 99 | +``` |
| 100 | + |
| 101 | +Called when processing a room creation request (right after `user_may_create_room`). |
| 102 | +The module is given the Matrix user ID of the user trying to create a room, as well as a |
| 103 | +list of Matrix users to invite and a list of third-party identifiers (3PID, e.g. email |
| 104 | +addresses) to invite. |
| 105 | + |
| 106 | +An invited Matrix user to invite is represented by their Matrix user IDs, and an invited |
| 107 | +3PIDs is represented by a dict that includes the 3PID medium (e.g. "email") through its |
| 108 | +`medium` key and its address (e.g. "[email protected]") through its `address` key. |
| 109 | + |
| 110 | +See [the Matrix specification](https://matrix.org/docs/spec/appendices#pid-types) for more |
| 111 | +information regarding third-party identifiers. |
| 112 | + |
| 113 | +If no invite and/or 3PID invite were specified in the room creation request, the |
| 114 | +corresponding list(s) will be empty. |
| 115 | + |
| 116 | +**Note**: This callback is not called when a room is cloned (e.g. during a room upgrade) |
| 117 | +since no invites are sent when cloning a room. To cover this case, modules also need to |
| 118 | +implement `user_may_create_room`. |
| 119 | + |
| 120 | +### `user_may_create_room_alias` |
| 121 | + |
| 122 | +```python |
| 123 | +async def user_may_create_room_alias(user: str, room_alias: "synapse.types.RoomAlias") -> bool |
| 124 | +``` |
| 125 | + |
| 126 | +Called when trying to associate an alias with an existing room. The module must return a |
| 127 | +`bool` indicating whether the given user (represented by their Matrix user ID) is allowed |
| 128 | +to set the given alias. |
| 129 | + |
| 130 | +### `user_may_publish_room` |
| 131 | + |
| 132 | +```python |
| 133 | +async def user_may_publish_room(user: str, room_id: str) -> bool |
| 134 | +``` |
| 135 | + |
| 136 | +Called when trying to publish a room to the homeserver's public rooms directory. The |
| 137 | +module must return a `bool` indicating whether the given user (represented by their |
| 138 | +Matrix user ID) is allowed to publish the given room. |
| 139 | + |
| 140 | +### `check_username_for_spam` |
| 141 | + |
| 142 | +```python |
| 143 | +async def check_username_for_spam(user_profile: Dict[str, str]) -> bool |
| 144 | +``` |
| 145 | + |
| 146 | +Called when computing search results in the user directory. The module must return a |
| 147 | +`bool` indicating whether the given user profile can appear in search results. The profile |
| 148 | +is represented as a dictionary with the following keys: |
| 149 | + |
| 150 | +* `user_id`: The Matrix ID for this user. |
| 151 | +* `display_name`: The user's display name. |
| 152 | +* `avatar_url`: The `mxc://` URL to the user's avatar. |
| 153 | + |
| 154 | +The module is given a copy of the original dictionary, so modifying it from within the |
| 155 | +module cannot modify a user's profile when included in user directory search results. |
| 156 | + |
| 157 | +### `check_registration_for_spam` |
| 158 | + |
| 159 | +```python |
| 160 | +async def check_registration_for_spam( |
| 161 | + email_threepid: Optional[dict], |
| 162 | + username: Optional[str], |
| 163 | + request_info: Collection[Tuple[str, str]], |
| 164 | + auth_provider_id: Optional[str] = None, |
| 165 | +) -> "synapse.spam_checker_api.RegistrationBehaviour" |
| 166 | +``` |
| 167 | + |
| 168 | +Called when registering a new user. The module must return a `RegistrationBehaviour` |
| 169 | +indicating whether the registration can go through or must be denied, or whether the user |
| 170 | +may be allowed to register but will be shadow banned. |
| 171 | + |
| 172 | +The arguments passed to this callback are: |
| 173 | + |
| 174 | +* `email_threepid`: The email address used for registering, if any. |
| 175 | +* `username`: The username the user would like to register. Can be `None`, meaning that |
| 176 | + Synapse will generate one later. |
| 177 | +* `request_info`: A collection of tuples, which first item is a user agent, and which |
| 178 | + second item is an IP address. These user agents and IP addresses are the ones that were |
| 179 | + used during the registration process. |
| 180 | +* `auth_provider_id`: The identifier of the SSO authentication provider, if any. |
| 181 | + |
| 182 | +### `check_media_file_for_spam` |
| 183 | + |
| 184 | +```python |
| 185 | +async def check_media_file_for_spam( |
| 186 | + file_wrapper: "synapse.rest.media.v1.media_storage.ReadableFileWrapper", |
| 187 | + file_info: "synapse.rest.media.v1._base.FileInfo", |
| 188 | +) -> bool |
| 189 | +``` |
| 190 | + |
| 191 | +Called when storing a local or remote file. The module must return a boolean indicating |
| 192 | +whether the given file can be stored in the homeserver's media store. |
| 193 | + |
| 194 | +## Example |
| 195 | + |
| 196 | +The example below is a module that implements the spam checker callback |
| 197 | +`check_event_for_spam` to deny any message sent by users whose Matrix user IDs are |
| 198 | +mentioned in a configured list, and registers a web resource to the path |
| 199 | +`/_synapse/client/list_spam_checker/is_evil` that returns a JSON object indicating |
| 200 | +whether the provided user appears in that list. |
| 201 | + |
| 202 | +```python |
| 203 | +import json |
| 204 | +from typing import Union |
| 205 | + |
| 206 | +from twisted.web.resource import Resource |
| 207 | +from twisted.web.server import Request |
| 208 | + |
| 209 | +from synapse.module_api import ModuleApi |
| 210 | + |
| 211 | + |
| 212 | +class IsUserEvilResource(Resource): |
| 213 | + def __init__(self, config): |
| 214 | + super(IsUserEvilResource, self).__init__() |
| 215 | + self.evil_users = config.get("evil_users") or [] |
| 216 | + |
| 217 | + def render_GET(self, request: Request): |
| 218 | + user = request.args.get(b"user")[0].decode() |
| 219 | + request.setHeader(b"Content-Type", b"application/json") |
| 220 | + return json.dumps({"evil": user in self.evil_users}).encode() |
| 221 | + |
| 222 | + |
| 223 | +class ListSpamChecker: |
| 224 | + def __init__(self, config: dict, api: ModuleApi): |
| 225 | + self.api = api |
| 226 | + self.evil_users = config.get("evil_users") or [] |
| 227 | + |
| 228 | + self.api.register_spam_checker_callbacks( |
| 229 | + check_event_for_spam=self.check_event_for_spam, |
| 230 | + ) |
| 231 | + |
| 232 | + self.api.register_web_resource( |
| 233 | + path="/_synapse/client/list_spam_checker/is_evil", |
| 234 | + resource=IsUserEvilResource(config), |
| 235 | + ) |
| 236 | + |
| 237 | + async def check_event_for_spam(self, event: "synapse.events.EventBase") -> Union[bool, str]: |
| 238 | + return event.sender not in self.evil_users |
| 239 | +``` |
0 commit comments