mirror of
https://github.com/element-hq/synapse.git
synced 2024-11-25 11:05:49 +03:00
Merge branch 'deletions' of github.com:matrix-org/synapse into develop
This commit is contained in:
commit
37bfe44046
17 changed files with 531 additions and 27 deletions
|
@ -19,7 +19,9 @@ from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import Membership, JoinRules
|
from synapse.api.constants import Membership, JoinRules
|
||||||
from synapse.api.errors import AuthError, StoreError, Codes, SynapseError
|
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
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@ -70,6 +72,9 @@ class Auth(object):
|
||||||
if event.type == RoomPowerLevelsEvent.TYPE:
|
if event.type == RoomPowerLevelsEvent.TYPE:
|
||||||
yield self._check_power_levels(event)
|
yield self._check_power_levels(event)
|
||||||
|
|
||||||
|
if event.type == RoomRedactionEvent.TYPE:
|
||||||
|
yield self._check_redaction(event)
|
||||||
|
|
||||||
defer.returnValue(True)
|
defer.returnValue(True)
|
||||||
else:
|
else:
|
||||||
raise AuthError(500, "Unknown event: %s" % event)
|
raise AuthError(500, "Unknown event: %s" % event)
|
||||||
|
@ -170,7 +175,7 @@ class Auth(object):
|
||||||
event.room_id,
|
event.room_id,
|
||||||
event.user_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:
|
if kick_level:
|
||||||
kick_level = int(kick_level)
|
kick_level = int(kick_level)
|
||||||
|
@ -187,7 +192,7 @@ class Auth(object):
|
||||||
event.user_id,
|
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:
|
if ban_level:
|
||||||
ban_level = int(ban_level)
|
ban_level = int(ban_level)
|
||||||
|
@ -321,6 +326,29 @@ class Auth(object):
|
||||||
"You don't have permission to change that state"
|
"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
|
@defer.inlineCallbacks
|
||||||
def _check_power_levels(self, event):
|
def _check_power_levels(self, event):
|
||||||
for k, v in event.content.items():
|
for k, v in event.content.items():
|
||||||
|
|
|
@ -22,7 +22,8 @@ def serialize_event(hs, e):
|
||||||
if not isinstance(e, SynapseEvent):
|
if not isinstance(e, SynapseEvent):
|
||||||
return e
|
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:
|
if "age_ts" in d:
|
||||||
d["age"] = int(hs.get_clock().time_msec()) - d["age_ts"]
|
d["age"] = int(hs.get_clock().time_msec()) - d["age_ts"]
|
||||||
del d["age_ts"]
|
del d["age_ts"]
|
||||||
|
@ -58,17 +59,19 @@ class SynapseEvent(JsonEncodedObject):
|
||||||
"required_power_level",
|
"required_power_level",
|
||||||
"age_ts",
|
"age_ts",
|
||||||
"prev_content",
|
"prev_content",
|
||||||
|
"prev_state",
|
||||||
|
"redacted_because",
|
||||||
]
|
]
|
||||||
|
|
||||||
internal_keys = [
|
internal_keys = [
|
||||||
"is_state",
|
"is_state",
|
||||||
"prev_events",
|
"prev_events",
|
||||||
"prev_state",
|
|
||||||
"depth",
|
"depth",
|
||||||
"destinations",
|
"destinations",
|
||||||
"origin",
|
"origin",
|
||||||
"outlier",
|
"outlier",
|
||||||
"power_level",
|
"power_level",
|
||||||
|
"redacted",
|
||||||
]
|
]
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
|
|
|
@ -17,7 +17,8 @@ from synapse.api.events.room import (
|
||||||
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
|
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
|
||||||
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
|
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
|
||||||
RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomOpsPowerLevelsEvent,
|
RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomOpsPowerLevelsEvent,
|
||||||
RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent
|
RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent,
|
||||||
|
RoomRedactionEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
from synapse.util.stringutils import random_string
|
from synapse.util.stringutils import random_string
|
||||||
|
@ -39,6 +40,7 @@ class EventFactory(object):
|
||||||
RoomAddStateLevelEvent,
|
RoomAddStateLevelEvent,
|
||||||
RoomSendEventLevelEvent,
|
RoomSendEventLevelEvent,
|
||||||
RoomOpsPowerLevelsEvent,
|
RoomOpsPowerLevelsEvent,
|
||||||
|
RoomRedactionEvent,
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
|
|
|
@ -180,3 +180,12 @@ class RoomAliasesEvent(SynapseStateEvent):
|
||||||
|
|
||||||
def get_content_template(self):
|
def get_content_template(self):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomRedactionEvent(SynapseEvent):
|
||||||
|
TYPE = "m.room.redaction"
|
||||||
|
|
||||||
|
valid_keys = SynapseEvent.valid_keys + ["redacts"]
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
64
synapse/api/events/utils.py
Normal file
64
synapse/api/events/utils.py
Normal file
|
@ -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
|
|
@ -244,6 +244,7 @@ class RoomCreationHandler(BaseHandler):
|
||||||
etype=RoomOpsPowerLevelsEvent.TYPE,
|
etype=RoomOpsPowerLevelsEvent.TYPE,
|
||||||
ban_level=50,
|
ban_level=50,
|
||||||
kick_level=50,
|
kick_level=50,
|
||||||
|
redact_level=50,
|
||||||
)
|
)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -19,7 +19,7 @@ from twisted.internet import defer
|
||||||
from base import RestServlet, client_path_pattern
|
from base import RestServlet, client_path_pattern
|
||||||
from synapse.api.errors import SynapseError, Codes
|
from synapse.api.errors import SynapseError, Codes
|
||||||
from synapse.streams.config import PaginationConfig
|
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
|
from synapse.api.constants import Membership
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
@ -431,6 +431,41 @@ class RoomMembershipRestServlet(RestServlet):
|
||||||
self.txns.store_client_transaction(request, txn_id, response)
|
self.txns.store_client_transaction(request, txn_id, response)
|
||||||
defer.returnValue(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):
|
def _parse_json(request):
|
||||||
try:
|
try:
|
||||||
|
@ -486,3 +521,4 @@ def register_servlets(hs, http_server):
|
||||||
PublicRoomListRestServlet(hs).register(http_server)
|
PublicRoomListRestServlet(hs).register(http_server)
|
||||||
RoomStateRestServlet(hs).register(http_server)
|
RoomStateRestServlet(hs).register(http_server)
|
||||||
RoomInitialSyncRestServlet(hs).register(http_server)
|
RoomInitialSyncRestServlet(hs).register(http_server)
|
||||||
|
RoomRedactEventRestServlet(hs).register(http_server)
|
||||||
|
|
|
@ -24,6 +24,7 @@ from synapse.api.events.room import (
|
||||||
RoomAddStateLevelEvent,
|
RoomAddStateLevelEvent,
|
||||||
RoomSendEventLevelEvent,
|
RoomSendEventLevelEvent,
|
||||||
RoomOpsPowerLevelsEvent,
|
RoomOpsPowerLevelsEvent,
|
||||||
|
RoomRedactionEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
|
@ -56,12 +57,13 @@ SCHEMAS = [
|
||||||
"presence",
|
"presence",
|
||||||
"im",
|
"im",
|
||||||
"room_aliases",
|
"room_aliases",
|
||||||
|
"redactions",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Remember to update this number every time an incompatible change is made to
|
# 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.
|
# database schema files, so the users will be informed on server restarts.
|
||||||
SCHEMA_VERSION = 3
|
SCHEMA_VERSION = 4
|
||||||
|
|
||||||
|
|
||||||
class _RollbackButIsFineException(Exception):
|
class _RollbackButIsFineException(Exception):
|
||||||
|
@ -182,6 +184,8 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||||
self._store_send_event_level(txn, event)
|
self._store_send_event_level(txn, event)
|
||||||
elif event.type == RoomOpsPowerLevelsEvent.TYPE:
|
elif event.type == RoomOpsPowerLevelsEvent.TYPE:
|
||||||
self._store_ops_level(txn, event)
|
self._store_ops_level(txn, event)
|
||||||
|
elif event.type == RoomRedactionEvent.TYPE:
|
||||||
|
self._store_redaction(txn, event)
|
||||||
|
|
||||||
vals = {
|
vals = {
|
||||||
"topological_ordering": event.depth,
|
"topological_ordering": event.depth,
|
||||||
|
@ -203,7 +207,7 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||||
unrec = {
|
unrec = {
|
||||||
k: v
|
k: v
|
||||||
for k, v in event.get_full_dict().items()
|
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)
|
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
|
@defer.inlineCallbacks
|
||||||
def get_current_state(self, room_id, event_type=None, state_key=""):
|
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 = (
|
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 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 "
|
"INNER JOIN state_events as s ON e.event_id = s.event_id "
|
||||||
"WHERE c.room_id = ? "
|
"WHERE c.room_id = ? "
|
||||||
)
|
) % {
|
||||||
|
"redacted": del_sql,
|
||||||
|
}
|
||||||
|
|
||||||
if event_type:
|
if event_type:
|
||||||
sql += " AND s.type = ? AND s.state_key = ? "
|
sql += " AND s.type = ? AND s.state_key = ? "
|
||||||
|
|
|
@ -17,6 +17,7 @@ import logging
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.errors import StoreError
|
from synapse.api.errors import StoreError
|
||||||
|
from synapse.api.events.utils import prune_event
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
@ -345,7 +346,7 @@ class SQLBaseStore(object):
|
||||||
return self.runInteraction(func)
|
return self.runInteraction(func)
|
||||||
|
|
||||||
def _parse_event_from_row(self, row_dict):
|
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("stream_ordering", None)
|
||||||
d.pop("topological_ordering", None)
|
d.pop("topological_ordering", None)
|
||||||
|
@ -373,8 +374,8 @@ class SQLBaseStore(object):
|
||||||
sql = "SELECT * FROM events WHERE event_id = ?"
|
sql = "SELECT * FROM events WHERE event_id = ?"
|
||||||
|
|
||||||
for ev in events:
|
for ev in events:
|
||||||
if hasattr(ev, "prev_state"):
|
if hasattr(ev, "prev_state"):
|
||||||
# Load previous state_content.
|
# Load previous state_content.
|
||||||
# TODO: Should we be pulling this out above?
|
# TODO: Should we be pulling this out above?
|
||||||
cursor = txn.execute(sql, (ev.prev_state,))
|
cursor = txn.execute(sql, (ev.prev_state,))
|
||||||
prevs = self.cursor_to_dict(cursor)
|
prevs = self.cursor_to_dict(cursor)
|
||||||
|
@ -382,8 +383,32 @@ class SQLBaseStore(object):
|
||||||
prev = self._parse_event_from_row(prevs[0])
|
prev = self._parse_event_from_row(prevs[0])
|
||||||
ev.prev_content = prev.content
|
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
|
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):
|
class Table(object):
|
||||||
""" A base class used to store information about a particular table.
|
""" A base class used to store information about a particular table.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -27,7 +27,7 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
class RoomStore(SQLBaseStore):
|
||||||
|
@ -189,7 +189,8 @@ class RoomStore(SQLBaseStore):
|
||||||
|
|
||||||
def _get_ops_levels(self, txn, room_id):
|
def _get_ops_levels(self, txn, room_id):
|
||||||
sql = (
|
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 "
|
"INNER JOIN current_state_events as c "
|
||||||
"ON r.event_id = c.event_id "
|
"ON r.event_id = c.event_id "
|
||||||
"WHERE c.room_id = ? "
|
"WHERE c.room_id = ? "
|
||||||
|
@ -198,7 +199,7 @@ class RoomStore(SQLBaseStore):
|
||||||
rows = txn.execute(sql, (room_id,)).fetchall()
|
rows = txn.execute(sql, (room_id,)).fetchall()
|
||||||
|
|
||||||
if len(rows) == 1:
|
if len(rows) == 1:
|
||||||
return OpsLevel(rows[0][0], rows[0][1])
|
return OpsLevel(rows[0][0], rows[0][1], rows[0][2])
|
||||||
else:
|
else:
|
||||||
return OpsLevel(None, None)
|
return OpsLevel(None, None)
|
||||||
|
|
||||||
|
@ -326,6 +327,9 @@ class RoomStore(SQLBaseStore):
|
||||||
if "ban_level" in event.content:
|
if "ban_level" in event.content:
|
||||||
content["ban_level"] = event.content["ban_level"]
|
content["ban_level"] = event.content["ban_level"]
|
||||||
|
|
||||||
|
if "redact_level" in event.content:
|
||||||
|
content["redact_level"] = event.content["redact_level"]
|
||||||
|
|
||||||
self._simple_insert_txn(
|
self._simple_insert_txn(
|
||||||
txn,
|
txn,
|
||||||
"room_ops_levels",
|
"room_ops_levels",
|
||||||
|
|
|
@ -182,14 +182,22 @@ class RoomMemberStore(SQLBaseStore):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_members_query_txn(self, txn, where_clause, where_values):
|
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 = (
|
sql = (
|
||||||
"SELECT e.* FROM events as e "
|
"SELECT e.*, (%(redacted)s) AS redacted FROM events as e "
|
||||||
"INNER JOIN room_memberships as m "
|
"INNER JOIN room_memberships as m "
|
||||||
"ON e.event_id = m.event_id "
|
"ON e.event_id = m.event_id "
|
||||||
"INNER JOIN current_state_events as c "
|
"INNER JOIN current_state_events as c "
|
||||||
"ON m.event_id = c.event_id "
|
"ON m.event_id = c.event_id "
|
||||||
"WHERE %s "
|
"WHERE %(where)s "
|
||||||
) % (where_clause,)
|
) % {
|
||||||
|
"redacted": del_sql,
|
||||||
|
"where": where_clause,
|
||||||
|
}
|
||||||
|
|
||||||
txn.execute(sql, where_values)
|
txn.execute(sql, where_values)
|
||||||
rows = self.cursor_to_dict(txn)
|
rows = self.cursor_to_dict(txn)
|
||||||
|
|
12
synapse/storage/schema/delta/v4.sql
Normal file
12
synapse/storage/schema/delta/v4.sql
Normal file
|
@ -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;
|
|
@ -150,7 +150,8 @@ CREATE TABLE IF NOT EXISTS room_ops_levels(
|
||||||
event_id TEXT NOT NULL,
|
event_id TEXT NOT NULL,
|
||||||
room_id TEXT NOT NULL,
|
room_id TEXT NOT NULL,
|
||||||
ban_level INTEGER,
|
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);
|
CREATE INDEX IF NOT EXISTS room_ops_levels_event_id ON room_ops_levels(event_id);
|
||||||
|
|
8
synapse/storage/schema/redactions.sql
Normal file
8
synapse/storage/schema/redactions.sql
Normal file
|
@ -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);
|
|
@ -157,6 +157,11 @@ class StreamStore(SQLBaseStore):
|
||||||
"WHERE m.user_id = ? "
|
"WHERE m.user_id = ? "
|
||||||
)
|
)
|
||||||
|
|
||||||
|
del_sql = (
|
||||||
|
"SELECT event_id FROM redactions WHERE redacts = e.event_id "
|
||||||
|
"LIMIT 1"
|
||||||
|
)
|
||||||
|
|
||||||
if limit:
|
if limit:
|
||||||
limit = max(limit, MAX_STREAM_SIZE)
|
limit = max(limit, MAX_STREAM_SIZE)
|
||||||
else:
|
else:
|
||||||
|
@ -171,13 +176,14 @@ class StreamStore(SQLBaseStore):
|
||||||
return
|
return
|
||||||
|
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT * FROM events as e WHERE "
|
"SELECT *, (%(redacted)s) AS redacted FROM events AS e WHERE "
|
||||||
"((room_id IN (%(current)s)) OR "
|
"((room_id IN (%(current)s)) OR "
|
||||||
"(event_id IN (%(invites)s))) "
|
"(event_id IN (%(invites)s))) "
|
||||||
"AND e.stream_ordering > ? AND e.stream_ordering <= ? "
|
"AND e.stream_ordering > ? AND e.stream_ordering <= ? "
|
||||||
"AND e.outlier = 0 "
|
"AND e.outlier = 0 "
|
||||||
"ORDER BY stream_ordering ASC LIMIT %(limit)d "
|
"ORDER BY stream_ordering ASC LIMIT %(limit)d "
|
||||||
) % {
|
) % {
|
||||||
|
"redacted": del_sql,
|
||||||
"current": current_room_membership_sql,
|
"current": current_room_membership_sql,
|
||||||
"invites": membership_sql,
|
"invites": membership_sql,
|
||||||
"limit": limit
|
"limit": limit
|
||||||
|
@ -224,11 +230,21 @@ class StreamStore(SQLBaseStore):
|
||||||
else:
|
else:
|
||||||
limit_str = ""
|
limit_str = ""
|
||||||
|
|
||||||
|
del_sql = (
|
||||||
|
"SELECT event_id FROM redactions WHERE redacts = events.event_id "
|
||||||
|
"LIMIT 1"
|
||||||
|
)
|
||||||
|
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT * FROM events "
|
"SELECT *, (%(redacted)s) AS redacted FROM events "
|
||||||
"WHERE outlier = 0 AND room_id = ? AND %(bounds)s "
|
"WHERE outlier = 0 AND room_id = ? AND %(bounds)s "
|
||||||
"ORDER BY topological_ordering %(order)s, stream_ordering %(order)s %(limit)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(
|
rows = yield self._execute_and_decode(
|
||||||
sql,
|
sql,
|
||||||
|
@ -257,11 +273,18 @@ class StreamStore(SQLBaseStore):
|
||||||
with_feedback=False):
|
with_feedback=False):
|
||||||
# TODO (erikj): Handle compressed feedback
|
# TODO (erikj): Handle compressed feedback
|
||||||
|
|
||||||
|
del_sql = (
|
||||||
|
"SELECT event_id FROM redactions WHERE redacts = events.event_id "
|
||||||
|
"LIMIT 1"
|
||||||
|
)
|
||||||
|
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT * FROM events "
|
"SELECT *, (%(redacted)s) AS redacted FROM events "
|
||||||
"WHERE room_id = ? AND stream_ordering <= ? "
|
"WHERE room_id = ? AND stream_ordering <= ? "
|
||||||
"ORDER BY topological_ordering DESC, stream_ordering DESC LIMIT ? "
|
"ORDER BY topological_ordering DESC, stream_ordering DESC LIMIT ? "
|
||||||
)
|
) % {
|
||||||
|
"redacted": del_sql,
|
||||||
|
}
|
||||||
|
|
||||||
rows = yield self._execute_and_decode(
|
rows = yield self._execute_and_decode(
|
||||||
sql,
|
sql,
|
||||||
|
|
262
tests/storage/test_redaction.py
Normal file
262
tests/storage/test_redaction.py
Normal file
|
@ -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,
|
||||||
|
)
|
|
@ -262,7 +262,7 @@ class MemoryDataStore(object):
|
||||||
return defer.succeed("invite")
|
return defer.succeed("invite")
|
||||||
|
|
||||||
def get_ops_levels(self, room_id):
|
def get_ops_levels(self, room_id):
|
||||||
return defer.succeed((5, 5))
|
return defer.succeed((5, 5, 5))
|
||||||
|
|
||||||
|
|
||||||
def _format_call(args, kwargs):
|
def _format_call(args, kwargs):
|
||||||
|
|
Loading…
Reference in a new issue