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

Commit cef83c7

Browse files
authored
Merge pull request #108 from matrix-org/babolivier/update_spam_checker
Bring spamchecker to parity with mainline
2 parents dd19126 + d859a6a commit cef83c7

File tree

15 files changed

+1045
-290
lines changed

15 files changed

+1045
-290
lines changed

changelog.d/10894.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a `user_may_send_3pid_invite` spam checker callback for modules to allow or deny 3PID invites.

changelog.d/10898.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a `user_may_create_room_with_invites` spam checker callback to allow modules to allow or deny a room creation request based on the invites and/or 3PID invites it includes.

changelog.d/10910.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a spam checker callback to allow or deny room joins.

changelog.d/11204.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a module API method to retrieve the current state of a room.
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
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

Comments
 (0)