diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 5321864d51..9bfd25c86e 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -19,7 +19,9 @@ from twisted.internet import defer from synapse.api.constants import Membership, JoinRules from synapse.api.errors import AuthError, StoreError, Codes, SynapseError -from synapse.api.events.room import RoomMemberEvent, RoomPowerLevelsEvent +from synapse.api.events.room import ( + RoomMemberEvent, RoomPowerLevelsEvent, RoomRedactionEvent, +) from synapse.util.logutils import log_function import logging @@ -70,6 +72,9 @@ class Auth(object): if event.type == RoomPowerLevelsEvent.TYPE: yield self._check_power_levels(event) + if event.type == RoomRedactionEvent.TYPE: + yield self._check_redaction(event) + defer.returnValue(True) else: raise AuthError(500, "Unknown event: %s" % event) @@ -170,7 +175,7 @@ class Auth(object): event.room_id, event.user_id, ) - _, kick_level = yield self.store.get_ops_levels(event.room_id) + _, kick_level, _ = yield self.store.get_ops_levels(event.room_id) if kick_level: kick_level = int(kick_level) @@ -187,7 +192,7 @@ class Auth(object): event.user_id, ) - ban_level, _ = yield self.store.get_ops_levels(event.room_id) + ban_level, _, _ = yield self.store.get_ops_levels(event.room_id) if ban_level: ban_level = int(ban_level) @@ -321,6 +326,29 @@ class Auth(object): "You don't have permission to change that state" ) + @defer.inlineCallbacks + def _check_redaction(self, event): + user_level = yield self.store.get_power_level( + event.room_id, + event.user_id, + ) + + if user_level: + user_level = int(user_level) + else: + user_level = 0 + + _, _, redact_level = yield self.store.get_ops_levels(event.room_id) + + if not redact_level: + redact_level = 50 + + if user_level < redact_level: + raise AuthError( + 403, + "You don't have permission to redact events" + ) + @defer.inlineCallbacks def _check_power_levels(self, event): for k, v in event.content.items(): diff --git a/synapse/api/events/__init__.py b/synapse/api/events/__init__.py index 0cee196851..f66fea2904 100644 --- a/synapse/api/events/__init__.py +++ b/synapse/api/events/__init__.py @@ -22,7 +22,8 @@ def serialize_event(hs, e): if not isinstance(e, SynapseEvent): return e - d = e.get_dict() + # Should this strip out None's? + d = {k: v for k, v in e.get_dict().items()} if "age_ts" in d: d["age"] = int(hs.get_clock().time_msec()) - d["age_ts"] del d["age_ts"] @@ -58,17 +59,19 @@ class SynapseEvent(JsonEncodedObject): "required_power_level", "age_ts", "prev_content", + "prev_state", + "redacted_because", ] internal_keys = [ "is_state", "prev_events", - "prev_state", "depth", "destinations", "origin", "outlier", "power_level", + "redacted", ] required_keys = [ diff --git a/synapse/api/events/factory.py b/synapse/api/events/factory.py index d3d96d73eb..0d94850cec 100644 --- a/synapse/api/events/factory.py +++ b/synapse/api/events/factory.py @@ -17,7 +17,8 @@ from synapse.api.events.room import ( RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent, InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent, RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomOpsPowerLevelsEvent, - RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent + RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent, + RoomRedactionEvent, ) from synapse.util.stringutils import random_string @@ -39,6 +40,7 @@ class EventFactory(object): RoomAddStateLevelEvent, RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent, + RoomRedactionEvent, ] def __init__(self, hs): diff --git a/synapse/api/events/room.py b/synapse/api/events/room.py index 3a4dbc58ce..cd936074fc 100644 --- a/synapse/api/events/room.py +++ b/synapse/api/events/room.py @@ -180,3 +180,12 @@ class RoomAliasesEvent(SynapseStateEvent): def get_content_template(self): return {} + + +class RoomRedactionEvent(SynapseEvent): + TYPE = "m.room.redaction" + + valid_keys = SynapseEvent.valid_keys + ["redacts"] + + def get_content_template(self): + return {} diff --git a/synapse/api/events/utils.py b/synapse/api/events/utils.py new file mode 100644 index 0000000000..c3a32be8c1 --- /dev/null +++ b/synapse/api/events/utils.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .room import ( + RoomMemberEvent, RoomJoinRulesEvent, RoomPowerLevelsEvent, + RoomAddStateLevelEvent, RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent, + RoomAliasesEvent, RoomCreateEvent, +) + +def prune_event(event): + """ Prunes the given event of all keys we don't know about or think could + potentially be dodgy. + + This is used when we "redact" an event. We want to remove all fields that + the user has specified, but we do want to keep necessary information like + type, state_key etc. + """ + + # Remove all extraneous fields. + event.unrecognized_keys = {} + + new_content = {} + + def add_fields(*fields): + for field in fields: + if field in event.content: + new_content[field] = event.content[field] + + if event.type == RoomMemberEvent.TYPE: + add_fields("membership") + elif event.type == RoomCreateEvent.TYPE: + add_fields("creator") + elif event.type == RoomJoinRulesEvent.TYPE: + add_fields("join_rule") + elif event.type == RoomPowerLevelsEvent.TYPE: + # TODO: Actually check these are valid user_ids etc. + add_fields("default") + for k, v in event.content.items(): + if k.startswith("@") and isinstance(v, (int, long)): + new_content[k] = v + elif event.type == RoomAddStateLevelEvent.TYPE: + add_fields("level") + elif event.type == RoomSendEventLevelEvent.TYPE: + add_fields("level") + elif event.type == RoomOpsPowerLevelsEvent.TYPE: + add_fields("kick_level", "ban_level", "redact_level") + elif event.type == RoomAliasesEvent.TYPE: + add_fields("aliases") + + event.content = new_content + + return event diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 4d8727e8af..c0f9a7c807 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -244,6 +244,7 @@ class RoomCreationHandler(BaseHandler): etype=RoomOpsPowerLevelsEvent.TYPE, ban_level=50, kick_level=50, + redact_level=50, ) return [ diff --git a/synapse/rest/room.py b/synapse/rest/room.py index cf2e7af2e4..a01dab1b8e 100644 --- a/synapse/rest/room.py +++ b/synapse/rest/room.py @@ -19,7 +19,7 @@ from twisted.internet import defer from base import RestServlet, client_path_pattern from synapse.api.errors import SynapseError, Codes from synapse.streams.config import PaginationConfig -from synapse.api.events.room import RoomMemberEvent +from synapse.api.events.room import RoomMemberEvent, RoomRedactionEvent from synapse.api.constants import Membership import json @@ -431,6 +431,41 @@ class RoomMembershipRestServlet(RestServlet): self.txns.store_client_transaction(request, txn_id, response) defer.returnValue(response) +class RoomRedactEventRestServlet(RestServlet): + def register(self, http_server): + PATTERN = ("/rooms/(?P[^/]*)/redact/(?P[^/]*)") + register_txn_path(self, PATTERN, http_server) + + @defer.inlineCallbacks + def on_POST(self, request, room_id, event_id): + user = yield self.auth.get_user_by_req(request) + content = _parse_json(request) + + event = self.event_factory.create_event( + etype=RoomRedactionEvent.TYPE, + room_id=urllib.unquote(room_id), + user_id=user.to_string(), + content=content, + redacts=event_id, + ) + + msg_handler = self.handlers.message_handler + yield msg_handler.send_message(event) + + defer.returnValue((200, {"event_id": event.event_id})) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, event_id, txn_id): + try: + defer.returnValue(self.txns.get_client_transaction(request, txn_id)) + except KeyError: + pass + + response = yield self.on_POST(request, room_id, event_id) + + self.txns.store_client_transaction(request, txn_id, response) + defer.returnValue(response) + def _parse_json(request): try: @@ -486,3 +521,4 @@ def register_servlets(hs, http_server): PublicRoomListRestServlet(hs).register(http_server) RoomStateRestServlet(hs).register(http_server) RoomInitialSyncRestServlet(hs).register(http_server) + RoomRedactEventRestServlet(hs).register(http_server) diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 1aaef3f493..15919eb580 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -24,6 +24,7 @@ from synapse.api.events.room import ( RoomAddStateLevelEvent, RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent, + RoomRedactionEvent, ) from synapse.util.logutils import log_function @@ -56,12 +57,13 @@ SCHEMAS = [ "presence", "im", "room_aliases", + "redactions", ] # Remember to update this number every time an incompatible change is made to # database schema files, so the users will be informed on server restarts. -SCHEMA_VERSION = 3 +SCHEMA_VERSION = 4 class _RollbackButIsFineException(Exception): @@ -182,6 +184,8 @@ class DataStore(RoomMemberStore, RoomStore, self._store_send_event_level(txn, event) elif event.type == RoomOpsPowerLevelsEvent.TYPE: self._store_ops_level(txn, event) + elif event.type == RoomRedactionEvent.TYPE: + self._store_redaction(txn, event) vals = { "topological_ordering": event.depth, @@ -203,7 +207,7 @@ class DataStore(RoomMemberStore, RoomStore, unrec = { k: v for k, v in event.get_full_dict().items() - if k not in vals.keys() + if k not in vals.keys() and k not in ["redacted", "redacted_because"] } vals["unrecognized_keys"] = json.dumps(unrec) @@ -242,14 +246,28 @@ class DataStore(RoomMemberStore, RoomStore, } ) + def _store_redaction(self, txn, event): + txn.execute( + "INSERT OR IGNORE INTO redactions " + "(event_id, redacts) VALUES (?,?)", + (event.event_id, event.redacts) + ) + @defer.inlineCallbacks def get_current_state(self, room_id, event_type=None, state_key=""): + del_sql = ( + "SELECT event_id FROM redactions WHERE redacts = e.event_id " + "LIMIT 1" + ) + sql = ( - "SELECT e.* FROM events as e " + "SELECT e.*, (%(redacted)s) AS redacted FROM events as e " "INNER JOIN current_state_events as c ON e.event_id = c.event_id " "INNER JOIN state_events as s ON e.event_id = s.event_id " "WHERE c.room_id = ? " - ) + ) % { + "redacted": del_sql, + } if event_type: sql += " AND s.type = ? AND s.state_key = ? " diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 76ed7d06fb..889de2bedc 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -17,6 +17,7 @@ import logging from twisted.internet import defer from synapse.api.errors import StoreError +from synapse.api.events.utils import prune_event from synapse.util.logutils import log_function import collections @@ -345,7 +346,7 @@ class SQLBaseStore(object): return self.runInteraction(func) def _parse_event_from_row(self, row_dict): - d = copy.deepcopy({k: v for k, v in row_dict.items() if v}) + d = copy.deepcopy({k: v for k, v in row_dict.items()}) d.pop("stream_ordering", None) d.pop("topological_ordering", None) @@ -373,8 +374,8 @@ class SQLBaseStore(object): sql = "SELECT * FROM events WHERE event_id = ?" for ev in events: - if hasattr(ev, "prev_state"): - # Load previous state_content. + if hasattr(ev, "prev_state"): + # Load previous state_content. # TODO: Should we be pulling this out above? cursor = txn.execute(sql, (ev.prev_state,)) prevs = self.cursor_to_dict(cursor) @@ -382,8 +383,32 @@ class SQLBaseStore(object): prev = self._parse_event_from_row(prevs[0]) ev.prev_content = prev.content + if not hasattr(ev, "redacted"): + logger.debug("Doesn't have redacted key: %s", ev) + ev.redacted = self._has_been_redacted_txn(txn, ev) + + if ev.redacted: + # Get the redaction event. + sql = "SELECT * FROM events WHERE event_id = ?" + txn.execute(sql, (ev.redacted,)) + + del_evs = self._parse_events_txn( + txn, self.cursor_to_dict(txn) + ) + + if del_evs: + prune_event(ev) + ev.redacted_because = del_evs[0] + return events + def _has_been_redacted_txn(self, txn, event): + sql = "SELECT event_id FROM redactions WHERE redacts = ?" + txn.execute(sql, (event.event_id,)) + result = txn.fetchone() + return result[0] if result else None + + class Table(object): """ A base class used to store information about a particular table. """ diff --git a/synapse/storage/room.py b/synapse/storage/room.py index 5adf8cdf1b..8cd46334cf 100644 --- a/synapse/storage/room.py +++ b/synapse/storage/room.py @@ -27,7 +27,7 @@ import logging logger = logging.getLogger(__name__) -OpsLevel = collections.namedtuple("OpsLevel", ("ban_level", "kick_level")) +OpsLevel = collections.namedtuple("OpsLevel", ("ban_level", "kick_level", "redact_level")) class RoomStore(SQLBaseStore): @@ -189,7 +189,8 @@ class RoomStore(SQLBaseStore): def _get_ops_levels(self, txn, room_id): sql = ( - "SELECT ban_level, kick_level FROM room_ops_levels as r " + "SELECT ban_level, kick_level, redact_level " + "FROM room_ops_levels as r " "INNER JOIN current_state_events as c " "ON r.event_id = c.event_id " "WHERE c.room_id = ? " @@ -198,7 +199,7 @@ class RoomStore(SQLBaseStore): rows = txn.execute(sql, (room_id,)).fetchall() if len(rows) == 1: - return OpsLevel(rows[0][0], rows[0][1]) + return OpsLevel(rows[0][0], rows[0][1], rows[0][2]) else: return OpsLevel(None, None) @@ -326,6 +327,9 @@ class RoomStore(SQLBaseStore): if "ban_level" in event.content: content["ban_level"] = event.content["ban_level"] + if "redact_level" in event.content: + content["redact_level"] = event.content["redact_level"] + self._simple_insert_txn( txn, "room_ops_levels", diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index 04b4067d03..958e730591 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -182,14 +182,22 @@ class RoomMemberStore(SQLBaseStore): ) def _get_members_query_txn(self, txn, where_clause, where_values): + del_sql = ( + "SELECT event_id FROM redactions WHERE redacts = e.event_id " + "LIMIT 1" + ) + sql = ( - "SELECT e.* FROM events as e " + "SELECT e.*, (%(redacted)s) AS redacted FROM events as e " "INNER JOIN room_memberships as m " "ON e.event_id = m.event_id " "INNER JOIN current_state_events as c " "ON m.event_id = c.event_id " - "WHERE %s " - ) % (where_clause,) + "WHERE %(where)s " + ) % { + "redacted": del_sql, + "where": where_clause, + } txn.execute(sql, where_values) rows = self.cursor_to_dict(txn) diff --git a/synapse/storage/schema/delta/v4.sql b/synapse/storage/schema/delta/v4.sql new file mode 100644 index 0000000000..25d2ead450 --- /dev/null +++ b/synapse/storage/schema/delta/v4.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS redactions ( + event_id TEXT NOT NULL, + redacts TEXT NOT NULL, + CONSTRAINT ev_uniq UNIQUE (event_id) +); + +CREATE INDEX IF NOT EXISTS redactions_event_id ON redactions (event_id); +CREATE INDEX IF NOT EXISTS redactions_redacts ON redactions (redacts); + +ALTER TABLE room_ops_levels ADD COLUMN redact_level INTEGER; + +PRAGMA user_version = 4; diff --git a/synapse/storage/schema/im.sql b/synapse/storage/schema/im.sql index 6ffea51310..3aa83f5c8c 100644 --- a/synapse/storage/schema/im.sql +++ b/synapse/storage/schema/im.sql @@ -150,7 +150,8 @@ CREATE TABLE IF NOT EXISTS room_ops_levels( event_id TEXT NOT NULL, room_id TEXT NOT NULL, ban_level INTEGER, - kick_level INTEGER + kick_level INTEGER, + redact_level INTEGER ); CREATE INDEX IF NOT EXISTS room_ops_levels_event_id ON room_ops_levels(event_id); diff --git a/synapse/storage/schema/redactions.sql b/synapse/storage/schema/redactions.sql new file mode 100644 index 0000000000..4c2829d05d --- /dev/null +++ b/synapse/storage/schema/redactions.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS redactions ( + event_id TEXT NOT NULL, + redacts TEXT NOT NULL, + CONSTRAINT ev_uniq UNIQUE (event_id) +); + +CREATE INDEX IF NOT EXISTS redactions_event_id ON redactions (event_id); +CREATE INDEX IF NOT EXISTS redactions_redacts ON redactions (redacts); diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py index a76fecf24f..d61f909939 100644 --- a/synapse/storage/stream.py +++ b/synapse/storage/stream.py @@ -157,6 +157,11 @@ class StreamStore(SQLBaseStore): "WHERE m.user_id = ? " ) + del_sql = ( + "SELECT event_id FROM redactions WHERE redacts = e.event_id " + "LIMIT 1" + ) + if limit: limit = max(limit, MAX_STREAM_SIZE) else: @@ -171,13 +176,14 @@ class StreamStore(SQLBaseStore): return sql = ( - "SELECT * FROM events as e WHERE " + "SELECT *, (%(redacted)s) AS redacted FROM events AS e WHERE " "((room_id IN (%(current)s)) OR " "(event_id IN (%(invites)s))) " "AND e.stream_ordering > ? AND e.stream_ordering <= ? " "AND e.outlier = 0 " "ORDER BY stream_ordering ASC LIMIT %(limit)d " ) % { + "redacted": del_sql, "current": current_room_membership_sql, "invites": membership_sql, "limit": limit @@ -224,11 +230,21 @@ class StreamStore(SQLBaseStore): else: limit_str = "" + del_sql = ( + "SELECT event_id FROM redactions WHERE redacts = events.event_id " + "LIMIT 1" + ) + sql = ( - "SELECT * FROM events " + "SELECT *, (%(redacted)s) AS redacted FROM events " "WHERE outlier = 0 AND room_id = ? AND %(bounds)s " "ORDER BY topological_ordering %(order)s, stream_ordering %(order)s %(limit)s " - ) % {"bounds": bounds, "order": order, "limit": limit_str} + ) % { + "redacted": del_sql, + "bounds": bounds, + "order": order, + "limit": limit_str + } rows = yield self._execute_and_decode( sql, @@ -257,11 +273,18 @@ class StreamStore(SQLBaseStore): with_feedback=False): # TODO (erikj): Handle compressed feedback + del_sql = ( + "SELECT event_id FROM redactions WHERE redacts = events.event_id " + "LIMIT 1" + ) + sql = ( - "SELECT * FROM events " + "SELECT *, (%(redacted)s) AS redacted FROM events " "WHERE room_id = ? AND stream_ordering <= ? " "ORDER BY topological_ordering DESC, stream_ordering DESC LIMIT ? " - ) + ) % { + "redacted": del_sql, + } rows = yield self._execute_and_decode( sql, diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py new file mode 100644 index 0000000000..dae1641ea1 --- /dev/null +++ b/tests/storage/test_redaction.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from tests import unittest +from twisted.internet import defer + +from synapse.server import HomeServer +from synapse.api.constants import Membership +from synapse.api.events.room import ( + RoomMemberEvent, MessageEvent, RoomRedactionEvent, +) + +from tests.utils import SQLiteMemoryDbPool + + +class RedactionTestCase(unittest.TestCase): + + @defer.inlineCallbacks + def setUp(self): + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "test", + db_pool=db_pool, + ) + + self.store = hs.get_datastore() + self.event_factory = hs.get_event_factory() + + self.u_alice = hs.parse_userid("@alice:test") + self.u_bob = hs.parse_userid("@bob:test") + + self.room1 = hs.parse_roomid("!abc123:test") + + self.depth = 1 + + @defer.inlineCallbacks + def inject_room_member(self, room, user, membership, prev_state=None, + extra_content={}): + self.depth += 1 + + event = self.event_factory.create_event( + etype=RoomMemberEvent.TYPE, + user_id=user.to_string(), + state_key=user.to_string(), + room_id=room.to_string(), + membership=membership, + content={"membership": membership}, + depth=self.depth, + ) + + event.content.update(extra_content) + + if prev_state: + event.prev_state = prev_state + + # Have to create a join event using the eventfactory + yield self.store.persist_event( + event + ) + + defer.returnValue(event) + + @defer.inlineCallbacks + def inject_message(self, room, user, body): + self.depth += 1 + + event = self.event_factory.create_event( + etype=MessageEvent.TYPE, + user_id=user.to_string(), + room_id=room.to_string(), + content={"body": body, "msgtype": u"message"}, + depth=self.depth, + ) + + yield self.store.persist_event( + event + ) + + defer.returnValue(event) + + @defer.inlineCallbacks + def inject_redaction(self, room, event_id, user, reason): + event = self.event_factory.create_event( + etype=RoomRedactionEvent.TYPE, + user_id=user.to_string(), + room_id=room.to_string(), + content={"reason": reason}, + depth=self.depth, + redacts=event_id, + ) + + yield self.store.persist_event( + event + ) + + defer.returnValue(event) + + @defer.inlineCallbacks + def test_redact(self): + yield self.inject_room_member( + self.room1, self.u_alice, Membership.JOIN + ) + + start = yield self.store.get_room_events_max_id() + + msg_event = yield self.inject_message(self.room1, self.u_alice, u"t") + + end = yield self.store.get_room_events_max_id() + + results, _ = yield self.store.get_room_events_stream( + self.u_alice.to_string(), + start, + end, + None, # Is currently ignored + ) + + self.assertEqual(1, len(results)) + + # Check event has not been redacted: + event = results[0] + + self.assertObjectHasAttributes( + { + "type": MessageEvent.TYPE, + "user_id": self.u_alice.to_string(), + "content": {"body": "t", "msgtype": "message"}, + }, + event, + ) + + self.assertFalse(hasattr(event, "redacted_because")) + + # Redact event + reason = "Because I said so" + yield self.inject_redaction( + self.room1, msg_event.event_id, self.u_alice, reason + ) + + results, _ = yield self.store.get_room_events_stream( + self.u_alice.to_string(), + start, + end, + None, # Is currently ignored + ) + + self.assertEqual(1, len(results)) + + # Check redaction + + event = results[0] + + self.assertObjectHasAttributes( + { + "type": MessageEvent.TYPE, + "user_id": self.u_alice.to_string(), + "content": {}, + }, + event, + ) + + self.assertTrue(hasattr(event, "redacted_because")) + + self.assertObjectHasAttributes( + { + "type": RoomRedactionEvent.TYPE, + "user_id": self.u_alice.to_string(), + "content": {"reason": reason}, + }, + event.redacted_because, + ) + + @defer.inlineCallbacks + def test_redact_join(self): + yield self.inject_room_member( + self.room1, self.u_alice, Membership.JOIN + ) + + start = yield self.store.get_room_events_max_id() + + msg_event = yield self.inject_room_member( + self.room1, self.u_bob, Membership.JOIN, + extra_content={"blue": "red"}, + ) + + end = yield self.store.get_room_events_max_id() + + results, _ = yield self.store.get_room_events_stream( + self.u_alice.to_string(), + start, + end, + None, # Is currently ignored + ) + + self.assertEqual(1, len(results)) + + # Check event has not been redacted: + event = results[0] + + self.assertObjectHasAttributes( + { + "type": RoomMemberEvent.TYPE, + "user_id": self.u_bob.to_string(), + "content": {"membership": Membership.JOIN, "blue": "red"}, + }, + event, + ) + + self.assertFalse(hasattr(event, "redacted_because")) + + # Redact event + reason = "Because I said so" + yield self.inject_redaction( + self.room1, msg_event.event_id, self.u_alice, reason + ) + + results, _ = yield self.store.get_room_events_stream( + self.u_alice.to_string(), + start, + end, + None, # Is currently ignored + ) + + self.assertEqual(1, len(results)) + + # Check redaction + + event = results[0] + + self.assertObjectHasAttributes( + { + "type": RoomMemberEvent.TYPE, + "user_id": self.u_bob.to_string(), + "content": {"membership": Membership.JOIN}, + }, + event, + ) + + self.assertTrue(hasattr(event, "redacted_because")) + + self.assertObjectHasAttributes( + { + "type": RoomRedactionEvent.TYPE, + "user_id": self.u_alice.to_string(), + "content": {"reason": reason}, + }, + event.redacted_because, + ) diff --git a/tests/utils.py b/tests/utils.py index bc5d35e56b..bb8e9964dd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -262,7 +262,7 @@ class MemoryDataStore(object): return defer.succeed("invite") def get_ops_levels(self, room_id): - return defer.succeed((5, 5)) + return defer.succeed((5, 5, 5)) def _format_call(args, kwargs):