mirror of
https://github.com/element-hq/synapse
synced 2024-09-02 02:10:20 +00:00
Merge branch 'erikj/context_api' into erikj/search
This commit is contained in:
commit
2f6ad79a80
17 changed files with 354 additions and 21 deletions
17
CHANGES.rst
17
CHANGES.rst
|
@ -1,3 +1,20 @@
|
||||||
|
Changes in synapse v0.10.1-rc1 (2015-10-15)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Add support for CAS, thanks to Steven Hammerton (PR #295, #296)
|
||||||
|
* Add support for using macaroons for ``access_token`` (PR #256, #229)
|
||||||
|
* Add support for ``m.room.canonical_alias`` (PR #287)
|
||||||
|
* Add support for viewing the history of rooms that they have left. (PR #276,
|
||||||
|
#294)
|
||||||
|
* Add support for refresh tokens (PR #240)
|
||||||
|
* Add flag on creation which disables federation of the room (PR #279)
|
||||||
|
* Add some room state to invites. (PR #275)
|
||||||
|
* Atomically persist events when joining a room over federation (PR #283)
|
||||||
|
* Change default history visibility for private rooms (PR #271)
|
||||||
|
* Allow users to redact their own sent events (PR #262)
|
||||||
|
* Use tox for tests (PR #247)
|
||||||
|
* Split up syutil into separate libraries (PR #243)
|
||||||
|
|
||||||
Changes in synapse v0.10.0-r2 (2015-09-16)
|
Changes in synapse v0.10.0-r2 (2015-09-16)
|
||||||
==========================================
|
==========================================
|
||||||
|
|
||||||
|
|
|
@ -16,4 +16,4 @@
|
||||||
""" This is a reference implementation of a Matrix home server.
|
""" This is a reference implementation of a Matrix home server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.10.0-r2"
|
__version__ = "0.10.1-rc1"
|
||||||
|
|
|
@ -14,13 +14,14 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""This module contains classes for authenticating the user."""
|
"""This module contains classes for authenticating the user."""
|
||||||
|
from canonicaljson import encode_canonical_json
|
||||||
from signedjson.key import decode_verify_key_bytes
|
from signedjson.key import decode_verify_key_bytes
|
||||||
from signedjson.sign import verify_signed_json, SignatureVerifyException
|
from signedjson.sign import verify_signed_json, SignatureVerifyException
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes, Membership, JoinRules
|
from synapse.api.constants import EventTypes, Membership, JoinRules
|
||||||
from synapse.api.errors import AuthError, Codes, SynapseError
|
from synapse.api.errors import AuthError, Codes, SynapseError, EventSizeError
|
||||||
from synapse.types import RoomID, UserID, EventID
|
from synapse.types import RoomID, UserID, EventID
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
from synapse.util import third_party_invites
|
from synapse.util import third_party_invites
|
||||||
|
@ -64,6 +65,8 @@ class Auth(object):
|
||||||
Returns:
|
Returns:
|
||||||
True if the auth checks pass.
|
True if the auth checks pass.
|
||||||
"""
|
"""
|
||||||
|
self.check_size_limits(event)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not hasattr(event, "room_id"):
|
if not hasattr(event, "room_id"):
|
||||||
raise AuthError(500, "Event has no room_id: %s" % event)
|
raise AuthError(500, "Event has no room_id: %s" % event)
|
||||||
|
@ -131,6 +134,23 @@ class Auth(object):
|
||||||
logger.info("Denying! %s", event)
|
logger.info("Denying! %s", event)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def check_size_limits(self, event):
|
||||||
|
def too_big(field):
|
||||||
|
raise EventSizeError("%s too large" % (field,))
|
||||||
|
|
||||||
|
if len(event.user_id) > 255:
|
||||||
|
too_big("user_id")
|
||||||
|
if len(event.room_id) > 255:
|
||||||
|
too_big("room_id")
|
||||||
|
if event.is_state() and len(event.state_key) > 255:
|
||||||
|
too_big("state_key")
|
||||||
|
if len(event.type) > 255:
|
||||||
|
too_big("type")
|
||||||
|
if len(event.event_id) > 255:
|
||||||
|
too_big("event_id")
|
||||||
|
if len(encode_canonical_json(event.get_pdu_json())) > 65536:
|
||||||
|
too_big("event")
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def check_joined_room(self, room_id, user_id, current_state=None):
|
def check_joined_room(self, room_id, user_id, current_state=None):
|
||||||
"""Check if the user is currently joined in the room
|
"""Check if the user is currently joined in the room
|
||||||
|
|
|
@ -119,6 +119,15 @@ class AuthError(SynapseError):
|
||||||
super(AuthError, self).__init__(*args, **kwargs)
|
super(AuthError, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class EventSizeError(SynapseError):
|
||||||
|
"""An error raised when an event is too big."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if "errcode" not in kwargs:
|
||||||
|
kwargs["errcode"] = Codes.TOO_LARGE
|
||||||
|
super(EventSizeError, self).__init__(413, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class EventStreamError(SynapseError):
|
class EventStreamError(SynapseError):
|
||||||
"""An error raised when there a problem with the event stream."""
|
"""An error raised when there a problem with the event stream."""
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
|
@ -183,10 +183,29 @@ class Filter(object):
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the event matches
|
bool: True if the event matches
|
||||||
"""
|
"""
|
||||||
|
if isinstance(event, dict):
|
||||||
|
return self.check_fields(
|
||||||
|
event.get("room_id", None),
|
||||||
|
event.get("sender", None),
|
||||||
|
event.get("type", None),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return self.check_fields(
|
||||||
|
getattr(event, "room_id", None),
|
||||||
|
getattr(event, "sender", None),
|
||||||
|
event.type,
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_fields(self, room_id, sender, event_type):
|
||||||
|
"""Checks whether the filter matches the given event fields.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the event fields match
|
||||||
|
"""
|
||||||
literal_keys = {
|
literal_keys = {
|
||||||
"rooms": lambda v: event.room_id == v,
|
"rooms": lambda v: room_id == v,
|
||||||
"senders": lambda v: event.sender == v,
|
"senders": lambda v: sender == v,
|
||||||
"types": lambda v: _matches_wildcard(event.type, v)
|
"types": lambda v: _matches_wildcard(event_type, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, match_func in literal_keys.items():
|
for name, match_func in literal_keys.items():
|
||||||
|
|
|
@ -25,7 +25,7 @@ class CasConfig(Config):
|
||||||
def read_config(self, config):
|
def read_config(self, config):
|
||||||
cas_config = config.get("cas_config", None)
|
cas_config = config.get("cas_config", None)
|
||||||
if cas_config:
|
if cas_config:
|
||||||
self.cas_enabled = True
|
self.cas_enabled = cas_config.get("enabled", True)
|
||||||
self.cas_server_url = cas_config["server_url"]
|
self.cas_server_url = cas_config["server_url"]
|
||||||
self.cas_required_attributes = cas_config.get("required_attributes", {})
|
self.cas_required_attributes = cas_config.get("required_attributes", {})
|
||||||
else:
|
else:
|
||||||
|
@ -37,6 +37,7 @@ class CasConfig(Config):
|
||||||
return """
|
return """
|
||||||
# Enable CAS for registration and login.
|
# Enable CAS for registration and login.
|
||||||
#cas_config:
|
#cas_config:
|
||||||
|
# enabled: true
|
||||||
# server_url: "https://cas-server.com"
|
# server_url: "https://cas-server.com"
|
||||||
# #required_attributes:
|
# #required_attributes:
|
||||||
# # name: value
|
# # name: value
|
||||||
|
|
|
@ -27,12 +27,14 @@ from .appservice import AppServiceConfig
|
||||||
from .key import KeyConfig
|
from .key import KeyConfig
|
||||||
from .saml2 import SAML2Config
|
from .saml2 import SAML2Config
|
||||||
from .cas import CasConfig
|
from .cas import CasConfig
|
||||||
|
from .password import PasswordConfig
|
||||||
|
|
||||||
|
|
||||||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
||||||
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
||||||
VoipConfig, RegistrationConfig, MetricsConfig,
|
VoipConfig, RegistrationConfig, MetricsConfig,
|
||||||
AppServiceConfig, KeyConfig, SAML2Config, CasConfig):
|
AppServiceConfig, KeyConfig, SAML2Config, CasConfig,
|
||||||
|
PasswordConfig,):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
32
synapse/config/password.py
Normal file
32
synapse/config/password.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 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 PasswordConfig(Config):
|
||||||
|
"""Password login configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
def read_config(self, config):
|
||||||
|
password_config = config.get("password_config", {})
|
||||||
|
self.password_enabled = password_config.get("enabled", True)
|
||||||
|
|
||||||
|
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||||
|
return """
|
||||||
|
# Enable password for login.
|
||||||
|
password_config:
|
||||||
|
enabled: true
|
||||||
|
"""
|
|
@ -33,7 +33,7 @@ class SAML2Config(Config):
|
||||||
def read_config(self, config):
|
def read_config(self, config):
|
||||||
saml2_config = config.get("saml2_config", None)
|
saml2_config = config.get("saml2_config", None)
|
||||||
if saml2_config:
|
if saml2_config:
|
||||||
self.saml2_enabled = True
|
self.saml2_enabled = saml2_config.get("enabled", True)
|
||||||
self.saml2_config_path = saml2_config["config_path"]
|
self.saml2_config_path = saml2_config["config_path"]
|
||||||
self.saml2_idp_redirect_url = saml2_config["idp_redirect_url"]
|
self.saml2_idp_redirect_url = saml2_config["idp_redirect_url"]
|
||||||
else:
|
else:
|
||||||
|
@ -49,6 +49,7 @@ class SAML2Config(Config):
|
||||||
# the user back to /login/saml2 with proper info.
|
# the user back to /login/saml2 with proper info.
|
||||||
# See pysaml2 docs for format of config.
|
# See pysaml2 docs for format of config.
|
||||||
#saml2_config:
|
#saml2_config:
|
||||||
|
# enabled: true
|
||||||
# config_path: "%s/sp_conf.py"
|
# config_path: "%s/sp_conf.py"
|
||||||
# idp_redirect_url: "http://%s/idp"
|
# idp_redirect_url: "http://%s/idp"
|
||||||
""" % (config_dir_path, server_name)
|
""" % (config_dir_path, server_name)
|
||||||
|
|
|
@ -17,7 +17,7 @@ from synapse.appservice.scheduler import AppServiceScheduler
|
||||||
from synapse.appservice.api import ApplicationServiceApi
|
from synapse.appservice.api import ApplicationServiceApi
|
||||||
from .register import RegistrationHandler
|
from .register import RegistrationHandler
|
||||||
from .room import (
|
from .room import (
|
||||||
RoomCreationHandler, RoomMemberHandler, RoomListHandler
|
RoomCreationHandler, RoomMemberHandler, RoomListHandler, RoomContextHandler,
|
||||||
)
|
)
|
||||||
from .message import MessageHandler
|
from .message import MessageHandler
|
||||||
from .events import EventStreamHandler, EventHandler
|
from .events import EventStreamHandler, EventHandler
|
||||||
|
@ -70,3 +70,4 @@ class Handlers(object):
|
||||||
self.auth_handler = AuthHandler(hs)
|
self.auth_handler = AuthHandler(hs)
|
||||||
self.identity_handler = IdentityHandler(hs)
|
self.identity_handler = IdentityHandler(hs)
|
||||||
self.search_handler = SearchHandler(hs)
|
self.search_handler = SearchHandler(hs)
|
||||||
|
self.room_context_handler = RoomContextHandler(hs)
|
||||||
|
|
|
@ -156,13 +156,7 @@ class ReceiptsHandler(BaseHandler):
|
||||||
if not result:
|
if not result:
|
||||||
defer.returnValue([])
|
defer.returnValue([])
|
||||||
|
|
||||||
event = {
|
defer.returnValue(result)
|
||||||
"type": "m.receipt",
|
|
||||||
"room_id": room_id,
|
|
||||||
"content": result,
|
|
||||||
}
|
|
||||||
|
|
||||||
defer.returnValue([event])
|
|
||||||
|
|
||||||
|
|
||||||
class ReceiptEventSource(object):
|
class ReceiptEventSource(object):
|
||||||
|
|
|
@ -33,6 +33,7 @@ from collections import OrderedDict
|
||||||
from unpaddedbase64 import decode_base64
|
from unpaddedbase64 import decode_base64
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import string
|
import string
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -747,6 +748,60 @@ class RoomListHandler(BaseHandler):
|
||||||
defer.returnValue({"start": "START", "end": "END", "chunk": chunk})
|
defer.returnValue({"start": "START", "end": "END", "chunk": chunk})
|
||||||
|
|
||||||
|
|
||||||
|
class RoomContextHandler(BaseHandler):
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_event_context(self, user, room_id, event_id, limit):
|
||||||
|
"""Retrieves events, pagination tokens and state around a given event
|
||||||
|
in a room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (UserID)
|
||||||
|
room_id (str)
|
||||||
|
event_id (str)
|
||||||
|
limit (int): The maximum number of events to return in total
|
||||||
|
(excluding state).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict
|
||||||
|
"""
|
||||||
|
before_limit = math.floor(limit/2.)
|
||||||
|
after_limit = limit - before_limit
|
||||||
|
|
||||||
|
now_token = yield self.hs.get_event_sources().get_current_token()
|
||||||
|
|
||||||
|
results = yield self.store.get_events_around(
|
||||||
|
room_id, event_id, before_limit, after_limit
|
||||||
|
)
|
||||||
|
|
||||||
|
results["events_before"] = yield self._filter_events_for_client(
|
||||||
|
user.to_string(), results["events_before"]
|
||||||
|
)
|
||||||
|
|
||||||
|
results["events_after"] = yield self._filter_events_for_client(
|
||||||
|
user.to_string(), results["events_after"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if results["events_after"]:
|
||||||
|
last_event_id = results["events_after"][-1].event_id
|
||||||
|
else:
|
||||||
|
last_event_id = event_id
|
||||||
|
|
||||||
|
state = yield self.store.get_state_for_events(
|
||||||
|
[last_event_id], None
|
||||||
|
)
|
||||||
|
results["state"] = state[last_event_id].values()
|
||||||
|
|
||||||
|
results["start"] = now_token.copy_and_replace(
|
||||||
|
"room_key", results["start"]
|
||||||
|
).to_string()
|
||||||
|
|
||||||
|
results["end"] = now_token.copy_and_replace(
|
||||||
|
"room_key", results["end"]
|
||||||
|
).to_string()
|
||||||
|
|
||||||
|
defer.returnValue(results)
|
||||||
|
|
||||||
|
|
||||||
class RoomEventSource(object):
|
class RoomEventSource(object):
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
|
|
|
@ -43,6 +43,7 @@ class LoginRestServlet(ClientV1RestServlet):
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
super(LoginRestServlet, self).__init__(hs)
|
super(LoginRestServlet, self).__init__(hs)
|
||||||
self.idp_redirect_url = hs.config.saml2_idp_redirect_url
|
self.idp_redirect_url = hs.config.saml2_idp_redirect_url
|
||||||
|
self.password_enabled = hs.config.password_enabled
|
||||||
self.saml2_enabled = hs.config.saml2_enabled
|
self.saml2_enabled = hs.config.saml2_enabled
|
||||||
self.cas_enabled = hs.config.cas_enabled
|
self.cas_enabled = hs.config.cas_enabled
|
||||||
self.cas_server_url = hs.config.cas_server_url
|
self.cas_server_url = hs.config.cas_server_url
|
||||||
|
@ -50,11 +51,13 @@ class LoginRestServlet(ClientV1RestServlet):
|
||||||
self.servername = hs.config.server_name
|
self.servername = hs.config.server_name
|
||||||
|
|
||||||
def on_GET(self, request):
|
def on_GET(self, request):
|
||||||
flows = [{"type": LoginRestServlet.PASS_TYPE}]
|
flows = []
|
||||||
if self.saml2_enabled:
|
if self.saml2_enabled:
|
||||||
flows.append({"type": LoginRestServlet.SAML2_TYPE})
|
flows.append({"type": LoginRestServlet.SAML2_TYPE})
|
||||||
if self.cas_enabled:
|
if self.cas_enabled:
|
||||||
flows.append({"type": LoginRestServlet.CAS_TYPE})
|
flows.append({"type": LoginRestServlet.CAS_TYPE})
|
||||||
|
if self.password_enabled:
|
||||||
|
flows.append({"type": LoginRestServlet.PASS_TYPE})
|
||||||
return (200, {"flows": flows})
|
return (200, {"flows": flows})
|
||||||
|
|
||||||
def on_OPTIONS(self, request):
|
def on_OPTIONS(self, request):
|
||||||
|
@ -65,6 +68,9 @@ class LoginRestServlet(ClientV1RestServlet):
|
||||||
login_submission = _parse_json(request)
|
login_submission = _parse_json(request)
|
||||||
try:
|
try:
|
||||||
if login_submission["type"] == LoginRestServlet.PASS_TYPE:
|
if login_submission["type"] == LoginRestServlet.PASS_TYPE:
|
||||||
|
if not self.password_enabled:
|
||||||
|
raise SynapseError(400, "Password login has been disabled.")
|
||||||
|
|
||||||
result = yield self.do_password_login(login_submission)
|
result = yield self.do_password_login(login_submission)
|
||||||
defer.returnValue(result)
|
defer.returnValue(result)
|
||||||
elif self.saml2_enabled and (login_submission["type"] ==
|
elif self.saml2_enabled and (login_submission["type"] ==
|
||||||
|
|
|
@ -397,6 +397,41 @@ class RoomTriggerBackfill(ClientV1RestServlet):
|
||||||
defer.returnValue((200, res))
|
defer.returnValue((200, res))
|
||||||
|
|
||||||
|
|
||||||
|
class RoomEventContext(ClientV1RestServlet):
|
||||||
|
PATTERN = client_path_pattern(
|
||||||
|
"/rooms/(?P<room_id>[^/]*)/context/(?P<event_id>[^/]*)$"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(RoomEventContext, self).__init__(hs)
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_GET(self, request, room_id, event_id):
|
||||||
|
user, _ = yield self.auth.get_user_by_req(request)
|
||||||
|
|
||||||
|
limit = int(request.args.get("limit", [10])[0])
|
||||||
|
|
||||||
|
results = yield self.handlers.room_context_handler.get_event_context(
|
||||||
|
user, room_id, event_id, limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
time_now = self.clock.time_msec()
|
||||||
|
results["events_before"] = [
|
||||||
|
serialize_event(event, time_now) for event in results["events_before"]
|
||||||
|
]
|
||||||
|
results["events_after"] = [
|
||||||
|
serialize_event(event, time_now) for event in results["events_after"]
|
||||||
|
]
|
||||||
|
results["state"] = [
|
||||||
|
serialize_event(event, time_now) for event in results["state"]
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info("Responding with %r", results)
|
||||||
|
|
||||||
|
defer.returnValue((200, results))
|
||||||
|
|
||||||
|
|
||||||
# TODO: Needs unit testing
|
# TODO: Needs unit testing
|
||||||
class RoomMembershipRestServlet(ClientV1RestServlet):
|
class RoomMembershipRestServlet(ClientV1RestServlet):
|
||||||
|
|
||||||
|
@ -628,3 +663,4 @@ def register_servlets(hs, http_server):
|
||||||
RoomRedactEventRestServlet(hs).register(http_server)
|
RoomRedactEventRestServlet(hs).register(http_server)
|
||||||
RoomTypingRestServlet(hs).register(http_server)
|
RoomTypingRestServlet(hs).register(http_server)
|
||||||
SearchRestServlet(hs).register(http_server)
|
SearchRestServlet(hs).register(http_server)
|
||||||
|
RoomEventContext(hs).register(http_server)
|
||||||
|
|
|
@ -23,7 +23,7 @@ paginate bacwards.
|
||||||
|
|
||||||
This is implemented by keeping two ordering columns: stream_ordering and
|
This is implemented by keeping two ordering columns: stream_ordering and
|
||||||
topological_ordering. Stream ordering is basically insertion/received order
|
topological_ordering. Stream ordering is basically insertion/received order
|
||||||
(except for events from backfill requests). The topolgical_ordering is a
|
(except for events from backfill requests). The topological_ordering is a
|
||||||
weak ordering of events based on the pdu graph.
|
weak ordering of events based on the pdu graph.
|
||||||
|
|
||||||
This means that we have to have two different types of tokens, depending on
|
This means that we have to have two different types of tokens, depending on
|
||||||
|
@ -436,3 +436,138 @@ class StreamStore(SQLBaseStore):
|
||||||
internal = event.internal_metadata
|
internal = event.internal_metadata
|
||||||
internal.before = str(RoomStreamToken(topo, stream - 1))
|
internal.before = str(RoomStreamToken(topo, stream - 1))
|
||||||
internal.after = str(RoomStreamToken(topo, stream))
|
internal.after = str(RoomStreamToken(topo, stream))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_events_around(self, room_id, event_id, before_limit, after_limit):
|
||||||
|
"""Retrieve events and pagination tokens around a given event in a
|
||||||
|
room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room_id (str)
|
||||||
|
event_id (str)
|
||||||
|
before_limit (int)
|
||||||
|
after_limit (int)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
results = yield self.runInteraction(
|
||||||
|
"get_events_around", self._get_events_around_txn,
|
||||||
|
room_id, event_id, before_limit, after_limit
|
||||||
|
)
|
||||||
|
|
||||||
|
events_before = yield self._get_events(
|
||||||
|
[e for e in results["before"]["event_ids"]],
|
||||||
|
get_prev_content=True
|
||||||
|
)
|
||||||
|
|
||||||
|
events_after = yield self._get_events(
|
||||||
|
[e for e in results["after"]["event_ids"]],
|
||||||
|
get_prev_content=True
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue({
|
||||||
|
"events_before": events_before,
|
||||||
|
"events_after": events_after,
|
||||||
|
"start": results["before"]["token"],
|
||||||
|
"end": results["after"]["token"],
|
||||||
|
})
|
||||||
|
|
||||||
|
def _get_events_around_txn(self, txn, room_id, event_id, before_limit, after_limit):
|
||||||
|
"""Retrieves event_ids and pagination tokens around a given event in a
|
||||||
|
room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room_id (str)
|
||||||
|
event_id (str)
|
||||||
|
before_limit (int)
|
||||||
|
after_limit (int)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
results = self._simple_select_one_txn(
|
||||||
|
txn,
|
||||||
|
"events",
|
||||||
|
keyvalues={
|
||||||
|
"event_id": event_id,
|
||||||
|
"room_id": room_id,
|
||||||
|
},
|
||||||
|
retcols=["stream_ordering", "topological_ordering"],
|
||||||
|
)
|
||||||
|
|
||||||
|
stream_ordering = results["stream_ordering"]
|
||||||
|
topological_ordering = results["topological_ordering"]
|
||||||
|
|
||||||
|
query_before = (
|
||||||
|
"SELECT topological_ordering, stream_ordering, event_id FROM events"
|
||||||
|
" WHERE room_id = ? AND (topological_ordering < ?"
|
||||||
|
" OR (topological_ordering = ? AND stream_ordering < ?))"
|
||||||
|
" ORDER BY topological_ordering DESC, stream_ordering DESC"
|
||||||
|
" LIMIT ?"
|
||||||
|
)
|
||||||
|
|
||||||
|
query_after = (
|
||||||
|
"SELECT topological_ordering, stream_ordering, event_id FROM events"
|
||||||
|
" WHERE room_id = ? AND (topological_ordering > ?"
|
||||||
|
" OR (topological_ordering = ? AND stream_ordering > ?))"
|
||||||
|
" ORDER BY topological_ordering ASC, stream_ordering ASC"
|
||||||
|
" LIMIT ?"
|
||||||
|
)
|
||||||
|
|
||||||
|
txn.execute(
|
||||||
|
query_before,
|
||||||
|
(
|
||||||
|
room_id, topological_ordering, topological_ordering,
|
||||||
|
stream_ordering, before_limit,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = self.cursor_to_dict(txn)
|
||||||
|
events_before = [r["event_id"] for r in rows]
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
start_token = str(RoomStreamToken(
|
||||||
|
rows[0]["topological_ordering"],
|
||||||
|
rows[0]["stream_ordering"] - 1,
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
start_token = str(RoomStreamToken(
|
||||||
|
topological_ordering,
|
||||||
|
stream_ordering - 1,
|
||||||
|
))
|
||||||
|
|
||||||
|
txn.execute(
|
||||||
|
query_after,
|
||||||
|
(
|
||||||
|
room_id, topological_ordering, topological_ordering,
|
||||||
|
stream_ordering, after_limit,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = self.cursor_to_dict(txn)
|
||||||
|
events_after = [r["event_id"] for r in rows]
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
end_token = str(RoomStreamToken(
|
||||||
|
rows[-1]["topological_ordering"],
|
||||||
|
rows[-1]["stream_ordering"],
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
end_token = str(RoomStreamToken(
|
||||||
|
topological_ordering,
|
||||||
|
stream_ordering,
|
||||||
|
))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"before": {
|
||||||
|
"event_ids": events_before,
|
||||||
|
"token": start_token,
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"event_ids": events_after,
|
||||||
|
"token": end_token,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ class DomainSpecificString(
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_string(cls, s):
|
def from_string(cls, s):
|
||||||
"""Parse the string given by 's' into a structure object."""
|
"""Parse the string given by 's' into a structure object."""
|
||||||
if s[0] != cls.SIGIL:
|
if len(s) < 1 or s[0] != cls.SIGIL:
|
||||||
raise SynapseError(400, "Expected %s string to start with '%s'" % (
|
raise SynapseError(400, "Expected %s string to start with '%s'" % (
|
||||||
cls.__name__, cls.SIGIL,
|
cls.__name__, cls.SIGIL,
|
||||||
))
|
))
|
||||||
|
|
|
@ -15,13 +15,14 @@
|
||||||
|
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
|
|
||||||
|
from synapse.api.errors import SynapseError
|
||||||
from synapse.server import BaseHomeServer
|
from synapse.server import BaseHomeServer
|
||||||
from synapse.types import UserID, RoomAlias
|
from synapse.types import UserID, RoomAlias
|
||||||
|
|
||||||
mock_homeserver = BaseHomeServer(hostname="my.domain")
|
mock_homeserver = BaseHomeServer(hostname="my.domain")
|
||||||
|
|
||||||
class UserIDTestCase(unittest.TestCase):
|
|
||||||
|
|
||||||
|
class UserIDTestCase(unittest.TestCase):
|
||||||
def test_parse(self):
|
def test_parse(self):
|
||||||
user = UserID.from_string("@1234abcd:my.domain")
|
user = UserID.from_string("@1234abcd:my.domain")
|
||||||
|
|
||||||
|
@ -29,6 +30,11 @@ class UserIDTestCase(unittest.TestCase):
|
||||||
self.assertEquals("my.domain", user.domain)
|
self.assertEquals("my.domain", user.domain)
|
||||||
self.assertEquals(True, mock_homeserver.is_mine(user))
|
self.assertEquals(True, mock_homeserver.is_mine(user))
|
||||||
|
|
||||||
|
def test_pase_empty(self):
|
||||||
|
with self.assertRaises(SynapseError):
|
||||||
|
UserID.from_string("")
|
||||||
|
|
||||||
|
|
||||||
def test_build(self):
|
def test_build(self):
|
||||||
user = UserID("5678efgh", "my.domain")
|
user = UserID("5678efgh", "my.domain")
|
||||||
|
|
||||||
|
@ -44,7 +50,6 @@ class UserIDTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
class RoomAliasTestCase(unittest.TestCase):
|
class RoomAliasTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def test_parse(self):
|
def test_parse(self):
|
||||||
room = RoomAlias.from_string("#channel:my.domain")
|
room = RoomAlias.from_string("#channel:my.domain")
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue