Compare commits

...

16 Commits

Author SHA1 Message Date
Shay
007c0ea42e
Merge af6ca26af04dbad5ad2a3813e96a64d7834fa0fb into c17fd947f30004745b3191ff0edd76c3cd87e352 2025-07-02 00:11:44 +02:00
H. Shay
af6ca26af0 fix other column type 2025-06-27 11:40:29 -07:00
H. Shay
17ce194081 fix column type 2025-06-27 11:30:04 -07:00
H. Shay
8d7eddd6f9 update code to reflect new table architecture 2025-06-27 11:22:08 -07:00
H. Shay
65d7b41b1c change table architecture 2025-06-27 11:21:30 -07:00
H. Shay
4323879d23 requested changes 2025-06-23 13:52:23 -07:00
Andrew Morgan
80e0979653
Merge branch 'develop' into shay/redact_on_ban 2025-06-20 14:18:13 +01:00
H. Shay
5a53b9337f requested changes 2025-06-16 16:10:29 -07:00
H. Shay
656b648420 remove debugging artifact 2025-06-12 16:14:46 -07:00
H. Shay
5ad9e66fdd ensure redact flag is respected when using /kick and /ban 2025-06-12 15:25:55 -07:00
H. Shay
e4a1014209 lint 2025-06-12 14:36:23 -07:00
H. Shay
b6aa6c0ccf newsfragment + run indexes in background 2025-06-12 14:34:25 -07:00
H. Shay
e8d328f3bb add indexes in background 2025-06-12 14:10:57 -07:00
H. Shay
8c606cd397 fix msc number typo 2025-06-12 14:10:33 -07:00
H. Shay
ce555e7cf7 tests 2025-06-11 17:56:15 -07:00
H. Shay
0766119fc9 add support for MSC4293 2025-06-11 17:55:15 -07:00
7 changed files with 1172 additions and 14 deletions

View File

@ -0,0 +1 @@
Add support for [MSC4293](https://github.com/matrix-org/matrix-spec-proposals/pull/4293) - Redact on Kick/Ban.

View File

@ -575,3 +575,6 @@ class ExperimentalConfig(Config):
# MSC4155: Invite filtering
self.msc4155_enabled: bool = experimental.get("msc4155_enabled", False)
# MSC4293: Redact on Kick/Ban
self.msc4293_enabled: bool = experimental.get("msc4293_enabled", False)

View File

@ -1088,6 +1088,7 @@ class RoomMembershipRestServlet(TransactionRestServlet):
super().__init__(hs)
self.room_member_handler = hs.get_room_member_handler()
self.auth = hs.get_auth()
self.config = hs.config
def register(self, http_server: HttpServer) -> None:
# /rooms/$roomid/[join|invite|leave|ban|unban|kick]
@ -1111,12 +1112,12 @@ class RoomMembershipRestServlet(TransactionRestServlet):
}:
raise AuthError(403, "Guest access not allowed")
content = parse_json_object_from_request(request, allow_empty_body=True)
request_body = parse_json_object_from_request(request, allow_empty_body=True)
if membership_action == "invite" and all(
key in content for key in ("medium", "address")
key in request_body for key in ("medium", "address")
):
if not all(key in content for key in ("id_server", "id_access_token")):
if not all(key in request_body for key in ("id_server", "id_access_token")):
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"`id_server` and `id_access_token` are required when doing 3pid invite",
@ -1127,12 +1128,12 @@ class RoomMembershipRestServlet(TransactionRestServlet):
await self.room_member_handler.do_3pid_invite(
room_id,
requester.user,
content["medium"],
content["address"],
content["id_server"],
request_body["medium"],
request_body["address"],
request_body["id_server"],
requester,
txn_id,
content["id_access_token"],
request_body["id_access_token"],
)
except ShadowBanError:
# Pretend the request succeeded.
@ -1141,12 +1142,19 @@ class RoomMembershipRestServlet(TransactionRestServlet):
target = requester.user
if membership_action in ["invite", "ban", "unban", "kick"]:
assert_params_in_dict(content, ["user_id"])
target = UserID.from_string(content["user_id"])
assert_params_in_dict(request_body, ["user_id"])
target = UserID.from_string(request_body["user_id"])
event_content = None
if "reason" in content:
event_content = {"reason": content["reason"]}
if "reason" in request_body:
event_content = {"reason": request_body["reason"]}
if self.config.experimental.msc4293_enabled:
if "org.matrix.msc4293.redact_events" in request_body:
if event_content is None:
event_content = {}
event_content["org.matrix.msc4293.redact_events"] = request_body[
"org.matrix.msc4293.redact_events"
]
try:
await self.room_member_handler.update_membership(
@ -1155,7 +1163,7 @@ class RoomMembershipRestServlet(TransactionRestServlet):
room_id=room_id,
action=membership_action,
txn_id=txn_id,
third_party_signed=content.get("third_party_signed", None),
third_party_signed=request_body.get("third_party_signed", None),
content=event_content,
)
except ShadowBanError:

View File

@ -108,6 +108,9 @@ SLIDING_SYNC_RELEVANT_STATE_SET = (
(EventTypes.Tombstone, ""),
)
# An arbitrarily large number
MAX_EVENTS = 1000000
@attr.s(slots=True, auto_attribs=True)
class DeltaState:
@ -376,6 +379,109 @@ class PersistEventsStore:
event_counter.labels(event.type, origin_type, origin_entity).inc()
if (
not self.hs.config.experimental.msc4293_enabled
or event.type != EventTypes.Member
or event.state_key is None
):
continue
# check if this is an unban/join that will undo a ban/kick redaction for
# user in room
if event.membership in [Membership.LEAVE, Membership.JOIN]:
if event.membership == Membership.LEAVE:
# self-leave, ignore
if event.sender == event.state_key:
continue
# check to see if there is an existing ban/leave causing redactions for
# this user/room combination
res = await self.db_pool.simple_select_list(
"room_ban_redactions",
{"room_id": event.room_id, "user_id": event.state_key},
["room_id", "user_id"],
)
if res:
# if so, update the entry with the stream ordering when the redactions should
# stop
await self.db_pool.simple_update(
"room_ban_redactions",
{"room_id": event.room_id, "user_id": event.state_key},
{
"redact_end_ordering": event.internal_metadata.stream_ordering
},
desc="room_ban_redactions update redact_end_ordering",
)
# check for msc4293 redact_events flag and apply if found
if event.membership not in [Membership.LEAVE, Membership.BAN]:
continue
redact = event.content.get("org.matrix.msc4293.redact_events", False)
if not redact or not isinstance(redact, bool):
continue
# self-bans currently are not authorized so we don't check for that
# case
if (
event.membership == Membership.LEAVE
and event.sender == event.state_key
):
continue
# check that sender can redact
state_filter = StateFilter.from_types([(EventTypes.PowerLevels, "")])
state = await self.store.get_partial_filtered_current_state_ids(
event.room_id, state_filter
)
pl_id = state[(EventTypes.PowerLevels, "")]
pl_event = await self.store.get_event(pl_id)
if pl_event:
sender_level = pl_event.content.get("users", {}).get(event.sender)
if sender_level is None:
sender_level = pl_event.content.get("users_default", 0)
redact_level = pl_event.content.get("redact")
if not redact_level:
redact_level = pl_event.content.get("events_default", 0)
room_redaction_level = pl_event.content.get("events", {}).get(
"m.room.redaction"
)
if room_redaction_level:
if sender_level < room_redaction_level:
continue
if sender_level >= redact_level:
await self.db_pool.simple_upsert(
"room_ban_redactions",
{"room_id": event.room_id, "user_id": event.state_key},
{
"redacting_event_id": event.event_id,
"redact_end_ordering": None,
},
{
"room_id": event.room_id,
"user_id": event.state_key,
"redacting_event_id": event.event_id,
"redact_end_ordering": None,
},
)
# normally the cache entry for a redacted event would be invalidated
# by an arriving redaction event, but since we are not creating redaction
# events we invalidate manually
ids_to_redact = (
await self.store.get_events_sent_by_user_in_room(
event.state_key, event.room_id, limit=MAX_EVENTS
)
)
if not ids_to_redact:
continue
for id in ids_to_redact:
await self.db_pool.runInteraction(
"invalidate cache",
self.store.invalidate_get_event_cache_after_txn,
id,
)
if new_forward_extremities:
self.store.get_latest_event_ids_in_room.prefill(
(room_id,), frozenset(new_forward_extremities)

View File

@ -17,7 +17,7 @@
# [This file includes modifications made by New Vector Limited]
#
#
import json
import logging
import threading
import weakref
@ -1571,6 +1571,42 @@ class EventsWorkerStore(SQLBaseStore):
if d:
d.redactions.append(redacter)
# check for MSC4932 redactions
to_check = []
events: List[_EventRow] = []
for e in evs:
event = event_dict.get(e)
if not event:
continue
events.append(event)
event_json = json.loads(event.json)
room_id = event_json.get("room_id")
user_id = event_json.get("sender")
to_check.append((room_id, user_id))
# likely that some of these events may be for the same room/user combo, in
# which case we don't need to do redundant queries
to_check_set = set(to_check)
for room_and_user in to_check_set:
room_redactions_sql = "SELECT redacting_event_id, redact_end_ordering FROM room_ban_redactions WHERE room_id = ? and user_id = ?"
txn.execute(room_redactions_sql, room_and_user)
res = txn.fetchone()
# we have a redaction for a room, user_id combo - apply it to matching events
if not res:
continue
for e_row in events:
e_json = json.loads(e_row.json)
room_id = e_json.get("room_id")
user_id = e_json.get("sender")
if room_and_user != (room_id, user_id):
continue
redacting_event_id, redact_end_ordering = res
if redact_end_ordering:
if e_row.stream_ordering < redact_end_ordering:
e_row.redactions.append(redacting_event_id)
else:
e_row.redactions.append(redacting_event_id)
return event_dict
def _maybe_redact_event_row(

View File

@ -0,0 +1,21 @@
--
-- This file is licensed under the Affero General Public License (AGPL) version 3.
--
-- Copyright (C) 2025 New Vector, Ltd
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
--
-- See the GNU Affero General Public License for more details:
-- <https://www.gnu.org/licenses/agpl-3.0.html>.
CREATE TABLE room_ban_redactions(
room_id text NOT NULL,
user_id text NOT NULL,
redacting_event_id text NOT NULL,
redact_end_ordering bigint DEFAULT NULL, -- stream ordering after which redactions are not applied
CONSTRAINT room_ban_redaction_uniqueness UNIQUE (room_id, user_id)
);

View File

@ -43,8 +43,9 @@ from synapse.api.constants import (
RoomTypes,
)
from synapse.api.errors import Codes, HttpResponseException
from synapse.api.room_versions import RoomVersions
from synapse.appservice import ApplicationService
from synapse.events import EventBase
from synapse.events import EventBase, make_event_from_dict
from synapse.events.snapshot import EventContext
from synapse.rest import admin
from synapse.rest.client import (
@ -4401,3 +4402,985 @@ class RoomParticipantTestCase(unittest.HomeserverTestCase):
self.store.get_room_participation(self.user2, self.room1)
)
self.assertFalse(participant)
class MSC4293RedactOnBanKickTestCase(unittest.FederatingHomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
login.register_servlets,
admin.register_servlets,
]
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
super().prepare(reactor, clock, hs)
self.creator = self.register_user("creator", "test")
self.creator_tok = self.login("creator", "test")
self.bad_user_id = self.register_user("bad", "test")
self.bad_tok = self.login("bad", "test")
self.room_id = self.helper.create_room_as(self.creator, tok=self.creator_tok)
self.store = hs.get_datastores().main
self._storage_controllers = hs.get_storage_controllers()
self.federation_event_handler = self.hs.get_federation_event_handler()
self.hs.config.experimental.msc4293_enabled = True
def _check_redactions(
self,
original_events: List[EventBase],
pulled_events: List[JsonDict],
expect_redaction: bool,
reason: Optional[str] = None,
) -> None:
"""
Checks a set of original events against a second set of the same events, pulled
from the /messages api. If expect_redaction is true, we expect that the second
set of events will be redacted, and the test will fail if that is not the case.
Otherwise, verifies that the events have not been redacted and fails if not.
Args:
original_events: A list of the original events sent
pulled_events: A list of the same events as the orignal events, fetched
over the /messages api
expect_redaction: Whether or not the pulled_events should be redacted
reason: If the events are expected to be redacted, the expected reason
for the redaction
"""
if expect_redaction:
redacted_count = 0
for pulled_event in pulled_events:
for old_event in original_events:
if pulled_event["event_id"] != old_event.event_id:
continue
# we have a matching event, check that it is redacted
event_content = pulled_event["content"]
if event_content:
self.fail(f"Expected event {pulled_event} to be redacted")
redacting_event = pulled_event.get("redacted_because")
if not redacting_event:
self.fail(
f"Expected event {pulled_event} to have a redacting event."
)
# check that the redacting event records the expected reason, and the
# redact_events flag
content = redacting_event["content"]
self.assertEqual(content["reason"], reason)
self.assertEqual(content["org.matrix.msc4293.redact_events"], True)
redacted_count += 1
# all provided events should be redacted
self.assertEqual(len(original_events), redacted_count)
else:
unredacted_events = 0
for pulled_event in pulled_events:
for old_event in original_events:
if pulled_event["event_id"] != old_event.event_id:
continue
# we have a matching event, make sure it is not redacted
redacted_because = pulled_event.get("redacted_because")
if redacted_because:
self.fail("Event should not have been redacted")
self.assertEqual(old_event.content, pulled_event["content"])
unredacted_events += 1
# all provided events should not have been redacted
self.assertEqual(unredacted_events, len(original_events))
def test_banning_local_member_with_flag_redacts_their_events(self) -> None:
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user send some messages
originals = []
for i in range(5):
event = {"body": f"bothersome noise {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
originals.append(res["event_id"])
# grab original events for comparison
original_events = [self.get_success(self.store.get_event(x)) for x in originals]
# creator bans user with redaction flag set
content = {
"reason": "flooding",
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.creator,
self.bad_user_id,
"ban",
content,
self.creator_tok,
)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
original_events,
channel.json_body["chunk"],
expect_redaction=True,
reason="flooding",
)
def test_banning_remote_member_with_flag_redacts_their_events(self) -> None:
bad_user = "@remote_bad_user:" + self.OTHER_SERVER_NAME
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{self.room_id}/{bad_user}?ver=10",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
RoomVersions.V10,
)
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{self.room_id}/x",
content=join_event_dict,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
# the room should show that the bad user is a member
r = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
self.assertEqual(r[("m.room.member", bad_user)].membership, "join")
auth_ids = [
r[("m.room.create", "")].event_id,
r[("m.room.power_levels", "")].event_id,
r[("m.room.member", "@remote_bad_user:other.example.com")].event_id,
]
original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"remote bummer{i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
original_messages.append(remote_message)
# creator bans bad user with redaction flag set
content = {
"reason": "bummer messages",
"org.matrix.msc4293.redact_events": True,
}
res = self.helper.change_membership(
self.room_id, self.creator, bad_user, "ban", content, self.creator_tok
)
ban_event_id = res["event_id"]
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
original_messages,
channel.json_body["chunk"],
expect_redaction=True,
reason="bummer messages",
)
# any future messages that are soft-failed are also redacted - send messages referencing
# dag before ban, they should be soft-failed but also redacted
new_original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"soft-fail remote bummer{i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
new_original_messages.append(remote_message)
# pull them from the db to check because they should be soft-failed and thus not available over
# cs-api
for message in new_original_messages:
original = self.get_success(self.store.get_event(message.event_id))
if not original:
self.fail("Expected to find remote message in DB")
redacted_because = original.unsigned.get("redacted_because")
if not redacted_because:
self.fail("Did not find redacted_because field")
self.assertEqual(redacted_because.event_id, ban_event_id)
def test_unbanning_remote_user_stops_redaction_action(self) -> None:
bad_user = "@remote_bad_user:" + self.OTHER_SERVER_NAME
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{self.room_id}/{bad_user}?ver=10",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
RoomVersions.V10,
)
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{self.room_id}/x",
content=join_event_dict,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
# the room should show that the bad user is a member
r = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
self.assertEqual(r[("m.room.member", bad_user)].membership, "join")
auth_ids = [
r[("m.room.create", "")].event_id,
r[("m.room.power_levels", "")].event_id,
r[("m.room.member", "@remote_bad_user:other.example.com")].event_id,
]
original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"annoying messages {i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
original_messages.append(remote_message)
# creator bans bad user with redaction flag set
content = {
"reason": "this dude sucks",
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id, self.creator, bad_user, "ban", content, self.creator_tok
)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
original_messages,
channel.json_body["chunk"],
True,
reason="this dude sucks",
)
# unban user
self.helper.change_membership(
self.room_id, self.creator, bad_user, "unban", {}, self.creator_tok
)
# user should be able to join again
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{self.room_id}/{bad_user}?ver=10",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
RoomVersions.V10,
)
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{self.room_id}/x",
content=join_event_dict,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
# the room should show that the bad user is a member again
new_state = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
self.assertEqual(new_state[("m.room.member", bad_user)].membership, "join")
new_state = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
auth_ids = [
new_state[("m.room.create", "")].event_id,
new_state[("m.room.power_levels", "")].event_id,
new_state[("m.room.member", "@remote_bad_user:other.example.com")].event_id,
]
# messages after unban and join proceed unredacted
new_original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"no longer a bummer {i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
new_original_messages.append(remote_message)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(new_original_messages, channel.json_body["chunk"], False)
def test_redaction_flag_ignored_for_user_if_banner_lacks_redaction_power(
self,
) -> None:
# change power levels so creator can ban but not redact
self.helper.send_state(
self.room_id,
"m.room.power_levels",
{"events_default": 0, "redact": 100, "users": {self.creator: 75}},
tok=self.creator_tok,
)
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user send some messages
original_ids = []
for i in range(15):
event = {"body": f"being a menace {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
original_ids.append(res["event_id"])
# grab original events before ban
originals = [self.get_success(self.store.get_event(x)) for x in original_ids]
# creator bans bad user with redaction flag
content = {
"reason": "flooding",
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.creator,
self.bad_user_id,
"ban",
content,
self.creator_tok,
)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
# messages are not redacted
self._check_redactions(originals, channel.json_body["chunk"], False)
def test_kicking_local_member_with_flag_redacts_their_events(self) -> None:
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user send some messages
originals = []
for i in range(5):
event = {"body": f"bothersome noise {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
originals.append(res["event_id"])
# grab original events for comparison
original_events = [self.get_success(self.store.get_event(x)) for x in originals]
# creator kicks user with redaction flag set
content = {
"reason": "flooding",
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.creator,
self.bad_user_id,
"kick",
content,
self.creator_tok,
)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
original_events,
channel.json_body["chunk"],
expect_redaction=True,
reason="flooding",
)
def test_kicking_remote_member_with_flag_redacts_their_events(self) -> None:
bad_user = "@remote_bad_user:" + self.OTHER_SERVER_NAME
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{self.room_id}/{bad_user}?ver=10",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
RoomVersions.V10,
)
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{self.room_id}/x",
content=join_event_dict,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
# the room should show that the bad user is a member
r = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
self.assertEqual(r[("m.room.member", bad_user)].membership, "join")
auth_ids = [
r[("m.room.create", "")].event_id,
r[("m.room.power_levels", "")].event_id,
r[("m.room.member", "@remote_bad_user:other.example.com")].event_id,
]
original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"remote bummer{i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
original_messages.append(remote_message)
# creator kicks bad user with redaction flag set
content = {
"reason": "bummer messages",
"org.matrix.msc4293.redact_events": True,
}
res = self.helper.change_membership(
self.room_id, self.creator, bad_user, "kick", content, self.creator_tok
)
ban_event_id = res["event_id"]
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
original_messages,
channel.json_body["chunk"],
expect_redaction=True,
reason="bummer messages",
)
# any future messages that are soft-failed are also redacted - send messages referencing
# dag before ban, they should be soft-failed but also redacted
new_original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"soft-fail remote bummer{i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
new_original_messages.append(remote_message)
# pull them from the db to check because they should be soft-failed and thus not available over
# cs-api
for message in new_original_messages:
original = self.get_success(self.store.get_event(message.event_id))
if not original:
self.fail("Expected to find remote message in DB")
self.assertEqual(original.unsigned["redacted_by"], ban_event_id)
def test_rejoining_kicked_remote_user_stops_redaction_action(self) -> None:
bad_user = "@remote_bad_user:" + self.OTHER_SERVER_NAME
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{self.room_id}/{bad_user}?ver=10",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
RoomVersions.V10,
)
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{self.room_id}/x",
content=join_event_dict,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
# the room should show that the bad user is a member
r = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
self.assertEqual(r[("m.room.member", bad_user)].membership, "join")
auth_ids = [
r[("m.room.create", "")].event_id,
r[("m.room.power_levels", "")].event_id,
r[("m.room.member", "@remote_bad_user:other.example.com")].event_id,
]
original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"annoying messages {i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
original_messages.append(remote_message)
# creator kicks bad user with redaction flag set
content = {
"reason": "this dude sucks",
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id, self.creator, bad_user, "kick", content, self.creator_tok
)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
original_messages,
channel.json_body["chunk"],
True,
reason="this dude sucks",
)
# user re-joins after kick
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{self.room_id}/{bad_user}?ver=10",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
RoomVersions.V10,
)
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{self.room_id}/x",
content=join_event_dict,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
# the room should show that the bad user is a member again
new_state = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
self.assertEqual(new_state[("m.room.member", bad_user)].membership, "join")
new_state = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
auth_ids = [
new_state[("m.room.create", "")].event_id,
new_state[("m.room.power_levels", "")].event_id,
new_state[("m.room.member", "@remote_bad_user:other.example.com")].event_id,
]
# messages after kick and re-join proceed unredacted
new_original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"no longer a bummer {i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
new_original_messages.append(remote_message)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(new_original_messages, channel.json_body["chunk"], False)
def test_redaction_flag_ignored_for_user_if_kicker_lacks_redaction_power(
self,
) -> None:
# change power levels so creator can kick but not redact
self.helper.send_state(
self.room_id,
"m.room.power_levels",
{"events_default": 0, "redact": 100, "users": {self.creator: 75}},
tok=self.creator_tok,
)
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user send some messages
original_ids = []
for i in range(15):
event = {"body": f"being a menace {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
original_ids.append(res["event_id"])
# grab original events before ban
originals = [self.get_success(self.store.get_event(x)) for x in original_ids]
# creator kicks bad user with redaction flag
content = {
"reason": "flooding",
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.creator,
self.bad_user_id,
"kick",
content,
self.creator_tok,
)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
# messages are not redacted
self._check_redactions(originals, channel.json_body["chunk"], False)
def test_MSC4293_flag_ignored_in_other_membership_events(self) -> None:
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user send some messages
original_ids = []
for i in range(15):
event = {"body": f"being a menace {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
original_ids.append(res["event_id"])
# grab original events before ban
originals = [self.get_success(self.store.get_event(x)) for x in original_ids]
# bad user leaves on their own with flag
content = {
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.bad_user_id,
self.bad_user_id,
"leave",
content,
self.bad_tok,
)
# their messages are not redacted
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(originals, channel.json_body["chunk"], False)
# bad user is invited with flag in invite event
content = {
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.creator,
self.bad_user_id,
"invite",
content,
self.creator_tok,
)
# their messages are still not redacted
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(originals, channel.json_body["chunk"], False)
# bad user joins with flag in invite event
content = {
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.bad_user_id,
self.bad_user_id,
"join",
content,
self.bad_tok,
)
# and still their messages are not redacted
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(originals, channel.json_body["chunk"], False)
def test_MSC4293_redaction_applied_via_kick_api(self) -> None:
"""
Test that MSC4239 field passed through and applied when using /kick
"""
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user send some messages
original_ids = []
for i in range(15):
event = {"body": f"being a menace {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
original_ids.append(res["event_id"])
# grab original events before kick
originals = [self.get_success(self.store.get_event(x)) for x in original_ids]
channel = self.make_request(
"POST",
f"/_matrix/client/v3/rooms/{self.room_id}/kick",
access_token=self.creator_tok,
content={
"reason": "being annoying",
"org.matrix.msc4293.redact_events": True,
"user_id": self.bad_user_id,
},
shorthand=False,
)
self.assertEqual(channel.code, 200)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
originals,
channel.json_body["chunk"],
expect_redaction=True,
reason="being annoying",
)
def test_MSC4293_redaction_applied_via_ban_api(self) -> None:
"""
Test that MSC4239 field passed through and applied when using /ban
"""
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user send some messages
original_ids = []
for i in range(15):
event = {"body": f"being a menace {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
original_ids.append(res["event_id"])
# grab original events before ban
originals = [self.get_success(self.store.get_event(x)) for x in original_ids]
channel = self.make_request(
"POST",
f"/_matrix/client/v3/rooms/{self.room_id}/ban",
access_token=self.creator_tok,
content={
"reason": "being disruptive",
"org.matrix.msc4293.redact_events": True,
"user_id": self.bad_user_id,
},
shorthand=False,
)
self.assertEqual(channel.code, 200)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
originals,
channel.json_body["chunk"],
expect_redaction=True,
reason="being disruptive",
)