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)
@@ -322,6 +327,29 @@ class Auth(object):
)
@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():
if k == "default":
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<room_id>[^/]*)/redact/(?P<event_id>[^/]*)")
+ 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):
|