diff --git a/synapse/__init__.py b/synapse/__init__.py
index bba551b2c4..a340a5db66 100644
--- a/synapse/__init__.py
+++ b/synapse/__init__.py
@@ -16,4 +16,4 @@
""" This is a reference implementation of a synapse home server.
"""
-__version__ = "0.3.3"
+__version__ = "0.3.4"
diff --git a/synapse/api/auth.py b/synapse/api/auth.py
index 8f32191b57..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":
@@ -372,11 +400,11 @@ class Auth(object):
}
removed = set(old_people.keys()) - set(new_people.keys())
- added = set(old_people.keys()) - set(new_people.keys())
+ added = set(new_people.keys()) - set(old_people.keys())
same = set(old_people.keys()) & set(new_people.keys())
for r in removed:
- if int(old_list.content[r]) > user_level:
+ if int(old_list[r]) > user_level:
raise AuthError(
403,
"You don't have permission to remove user: %s" % (r, )
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/config/captcha.py b/synapse/config/captcha.py
index 8ebcfc3623..4ed9070b9e 100644
--- a/synapse/config/captcha.py
+++ b/synapse/config/captcha.py
@@ -24,6 +24,7 @@ class CaptchaConfig(Config):
self.captcha_ip_origin_is_x_forwarded = (
args.captcha_ip_origin_is_x_forwarded
)
+ self.captcha_bypass_secret = args.captcha_bypass_secret
@classmethod
def add_arguments(cls, parser):
@@ -43,4 +44,8 @@ class CaptchaConfig(Config):
"--captcha_ip_origin_is_x_forwarded", type=bool, default=False,
help="When checking captchas, use the X-Forwarded-For (XFF) header"
+ " as the client IP and not the actual client IP."
- )
\ No newline at end of file
+ )
+ group.add_argument(
+ "--captcha_bypass_secret", type=str,
+ help="A secret key used to bypass the captcha test entirely."
+ )
diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py
index 4b810a2302..5a11fd6c76 100644
--- a/synapse/config/homeserver.py
+++ b/synapse/config/homeserver.py
@@ -21,11 +21,12 @@ from .ratelimiting import RatelimitConfig
from .repository import ContentRepositoryConfig
from .captcha import CaptchaConfig
from .email import EmailConfig
+from .voip import VoipConfig
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
- EmailConfig):
+ EmailConfig, VoipConfig):
pass
diff --git a/synapse/config/voip.py b/synapse/config/voip.py
new file mode 100644
index 0000000000..3a51664f46
--- /dev/null
+++ b/synapse/config/voip.py
@@ -0,0 +1,41 @@
+# 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 ._base import Config
+
+
+class VoipConfig(Config):
+
+ def __init__(self, args):
+ super(VoipConfig, self).__init__(args)
+ self.turn_uris = args.turn_uris
+ self.turn_shared_secret = args.turn_shared_secret
+ self.turn_user_lifetime = args.turn_user_lifetime
+
+ @classmethod
+ def add_arguments(cls, parser):
+ super(VoipConfig, cls).add_arguments(parser)
+ group = parser.add_argument_group("voip")
+ group.add_argument(
+ "--turn-uris", type=str, default=None,
+ help="The public URIs of the TURN server to give to clients"
+ )
+ group.add_argument(
+ "--turn-shared-secret", type=str, default=None,
+ help="The shared secret used to compute passwords for the TURN server"
+ )
+ group.add_argument(
+ "--turn-user-lifetime", type=int, default=(1000 * 60 * 60),
+ help="How long generated TURN credentials last, in ms"
+ )
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index 001c6c110c..f52591d2a3 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -169,7 +169,15 @@ class FederationHandler(BaseHandler):
)
if not backfilled:
- yield self.notifier.on_new_room_event(event)
+ extra_users = []
+ if event.type == RoomMemberEvent.TYPE:
+ target_user_id = event.state_key
+ target_user = self.hs.parse_userid(target_user_id)
+ extra_users.append(target_user)
+
+ yield self.notifier.on_new_room_event(
+ event, extra_users=extra_users
+ )
if event.type == RoomMemberEvent.TYPE:
if event.membership == Membership.JOIN:
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index 14fae689f2..317ef2c80c 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -233,6 +233,22 @@ class MessageHandler(BaseHandler):
yield self._on_new_room_event(event, snapshot)
@defer.inlineCallbacks
+ def get_state_events(self, user_id, room_id):
+ """Retrieve all state events for a given room.
+
+ Args:
+ user_id(str): The user requesting state events.
+ room_id(str): The room ID to get all state events from.
+ Returns:
+ A list of dicts representing state events. [{}, {}, {}]
+ """
+ yield self.auth.check_joined_room(room_id, user_id)
+
+ # TODO: This is duplicating logic from snapshot_all_rooms
+ current_state = yield self.store.get_current_state(room_id)
+ defer.returnValue([self.hs.serialize_event(c) for c in current_state])
+
+ @defer.inlineCallbacks
def snapshot_all_rooms(self, user_id=None, pagin_config=None,
feedback=False):
"""Retrieve a snapshot of all rooms the user is invited or has joined.
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 5bc1280432..c0f9a7c807 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -146,17 +146,6 @@ class RoomCreationHandler(BaseHandler):
)
yield handle_event(name_event)
- elif room_alias:
- name = room_alias.to_string()
- name_event = self.event_factory.create_event(
- etype=RoomNameEvent.TYPE,
- room_id=room_id,
- user_id=user_id,
- required_power_level=50,
- content={"name": name},
- )
-
- yield handle_event(name_event)
if "topic" in config:
topic = config["topic"]
@@ -255,6 +244,7 @@ class RoomCreationHandler(BaseHandler):
etype=RoomOpsPowerLevelsEvent.TYPE,
ban_level=50,
kick_level=50,
+ redact_level=50,
)
return [
diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py
index ed785cfbd5..3b9aa59733 100644
--- a/synapse/rest/__init__.py
+++ b/synapse/rest/__init__.py
@@ -15,7 +15,7 @@
from . import (
- room, events, register, login, profile, presence, initial_sync, directory
+ room, events, register, login, profile, presence, initial_sync, directory, voip
)
@@ -42,3 +42,4 @@ class RestServletFactory(object):
presence.register_servlets(hs, client_resource)
initial_sync.register_servlets(hs, client_resource)
directory.register_servlets(hs, client_resource)
+ voip.register_servlets(hs, client_resource)
diff --git a/synapse/rest/register.py b/synapse/rest/register.py
index af528a44f6..4935e323d9 100644
--- a/synapse/rest/register.py
+++ b/synapse/rest/register.py
@@ -21,6 +21,8 @@ from synapse.api.constants import LoginType
from base import RestServlet, client_path_pattern
import synapse.util.stringutils as stringutils
+from hashlib import sha1
+import hmac
import json
import logging
import urllib
@@ -28,6 +30,16 @@ import urllib
logger = logging.getLogger(__name__)
+# We ought to be using hmac.compare_digest() but on older pythons it doesn't
+# exist. It's a _really minor_ security flaw to use plain string comparison
+# because the timing attack is so obscured by all the other code here it's
+# unlikely to make much difference
+if hasattr(hmac, "compare_digest"):
+ compare_digest = hmac.compare_digest
+else:
+ compare_digest = lambda a, b: a == b
+
+
class RegisterRestServlet(RestServlet):
"""Handles registration with the home server.
@@ -142,6 +154,38 @@ class RegisterRestServlet(RestServlet):
if not self.hs.config.enable_registration_captcha:
raise SynapseError(400, "Captcha not required.")
+ yield self._check_recaptcha(request, register_json, session)
+
+ session[LoginType.RECAPTCHA] = True # mark captcha as done
+ self._save_session(session)
+ defer.returnValue({
+ "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY]
+ })
+
+ @defer.inlineCallbacks
+ def _check_recaptcha(self, request, register_json, session):
+ if ("captcha_bypass_hmac" in register_json and
+ self.hs.config.captcha_bypass_secret):
+ if "user" not in register_json:
+ raise SynapseError(400, "Captcha bypass needs 'user'")
+
+ want = hmac.new(
+ key=self.hs.config.captcha_bypass_secret,
+ msg=register_json["user"],
+ digestmod=sha1,
+ ).hexdigest()
+
+ # str() because otherwise hmac complains that 'unicode' does not
+ # have the buffer interface
+ got = str(register_json["captcha_bypass_hmac"])
+
+ if compare_digest(want, got):
+ session["user"] = register_json["user"]
+ defer.returnValue(None)
+ else:
+ raise SynapseError(400, "Captcha bypass HMAC incorrect",
+ errcode=Codes.CAPTCHA_NEEDED)
+
challenge = None
user_response = None
try:
@@ -166,11 +210,6 @@ class RegisterRestServlet(RestServlet):
challenge,
user_response
)
- session[LoginType.RECAPTCHA] = True # mark captcha as done
- self._save_session(session)
- defer.returnValue({
- "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY]
- })
@defer.inlineCallbacks
def _do_email_identity(self, request, register_json, session):
@@ -195,6 +234,10 @@ class RegisterRestServlet(RestServlet):
# captcha should've been done by this stage!
raise SynapseError(400, "Captcha is required.")
+ if ("user" in session and "user" in register_json and
+ session["user"] != register_json["user"]):
+ raise SynapseError(400, "Cannot change user ID during registration")
+
password = register_json["password"].encode("utf-8")
desired_user_id = (register_json["user"].encode("utf-8") if "user"
in register_json else None)
diff --git a/synapse/rest/room.py b/synapse/rest/room.py
index ecb1e346d9..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
@@ -329,12 +329,13 @@ class RoomStateRestServlet(RestServlet):
@defer.inlineCallbacks
def on_GET(self, request, room_id):
user = yield self.auth.get_user_by_req(request)
- # TODO: Get all the current state for this room and return in the same
- # format as initial sync, that is:
- # [
- # { state event }, { state event }
- # ]
- defer.returnValue((200, []))
+ handler = self.handlers.message_handler
+ # Get all the current state for this room
+ events = yield handler.get_state_events(
+ room_id=urllib.unquote(room_id),
+ user_id=user.to_string(),
+ )
+ defer.returnValue((200, events))
# TODO: Needs unit testing
@@ -430,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:
@@ -485,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/rest/voip.py b/synapse/rest/voip.py
new file mode 100644
index 0000000000..2e4627606f
--- /dev/null
+++ b/synapse/rest/voip.py
@@ -0,0 +1,60 @@
+# -*- 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 twisted.internet import defer
+
+from base import RestServlet, client_path_pattern
+
+
+import hmac
+import hashlib
+import base64
+
+
+class VoipRestServlet(RestServlet):
+ PATTERN = client_path_pattern("/voip/turnServer$")
+
+ @defer.inlineCallbacks
+ def on_GET(self, request):
+ auth_user = yield self.auth.get_user_by_req(request)
+
+ turnUris = self.hs.config.turn_uris
+ turnSecret = self.hs.config.turn_shared_secret
+ userLifetime = self.hs.config.turn_user_lifetime
+ if not turnUris or not turnSecret or not userLifetime:
+ defer.returnValue( (200, {}) )
+
+ expiry = self.hs.get_clock().time_msec() + userLifetime
+ username = "%d:%s" % (expiry, auth_user.to_string())
+
+ mac = hmac.new(turnSecret, msg=username, digestmod=hashlib.sha1)
+ # We need to use standard base64 encoding here, *not* syutil's encode_base64
+ # because we need to add the standard padding to get the same result as the
+ # TURN server.
+ password = base64.b64encode(mac.digest())
+
+ defer.returnValue( (200, {
+ 'username': username,
+ 'password': password,
+ 'ttl': userLifetime / 1000,
+ 'uris': turnUris,
+ }) )
+
+ def on_OPTIONS(self, request):
+ return (200, {})
+
+
+def register_servlets(hs, http_server):
+ VoipRestServlet(hs).register(http_server)
diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py
index 66658f6721..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)
@@ -217,7 +221,8 @@ class DataStore(RoomMemberStore, RoomStore,
)
raise _RollbackButIsFineException("_persist_event")
- if is_new_state and hasattr(event, "state_key"):
+ is_state = hasattr(event, "state_key") and event.state_key is not None
+ if is_new_state and is_state:
vals = {
"event_id": event.event_id,
"room_id": event.room_id,
@@ -241,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,
|