mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-27 22:34:55 +00:00
add before and after filter to redact user method (/_synapse/admin/v1/user/$user_id/redact) (#19802)
Closes #19441 --------- Co-authored-by: Olivier 'reivilibre <oliverw@matrix.org>
This commit is contained in:
co-authored by
Olivier 'reivilibre
parent
acb0ae1a15
commit
2517db46b3
@@ -35,6 +35,7 @@ __pycache__/
|
||||
/media_store/
|
||||
/uploads
|
||||
/homeserver-config-overrides.d
|
||||
tmp/
|
||||
|
||||
# For direnv users
|
||||
/.envrc
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Add before and after time filters to the 'Redact events of a user' Admin API.
|
||||
@@ -1512,24 +1512,28 @@ Returns a `404` HTTP status code if no user was found, with a response body like
|
||||
_Added in Synapse 1.72.0._
|
||||
|
||||
|
||||
## Redact all the events of a user
|
||||
## Redact events of a user
|
||||
|
||||
This endpoint allows an admin to redact the events of a given user. There are no restrictions on
|
||||
redactions for a local user. By default, we puppet the user who sent the message to redact it themselves.
|
||||
Redactions for non-local users are issued using the admin user, and will fail in rooms where the
|
||||
admin user is not admin/does not have the specified power level to issue redactions. An option
|
||||
is provided to override the default and allow the admin to issue the redactions in all cases.
|
||||
is provided to override the default and allow the admin to issue the redactions in all cases.
|
||||
There are optional parameters to filter for events that happened in the given time period.
|
||||
|
||||
The API is
|
||||
```
|
||||
POST /_synapse/admin/v1/user/<user_id>/redact
|
||||
|
||||
{
|
||||
"rooms": ["!roomid1", "!roomid2"]
|
||||
"rooms": ["!roomid1", "!roomid2"],
|
||||
"after_ts": 1779564103728,
|
||||
"before_ts": 1779564103730
|
||||
}
|
||||
```
|
||||
If an empty list is provided as the key for `rooms`, all events in all the rooms the user is member of will be redacted,
|
||||
otherwise all the events in the rooms provided in the request will be redacted.
|
||||
If neither `after_ts` nor `before_ts` is provided, events will be redacted regardless of when they happened. If only one parameter is provided, all events occurring on or before/after given time will be redacted.
|
||||
|
||||
The API starts redaction process running, and returns immediately with a JSON body with
|
||||
a redact id which can be used to query the status of the redaction process:
|
||||
@@ -1557,7 +1561,9 @@ The following JSON body parameters are optional:
|
||||
- `limit` - a limit on the number of the user's events to search for ones that can be redacted (events are redacted newest to oldest) in each room, defaults to 1000 if not provided.
|
||||
- `use_admin` - If set to `true`, the admin user is used to issue the redactions, rather than puppeting the user. Useful
|
||||
when the admin is also the moderator of the rooms that require redactions. Note that the redactions will fail in rooms
|
||||
where the admin does not have the sufficient power level to issue the redactions.
|
||||
where the admin does not have the sufficient power level to issue the redactions.
|
||||
- `after_ts` - Redact only events that were sent at this time or after. Format: milliseconds timestamp. _Added in Synapse 1.157.0._
|
||||
- `before_ts` - Redact only events that were sent at this time or before. Format: milliseconds timestamp. _Added in Synapse 1.157.0._
|
||||
|
||||
_Added in Synapse 1.116.0._
|
||||
|
||||
@@ -1599,5 +1605,3 @@ The following fields are returned in the JSON response body:
|
||||
the corresponding error that caused the redaction to fail
|
||||
|
||||
_Added in Synapse 1.116.0._
|
||||
|
||||
|
||||
|
||||
@@ -363,7 +363,9 @@ class AdminHandler:
|
||||
requester: JsonMapping,
|
||||
use_admin: bool,
|
||||
reason: str | None,
|
||||
limit: int | None,
|
||||
before_ts: int | None = None,
|
||||
after_ts: int | None = None,
|
||||
limit: int | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Start a task redacting the events of the given user in the given rooms
|
||||
@@ -374,6 +376,8 @@ class AdminHandler:
|
||||
requester: the user requesting the events
|
||||
use_admin: whether to use the admin account to issue the redactions
|
||||
reason: reason for requesting the redaction, ie spam, etc
|
||||
before_ts: only redact events that happened before this time
|
||||
after_ts: only redact events that happened after this time
|
||||
limit: limit on the number of events in each room to redact
|
||||
|
||||
Returns:
|
||||
@@ -402,6 +406,8 @@ class AdminHandler:
|
||||
"user_id": user_id,
|
||||
"use_admin": use_admin,
|
||||
"reason": reason,
|
||||
"before_ts": before_ts,
|
||||
"after_ts": after_ts,
|
||||
"limit": limit,
|
||||
},
|
||||
)
|
||||
@@ -417,8 +423,8 @@ class AdminHandler:
|
||||
self, task: ScheduledTask
|
||||
) -> tuple[TaskStatus, Mapping[str, Any] | None, str | None]:
|
||||
"""
|
||||
Task to redact all of a users events in the given rooms, tracking which, if any, events
|
||||
whose redaction failed
|
||||
Task to redact all of a users events in the given rooms in the given time period,
|
||||
tracking which, if any, events whose redaction failed
|
||||
"""
|
||||
|
||||
assert task.params is not None
|
||||
@@ -446,6 +452,8 @@ class AdminHandler:
|
||||
authenticated_entity=admin.user.to_string(),
|
||||
)
|
||||
|
||||
before_ts = task.params.get("before_ts")
|
||||
after_ts = task.params.get("after_ts")
|
||||
reason = task.params.get("reason")
|
||||
limit = task.params.get("limit")
|
||||
assert limit is not None
|
||||
@@ -460,6 +468,8 @@ class AdminHandler:
|
||||
room,
|
||||
limit,
|
||||
["m.room.member", "m.room.message", "m.room.encrypted"],
|
||||
before_ts,
|
||||
after_ts,
|
||||
)
|
||||
if not event_ids:
|
||||
# nothing to redact in this room
|
||||
|
||||
@@ -1495,9 +1495,14 @@ class UserByThreePid(RestServlet):
|
||||
|
||||
class RedactUser(RestServlet):
|
||||
"""
|
||||
Redact all the events of a given user in the given rooms or if empty dict is provided
|
||||
then all events in all rooms user is member of. Kicks off a background process and
|
||||
returns an id that can be used to check on the progress of the redaction progress.
|
||||
Redact all the events of a given user in the given rooms in the given time period.
|
||||
Kicks off a background process and returns an id that can be used to check on the
|
||||
progress of the redaction progress.
|
||||
If empty rooms dict is provided then all events in all rooms user is member of will
|
||||
be affected.
|
||||
Parameters before_ts and after_ts are millisecond timestamps.
|
||||
If both are omitted, then messages will be redacted regardless the time they were sent.
|
||||
If only one parameter is sent, then all messages before or after given time will be redacted.
|
||||
"""
|
||||
|
||||
PATTERNS = admin_patterns("/user/(?P<user_id>[^/]*)/redact")
|
||||
@@ -1512,6 +1517,8 @@ class RedactUser(RestServlet):
|
||||
reason: StrictStr | None = None
|
||||
limit: StrictInt | None = None
|
||||
use_admin: StrictBool | None = None
|
||||
before_ts: StrictInt | None = None
|
||||
after_ts: StrictInt | None = None
|
||||
|
||||
async def on_POST(
|
||||
self, request: SynapseRequest, user_id: str
|
||||
@@ -1543,8 +1550,18 @@ class RedactUser(RestServlet):
|
||||
if not use_admin:
|
||||
use_admin = False
|
||||
|
||||
before_ts = body.before_ts
|
||||
after_ts = body.after_ts
|
||||
|
||||
redact_id = await self.admin_handler.start_redact_events(
|
||||
user_id, rooms, requester.serialize(), use_admin, body.reason, limit
|
||||
user_id,
|
||||
rooms,
|
||||
requester.serialize(),
|
||||
use_admin,
|
||||
body.reason,
|
||||
before_ts,
|
||||
after_ts,
|
||||
limit,
|
||||
)
|
||||
|
||||
return HTTPStatus.OK, {"redact_id": redact_id}
|
||||
|
||||
@@ -2715,15 +2715,23 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
self.invalidate_get_event_cache_after_txn(txn, event_id)
|
||||
|
||||
async def get_events_sent_by_user_in_room(
|
||||
self, user_id: str, room_id: str, limit: int, filter: list[str] | None = None
|
||||
self,
|
||||
user_id: str,
|
||||
room_id: str,
|
||||
limit: int,
|
||||
filter: list[str] | None = None,
|
||||
before_ts: int | None = None,
|
||||
after_ts: int | None = None,
|
||||
) -> list[str] | None:
|
||||
"""
|
||||
Get a list of event ids of events sent by the user in the specified room
|
||||
Get a list of event ids of events sent by the user in the specified room in the specified time period
|
||||
|
||||
Args:
|
||||
user_id: user ID to search against
|
||||
room_id: room ID of the room to search for events in
|
||||
filter: type of events to filter for
|
||||
before_ts: filter for events that happened before this time (optional)
|
||||
after_ts: filter for events that happened after this time (optional)
|
||||
limit: maximum number of event ids to return
|
||||
"""
|
||||
|
||||
@@ -2734,16 +2742,32 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
filter: list[str] | None,
|
||||
batch_size: int,
|
||||
offset: int,
|
||||
before_ts: int | None = None,
|
||||
after_ts: int | None = None,
|
||||
) -> tuple[list[str] | None, int]:
|
||||
clause = ""
|
||||
if filter:
|
||||
base_clause, args = make_in_list_sql_clause(
|
||||
txn.database_engine, "type", filter
|
||||
)
|
||||
clause = f"AND {base_clause}"
|
||||
parameters = (user_id, room_id, *args, batch_size, offset)
|
||||
parameters = (user_id, room_id, *args)
|
||||
else:
|
||||
clause = ""
|
||||
parameters = (user_id, room_id, batch_size, offset)
|
||||
parameters = (user_id, room_id)
|
||||
|
||||
if before_ts:
|
||||
if clause:
|
||||
clause += " AND "
|
||||
clause += "origin_server_ts <= ?"
|
||||
parameters += (before_ts,)
|
||||
|
||||
if after_ts:
|
||||
if clause:
|
||||
clause += " AND "
|
||||
clause += "origin_server_ts >= ?"
|
||||
parameters += (after_ts,)
|
||||
|
||||
parameters += (batch_size, offset)
|
||||
|
||||
sql = f"""
|
||||
SELECT event_id FROM events
|
||||
@@ -2777,6 +2801,8 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
filter,
|
||||
batch_size,
|
||||
offset,
|
||||
before_ts,
|
||||
after_ts,
|
||||
)
|
||||
if res:
|
||||
selected_ids = selected_ids + res
|
||||
|
||||
@@ -5334,6 +5334,69 @@ class UserRedactionTestCase(unittest.HomeserverTestCase):
|
||||
matched.append(event_id)
|
||||
self.assertEqual(len(matched), len(originals))
|
||||
|
||||
def test_redact_messages_all_rooms_within_timeframe(self) -> None:
|
||||
"""
|
||||
Test that request to redact user's events in all rooms within a specific timeframe is successful
|
||||
"""
|
||||
# join rooms, send some messages
|
||||
|
||||
# (event_id, timestamp) pairs
|
||||
all_message_ids: list[tuple[str, int]] = []
|
||||
for rm in [self.rm1, self.rm2, self.rm3]:
|
||||
self.helper.join(rm, self.bad_user, tok=self.bad_user_tok)
|
||||
|
||||
for i in range(4):
|
||||
for rm in [self.rm1, self.rm2, self.rm3]:
|
||||
event = {"body": f"hello{i}", "msgtype": "m.text"}
|
||||
res = self.helper.send_event(
|
||||
rm, "m.room.message", event, tok=self.bad_user_tok, expect_code=200
|
||||
)
|
||||
event_id = res["event_id"]
|
||||
event_ts = self.get_success(
|
||||
self.store.get_event(event_id)
|
||||
).origin_server_ts
|
||||
all_message_ids.append((event_id, event_ts))
|
||||
|
||||
expected_saved_message_ids = {
|
||||
event_id for event_id, _ in all_message_ids[:5] + all_message_ids[10:]
|
||||
}
|
||||
expected_redacted_message_ids = {
|
||||
event_id for event_id, _ in all_message_ids[5:10]
|
||||
}
|
||||
|
||||
# Redact events 5 up to and including 9
|
||||
_after_event_id, after_ts = all_message_ids[5]
|
||||
_before_event_id, before_ts = all_message_ids[9]
|
||||
|
||||
# redact events in all rooms within specific timeframe
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
f"/_synapse/admin/v1/user/{self.bad_user}/redact",
|
||||
content={"rooms": [], "after_ts": after_ts, "before_ts": before_ts},
|
||||
access_token=self.admin_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 200)
|
||||
|
||||
# Get the set of all redacted event IDs
|
||||
all_redacted_event_ids: set[str] = set()
|
||||
for rm in [self.rm1, self.rm2, self.rm3]:
|
||||
filter = json.dumps({"types": [EventTypes.Redaction]})
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
f"rooms/{rm}/messages?filter={filter}&limit=50",
|
||||
access_token=self.admin_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 200)
|
||||
|
||||
# Get the IDs of all redacted events
|
||||
for event in channel.json_body["chunk"]:
|
||||
assert event["type"] == EventTypes.Redaction
|
||||
all_redacted_event_ids.add(event["redacts"])
|
||||
|
||||
# check that only expected messages were redacted
|
||||
self.assertSetEqual(expected_redacted_message_ids, all_redacted_event_ids)
|
||||
self.assertSetEqual(expected_saved_message_ids & all_redacted_event_ids, set())
|
||||
|
||||
def test_redact_messages_specific_rooms(self) -> None:
|
||||
"""
|
||||
Test that request to redact events in specified rooms user is member of is successful
|
||||
|
||||
Reference in New Issue
Block a user