diff --git a/README.rst b/README.rst
index cfdc2a1c75..9c44bf61b8 100644
--- a/README.rst
+++ b/README.rst
@@ -173,7 +173,11 @@ IDs:
For the first form, simply pass the required hostname (of the machine) as the
--host parameter::
- $ python synapse/app/homeserver.py --host machine.my.domain.name
+ $ python synapse/app/homeserver.py \
+ --server-name machine.my.domain.name \
+ --config-path homeserver.config \
+ --generate-config
+ $ python synapse/app/homeserver.py --config-path homeserver.config
For the second form, first create your SRV record and publish it in DNS. This
needs to be named _matrix._tcp.YOURDOMAIN, and point at at least one hostname
@@ -186,7 +190,13 @@ record would then look something like::
At this point, you should then run the homeserver with the hostname of this
SRV record, as that is the name other machines will expect it to have::
- $ python synapse/app/homeserver.py --host my.domain.name --port 8448
+ $ python synapse/app/homeserver.py \
+ --server-name YOURDOMAIN \
+ --bind-port 8448 \
+ --config-path homeserver.config \
+ --generate-config
+ $ python synapse/app/homeserver.py --config-path homeserver.config
+
You may additionally want to pass one or more "-v" options, in order to
increase the verbosity of logging output; at least for initial testing.
diff --git a/demo/demo.tls.dh b/demo/demo.tls.dh
new file mode 100644
index 0000000000..cbc58272a0
--- /dev/null
+++ b/demo/demo.tls.dh
@@ -0,0 +1,9 @@
+2048-bit DH parameters taken from rfc3526
+-----BEGIN DH PARAMETERS-----
+MIIBCAKCAQEA///////////JD9qiIWjCNMTGYouA3BzRKQJOCIpnzHQCC76mOxOb
+IlFKCHmONATd75UZs806QxswKwpt8l8UN0/hNW1tUcJF5IW1dmJefsb0TELppjft
+awv/XLb0Brft7jhr+1qJn6WunyQRfEsf5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT
+mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVSu57VKQdwlpZtZww1Tkq8mATxdGwIyhgh
+fDKQXkYuNs474553LBgOhgObJ4Oi7Aeij7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq
+5RXSJhiY+gUQFXKOWoqsqmj//////////wIBAg==
+-----END DH PARAMETERS-----
diff --git a/demo/start.sh b/demo/start.sh
index 1e591aabb8..56a1344344 100755
--- a/demo/start.sh
+++ b/demo/start.sh
@@ -6,17 +6,27 @@ CWD=$(pwd)
cd "$DIR/.."
+mkdir -p demo/etc
+
for port in 8080 8081 8082; do
echo "Starting server on port $port... "
python -m synapse.app.homeserver \
+ --generate-config \
+ --config-path "demo/etc/$port.config" \
+ -H "localhost:$port" \
-p "$port" \
-H "localhost:$port" \
-f "$DIR/$port.log" \
-d "$DIR/$port.db" \
- -vv \
-D --pid-file "$DIR/$port.pid" \
- --manhole $((port + 1000))
+ --manhole $((port + 1000)) \
+ --tls-dh-params-path "demo/demo.tls.dh"
+
+ python -m synapse.app.homeserver \
+ --config-path "demo/etc/$port.config" \
+ -vv \
+
done
echo "Starting webclient on port 8000..."
diff --git a/setup.py b/setup.py
index 59ac4fbf94..0eec5c9354 100755
--- a/setup.py
+++ b/setup.py
@@ -34,6 +34,7 @@ setup(
"syutil==0.0.1",
"Twisted>=14.0.0",
"service_identity>=1.0.0",
+ "pyyaml",
"pyasn1",
"pynacl",
"daemonize",
diff --git a/synapse/api/auth.py b/synapse/api/auth.py
index 2473a2b2bb..abd7d73b0a 100644
--- a/synapse/api/auth.py
+++ b/synapse/api/auth.py
@@ -17,9 +17,10 @@
from twisted.internet import defer
-from synapse.api.constants import Membership
+from synapse.api.constants import Membership, JoinRules
from synapse.api.errors import AuthError, StoreError, Codes
from synapse.api.events.room import RoomMemberEvent
+from synapse.util.logutils import log_function
import logging
@@ -44,16 +45,29 @@ class Auth(object):
"""
try:
if hasattr(event, "room_id"):
+ is_state = hasattr(event, "state_key")
+
if event.type == RoomMemberEvent.TYPE:
+ yield self._can_replace_state(event)
allowed = yield self.is_membership_change_allowed(event)
defer.returnValue(allowed)
+ return
+
+ self._check_joined_room(
+ member=snapshot.membership_state,
+ user_id=snapshot.user_id,
+ room_id=snapshot.room_id,
+ )
+
+ if is_state:
+ # TODO (erikj): This really only should be called for *new*
+ # state
+ yield self._can_add_state(event)
+ yield self._can_replace_state(event)
else:
- self._check_joined_room(
- member=snapshot.membership_state,
- user_id=snapshot.user_id,
- room_id=snapshot.room_id,
- )
- defer.returnValue(True)
+ yield self._can_send_event(event)
+
+ defer.returnValue(True)
else:
raise AuthError(500, "Unknown event: %s" % event)
except AuthError as e:
@@ -111,7 +125,14 @@ class Auth(object):
membership = event.content["membership"]
+ join_rule = yield self.store.get_room_join_rule(event.room_id)
+ if not join_rule:
+ join_rule = JoinRules.INVITE
+
if Membership.INVITE == membership:
+ # TODO (erikj): We should probably handle this more intelligently
+ # PRIVATE join rules.
+
# Invites are valid iff caller is in the room and target isn't.
if not caller_in_room: # caller isn't joined
raise AuthError(403, "You are not in room %s." % event.room_id)
@@ -124,18 +145,42 @@ class Auth(object):
# joined: It's a NOOP
if event.user_id != target_user_id:
raise AuthError(403, "Cannot force another user to join.")
- elif room.is_public:
- pass # anyone can join public rooms.
- elif (not caller or caller.membership not in
- [Membership.INVITE, Membership.JOIN]):
- raise AuthError(403, "You are not invited to this room.")
+ elif join_rule == JoinRules.PUBLIC or room.is_public:
+ pass
+ elif join_rule == JoinRules.INVITE:
+ if (
+ not caller or caller.membership not in
+ [Membership.INVITE, Membership.JOIN]
+ ):
+ raise AuthError(403, "You are not invited to this room.")
+ else:
+ # TODO (erikj): may_join list
+ # TODO (erikj): private rooms
+ raise AuthError(403, "You are not allowed to join this room")
elif Membership.LEAVE == membership:
+ # TODO (erikj): Implement kicks.
+
if not caller_in_room: # trying to leave a room you aren't joined
raise AuthError(403, "You are not in room %s." % event.room_id)
elif target_user_id != event.user_id:
# trying to force another user to leave
raise AuthError(403, "Cannot force %s to leave." %
target_user_id)
+ elif Membership.BAN == membership:
+ user_level = yield self.store.get_power_level(
+ event.room_id,
+ event.user_id,
+ )
+
+ ban_level, _ = yield self.store.get_ops_levels(event.room_id)
+
+ if ban_level:
+ ban_level = int(ban_level)
+ else:
+ ban_level = 5 # FIXME (erikj): What should we do here?
+
+ if user_level < ban_level:
+ raise AuthError(403, "You don't have permission to ban")
else:
raise AuthError(500, "Unknown membership %s" % membership)
@@ -176,3 +221,85 @@ class Auth(object):
except StoreError:
raise AuthError(403, "Unrecognised access token.",
errcode=Codes.UNKNOWN_TOKEN)
+
+ @defer.inlineCallbacks
+ @log_function
+ def _can_send_event(self, event):
+ send_level = yield self.store.get_send_event_level(event.room_id)
+
+ if send_level:
+ send_level = int(send_level)
+ else:
+ send_level = 0
+
+ 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
+
+ if user_level < send_level:
+ raise AuthError(
+ 403, "You don't have permission to post to the room"
+ )
+
+ defer.returnValue(True)
+
+ @defer.inlineCallbacks
+ def _can_add_state(self, event):
+ add_level = yield self.store.get_add_state_level(event.room_id)
+
+ if not add_level:
+ defer.returnValue(True)
+
+ add_level = int(add_level)
+
+ user_level = yield self.store.get_power_level(
+ event.room_id,
+ event.user_id,
+ )
+
+ user_level = int(user_level)
+
+ if user_level < add_level:
+ raise AuthError(
+ 403, "You don't have permission to add state to the room"
+ )
+
+ defer.returnValue(True)
+
+ @defer.inlineCallbacks
+ def _can_replace_state(self, event):
+ current_state = yield self.store.get_current_state(
+ event.room_id,
+ event.type,
+ event.state_key,
+ )
+
+ if current_state:
+ current_state = current_state[0]
+
+ 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
+
+ logger.debug("Checking power level for %s, %s", event.user_id, user_level)
+ if current_state and hasattr(current_state, "required_power_level"):
+ req = current_state.required_power_level
+
+ logger.debug("Checked power level for %s, %s", event.user_id, req)
+ if user_level < req:
+ raise AuthError(
+ 403,
+ "You don't have permission to change that state"
+ )
diff --git a/synapse/api/constants.py b/synapse/api/constants.py
index f69f2445a2..668ffa07ca 100644
--- a/synapse/api/constants.py
+++ b/synapse/api/constants.py
@@ -23,7 +23,8 @@ class Membership(object):
JOIN = u"join"
KNOCK = u"knock"
LEAVE = u"leave"
- LIST = (INVITE, JOIN, KNOCK, LEAVE)
+ BAN = u"ban"
+ LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN)
class Feedback(object):
@@ -42,3 +43,10 @@ class PresenceState(object):
UNAVAILABLE = u"unavailable"
ONLINE = u"online"
FREE_FOR_CHAT = u"free_for_chat"
+
+
+class JoinRules(object):
+ PUBLIC = u"public"
+ KNOCK = u"knock"
+ INVITE = u"invite"
+ PRIVATE = u"private"
diff --git a/synapse/api/events/__init__.py b/synapse/api/events/__init__.py
index f9653e0b2a..9502f5df8f 100644
--- a/synapse/api/events/__init__.py
+++ b/synapse/api/events/__init__.py
@@ -42,6 +42,7 @@ class SynapseEvent(JsonEncodedObject):
"user_id", # sender/initiator
"content", # HTTP body, JSON
"state_key",
+ "required_power_level",
]
internal_keys = [
@@ -52,6 +53,7 @@ class SynapseEvent(JsonEncodedObject):
"destinations",
"origin",
"outlier",
+ "power_level",
]
required_keys = [
@@ -152,3 +154,10 @@ class SynapseEvent(JsonEncodedObject):
msg = self._check_json(entry, template[key][0])
if msg:
return msg
+
+
+class SynapseStateEvent(SynapseEvent):
+ def __init__(self, **kwargs):
+ if "state_key" not in kwargs:
+ kwargs["state_key"] = ""
+ super(SynapseStateEvent, self).__init__(**kwargs)
diff --git a/synapse/api/events/factory.py b/synapse/api/events/factory.py
index c2cdcddf41..159728b2d2 100644
--- a/synapse/api/events/factory.py
+++ b/synapse/api/events/factory.py
@@ -16,6 +16,8 @@
from synapse.api.events.room import (
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
+ RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomOpsPowerLevelsEvent,
+ RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent
)
from synapse.util.stringutils import random_string
@@ -30,7 +32,13 @@ class EventFactory(object):
RoomMemberEvent,
FeedbackEvent,
InviteJoinEvent,
- RoomConfigEvent
+ RoomConfigEvent,
+ RoomPowerLevelsEvent,
+ RoomJoinRulesEvent,
+ RoomCreateEvent,
+ RoomAddStateLevelEvent,
+ RoomSendEventLevelEvent,
+ RoomOpsPowerLevelsEvent,
]
def __init__(self, hs):
diff --git a/synapse/api/events/room.py b/synapse/api/events/room.py
index 9faad57ac0..f6d3c59a9a 100644
--- a/synapse/api/events/room.py
+++ b/synapse/api/events/room.py
@@ -15,7 +15,7 @@
from synapse.api.constants import Feedback, Membership
from synapse.api.errors import SynapseError
-from . import SynapseEvent
+from . import SynapseEvent, SynapseStateEvent
class GenericEvent(SynapseEvent):
@@ -132,3 +132,45 @@ class RoomConfigEvent(SynapseEvent):
def get_content_template(self):
return {}
+
+
+class RoomCreateEvent(SynapseStateEvent):
+ TYPE = "m.room.create"
+
+ def get_content_template(self):
+ return {}
+
+
+class RoomJoinRulesEvent(SynapseStateEvent):
+ TYPE = "m.room.join_rules"
+
+ def get_content_template(self):
+ return {}
+
+
+class RoomPowerLevelsEvent(SynapseStateEvent):
+ TYPE = "m.room.power_levels"
+
+ def get_content_template(self):
+ return {}
+
+
+class RoomAddStateLevelEvent(SynapseStateEvent):
+ TYPE = "m.room.add_state_level"
+
+ def get_content_template(self):
+ return {}
+
+
+class RoomSendEventLevelEvent(SynapseStateEvent):
+ TYPE = "m.room.send_event_level"
+
+ def get_content_template(self):
+ return {}
+
+
+class RoomOpsPowerLevelsEvent(SynapseStateEvent):
+ TYPE = "m.room.ops_levels"
+
+ def get_content_template(self):
+ return {}
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index 6d292ccf9a..606c9c650d 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -20,7 +20,6 @@ from synapse.server import HomeServer
from twisted.internet import reactor
from twisted.enterprise import adbapi
-from twisted.python.log import PythonLoggingObserver
from twisted.web.resource import Resource
from twisted.web.static import File
from twisted.web.server import Site
@@ -29,16 +28,17 @@ from synapse.http.client import TwistedHttpClient
from synapse.api.urls import (
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX
)
+from synapse.config.homeserver import HomeServerConfig
+from synapse.crypto import context_factory
from daemonize import Daemonize
import twisted.manhole.telnet
-import argparse
import logging
-import logging.config
import sqlite3
import os
import re
+import sys
logger = logging.getLogger(__name__)
@@ -56,13 +56,13 @@ SCHEMAS = [
# 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 = 1
+SCHEMA_VERSION = 2
class SynapseHomeServer(HomeServer):
def build_http_client(self):
- return TwistedHttpClient()
+ return TwistedHttpClient(self)
def build_resource_for_client(self):
return JsonResource()
@@ -206,37 +206,17 @@ class SynapseHomeServer(HomeServer):
"""
return "%s-%s" % (resource, path_seg)
- def start_listening(self, port):
- reactor.listenTCP(port, Site(self.root_resource))
- logger.info("Synapse now listening on port %d", port)
-
-
-def setup_logging(verbosity=0, filename=None, config_path=None):
- """ Sets up logging with verbosity levels.
-
- Args:
- verbosity: The verbosity level.
- filename: Log to the given file rather than to the console.
- config_path: Path to a python logging config file.
- """
-
- if config_path is None:
- log_format = (
- '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s'
- )
-
- level = logging.INFO
- if verbosity:
- level = logging.DEBUG
-
- # FIXME: we need a logging.WARN for a -q quiet option
-
- logging.basicConfig(level=level, filename=filename, format=log_format)
- else:
- logging.config.fileConfig(config_path)
-
- observer = PythonLoggingObserver()
- observer.start()
+ def start_listening(self, secure_port, unsecure_port):
+ if secure_port is not None:
+ reactor.listenSSL(
+ secure_port, Site(self.root_resource), self.tls_context_factory
+ )
+ logger.info("Synapse now listening on port %d", secure_port)
+ if unsecure_port is not None:
+ reactor.listenTCP(
+ unsecure_port, Site(self.root_resource)
+ )
+ logger.info("Synapse now listening on port %d", unsecure_port)
def run():
@@ -244,78 +224,53 @@ def run():
def setup():
- parser = argparse.ArgumentParser()
- parser.add_argument("-p", "--port", dest="port", type=int, default=8080,
- help="The port to listen on.")
- parser.add_argument("-d", "--database", dest="db", default="homeserver.db",
- help="The database name.")
- parser.add_argument("-H", "--host", dest="host", default="localhost",
- help="The hostname of the server.")
- parser.add_argument('-v', '--verbose', dest="verbose", action='count',
- help="The verbosity level.")
- parser.add_argument('-f', '--log-file', dest="log_file", default=None,
- help="File to log to.")
- parser.add_argument('--log-config', dest="log_config", default=None,
- help="Python logging config")
- parser.add_argument('-D', '--daemonize', action='store_true',
- default=False, help="Daemonize the home server")
- parser.add_argument('--pid-file', dest="pid", help="When running as a "
- "daemon, the file to store the pid in",
- default="hs.pid")
- parser.add_argument("-W", "--webclient", dest="webclient", default=True,
- action="store_false", help="Don't host a web client.")
- parser.add_argument("--manhole", dest="manhole", type=int, default=None,
- help="Turn on the twisted telnet manhole service.")
- args = parser.parse_args()
-
- verbosity = int(args.verbose) if args.verbose else None
-
- # Because if/when we daemonize we change to root dir.
- db_name = os.path.abspath(args.db)
- log_file = args.log_file
- if log_file:
- log_file = os.path.abspath(log_file)
-
- setup_logging(
- verbosity=verbosity,
- filename=log_file,
- config_path=args.log_config,
+ config = HomeServerConfig.load_config(
+ "Synapse Homeserver",
+ sys.argv[1:],
+ generate_section="Homeserver"
)
- logger.info("Server hostname: %s", args.host)
+ config.setup_logging()
- if re.search(":[0-9]+$", args.host):
- domain_with_port = args.host
+ logger.info("Server hostname: %s", config.server_name)
+
+ if re.search(":[0-9]+$", config.server_name):
+ domain_with_port = config.server_name
else:
- domain_with_port = "%s:%s" % (args.host, args.port)
+ domain_with_port = "%s:%s" % (config.server_name, config.bind_port)
+
+ tls_context_factory = context_factory.ServerContextFactory(config)
hs = SynapseHomeServer(
- args.host,
+ config.server_name,
domain_with_port=domain_with_port,
upload_dir=os.path.abspath("uploads"),
- db_name=db_name,
+ db_name=config.database_path,
+ tls_context_factory=tls_context_factory,
)
hs.register_servlets()
hs.create_resource_tree(
- web_client=args.webclient,
- redirect_root_to_web_client=True)
- hs.start_listening(args.port)
+ web_client=config.webclient,
+ redirect_root_to_web_client=True,
+ )
+ hs.start_listening(config.bind_port, config.unsecure_port)
hs.get_db_pool()
- if args.manhole:
+ if config.manhole:
f = twisted.manhole.telnet.ShellFactory()
f.username = "matrix"
f.password = "rabbithole"
f.namespace['hs'] = hs
- reactor.listenTCP(args.manhole, f, interface='127.0.0.1')
+ reactor.listenTCP(config.manhole, f, interface='127.0.0.1')
- if args.daemonize:
+ if config.daemonize:
+ print config.pid_file
daemon = Daemonize(
app="synapse-homeserver",
- pid=args.pid,
+ pid=config.pid_file,
action=run,
auto_close_fds=False,
verbose=True,
diff --git a/synapse/config/__init__.py b/synapse/config/__init__.py
new file mode 100644
index 0000000000..fe8a073cd3
--- /dev/null
+++ b/synapse/config/__init__.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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.
diff --git a/synapse/config/_base.py b/synapse/config/_base.py
new file mode 100644
index 0000000000..08de6ee5ec
--- /dev/null
+++ b/synapse/config/_base.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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.
+
+
+import ConfigParser as configparser
+import argparse
+import sys
+import os
+import yaml
+
+
+class Config(object):
+ def __init__(self, args):
+ pass
+
+ @staticmethod
+ def abspath(file_path):
+ return os.path.abspath(file_path) if file_path else file_path
+
+ @staticmethod
+ def read_file(file_path):
+ with open(file_path) as file_stream:
+ return file_stream.read()
+
+ @staticmethod
+ def read_config_file(file_path):
+ with open(file_path) as file_stream:
+ return yaml.load(file_stream)
+
+ @classmethod
+ def add_arguments(cls, parser):
+ pass
+
+ @classmethod
+ def generate_config(cls, args, config_dir_path):
+ pass
+
+ @classmethod
+ def load_config(cls, description, argv, generate_section=None):
+ config_parser = argparse.ArgumentParser(add_help=False)
+ config_parser.add_argument(
+ "-c", "--config-path",
+ metavar="CONFIG_FILE",
+ help="Specify config file"
+ )
+ config_parser.add_argument(
+ "--generate-config",
+ action="store_true",
+ help="Generate config file"
+ )
+ config_args, remaining_args = config_parser.parse_known_args(argv)
+
+ if config_args.generate_config:
+ if not config_args.config_path:
+ config_parser.error(
+ "Must specify where to generate the config file"
+ )
+ config_dir_path = os.path.dirname(config_args.config_path)
+ if os.path.exists(config_args.config_path):
+ defaults = cls.read_config_file(config_args.config_path)
+ else:
+ defaults = {}
+ else:
+ if config_args.config_path:
+ defaults = cls.read_config_file(config_args.config_path)
+ else:
+ defaults = {}
+
+ parser = argparse.ArgumentParser(
+ parents=[config_parser],
+ description=description,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ cls.add_arguments(parser)
+ parser.set_defaults(**defaults)
+
+ args = parser.parse_args(remaining_args)
+
+ if config_args.generate_config:
+ config_dir_path = os.path.dirname(config_args.config_path)
+ config_dir_path = os.path.abspath(config_dir_path)
+ cls.generate_config(args, config_dir_path)
+ config = {}
+ for key, value in vars(args).items():
+ if (key not in set(["config_path", "generate_config"])
+ and value is not None):
+ config[key] = value
+ with open(config_args.config_path, "w") as config_file:
+ yaml.dump(config, config_file, default_flow_style=False)
+ sys.exit(0)
+
+ return cls(args)
+
+
+
diff --git a/synapse/config/database.py b/synapse/config/database.py
new file mode 100644
index 0000000000..edf2361914
--- /dev/null
+++ b/synapse/config/database.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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
+import os
+
+class DatabaseConfig(Config):
+ def __init__(self, args):
+ super(DatabaseConfig, self).__init__(args)
+ self.database_path = self.abspath(args.database_path)
+
+ @classmethod
+ def add_arguments(cls, parser):
+ super(DatabaseConfig, cls).add_arguments(parser)
+ db_group = parser.add_argument_group("database")
+ db_group.add_argument(
+ "-d", "--database-path", default="homeserver.db",
+ help="The database name."
+ )
+
+ @classmethod
+ def generate_config(cls, args, config_dir_path):
+ super(DatabaseConfig, cls).generate_config(args, config_dir_path)
+ args.database_path = os.path.abspath(args.database_path)
+
diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py
new file mode 100644
index 0000000000..18072e3196
--- /dev/null
+++ b/synapse/config/homeserver.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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 .tls import TlsConfig
+from .server import ServerConfig
+from .logger import LoggingConfig
+from .database import DatabaseConfig
+
+class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig):
+ pass
+
+if __name__=='__main__':
+ import sys
+ HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")
diff --git a/synapse/config/logger.py b/synapse/config/logger.py
new file mode 100644
index 0000000000..8db6621ae8
--- /dev/null
+++ b/synapse/config/logger.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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
+
+from twisted.python.log import PythonLoggingObserver
+import logging
+import logging.config
+
+class LoggingConfig(Config):
+ def __init__(self, args):
+ super(LoggingConfig, self).__init__(args)
+ self.verbosity = int(args.verbose) if args.verbose else None
+ self.log_config = self.abspath(args.log_config)
+ self.log_file = self.abspath(args.log_file)
+
+ @classmethod
+ def add_arguments(cls, parser):
+ super(LoggingConfig, cls).add_arguments(parser)
+ logging_group = parser.add_argument_group("logging")
+ logging_group.add_argument(
+ '-v', '--verbose', dest="verbose", action='count',
+ help="The verbosity level."
+ )
+ logging_group.add_argument(
+ '-f', '--log-file', dest="log_file", default=None,
+ help="File to log to."
+ )
+ logging_group.add_argument(
+ '--log-config', dest="log_config", default=None,
+ help="Python logging config file"
+ )
+
+ def setup_logging(self):
+ log_format = (
+ '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s'
+ )
+ if self.log_config is None:
+
+ level = logging.INFO
+ if self.verbosity:
+ level = logging.DEBUG
+
+ # FIXME: we need a logging.WARN for a -q quiet option
+
+ logging.basicConfig(
+ level=level,
+ filename=self.log_file,
+ format=log_format
+ )
+ else:
+ logging.config.fileConfig(self.log_config)
+
+ observer = PythonLoggingObserver()
+ observer.start()
diff --git a/synapse/config/server.py b/synapse/config/server.py
new file mode 100644
index 0000000000..1f20d55d8f
--- /dev/null
+++ b/synapse/config/server.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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.
+
+import nacl.signing
+import os
+from ._base import Config
+from syutil.base64util import encode_base64, decode_base64
+
+
+class ServerConfig(Config):
+ def __init__(self, args):
+ super(ServerConfig, self).__init__(args)
+ self.server_name = args.server_name
+ self.signing_key = self.read_signing_key(args.signing_key_path)
+ self.bind_port = args.bind_port
+ self.bind_host = args.bind_host
+ self.unsecure_port = args.unsecure_port
+ self.daemonize = args.daemonize
+ self.pid_file = self.abspath(args.pid_file)
+ self.webclient = args.no_webclient
+ self.manhole = args.manhole
+
+ @classmethod
+ def add_arguments(cls, parser):
+ super(ServerConfig, cls).add_arguments(parser)
+ server_group = parser.add_argument_group("server")
+ server_group.add_argument("-H", "--server-name", default="localhost",
+ help="The name of the server")
+ server_group.add_argument("--signing-key-path",
+ help="The signing key to sign messages with")
+ server_group.add_argument("-p", "--bind-port", metavar="PORT",
+ type=int, help="https port to listen on")
+ server_group.add_argument("--unsecure-port", metavar="PORT",
+ type=int, help="http port to listen on")
+ server_group.add_argument("--bind-host", default="",
+ help="Local interface to listen on")
+ server_group.add_argument("-D", "--daemonize", action='store_true',
+ help="Daemonize the home server")
+ server_group.add_argument('--pid-file', default="hs.pid",
+ help="When running as a daemon, the file to"
+ " store the pid in")
+ server_group.add_argument("-W", "--no-webclient", default=True,
+ action="store_false",
+ help="Don't host a web client.")
+ server_group.add_argument("--manhole", metavar="PORT", dest="manhole",
+ type=int,
+ help="Turn on the twisted telnet manhole"
+ " service on the given port.")
+
+ def read_signing_key(self, signing_key_path):
+ signing_key_base64 = self.read_file(signing_key_path)
+ signing_key_bytes = decode_base64(signing_key_base64)
+ return nacl.signing.SigningKey(signing_key_bytes)
+
+ @classmethod
+ def generate_config(cls, args, config_dir_path):
+ super(ServerConfig, cls).generate_config(args, config_dir_path)
+ base_key_name = os.path.join(config_dir_path, args.server_name)
+
+ args.pid_file = os.path.abspath(args.pid_file)
+
+ if not args.signing_key_path:
+ args.signing_key_path = base_key_name + ".signing.key"
+
+ if not os.path.exists(args.signing_key_path):
+ with open(args.signing_key_path, "w") as signing_key_file:
+ key = nacl.signing.SigningKey.generate()
+ signing_key_file.write(encode_base64(key.encode()))
diff --git a/synapse/config/tls.py b/synapse/config/tls.py
new file mode 100644
index 0000000000..005fc1d16e
--- /dev/null
+++ b/synapse/config/tls.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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
+
+from OpenSSL import crypto
+import subprocess
+import os
+
+GENERATE_DH_PARAMS=False
+
+
+class TlsConfig(Config):
+ def __init__(self, args):
+ super(TlsConfig, self).__init__(args)
+ self.tls_certificate = self.read_tls_certificate(
+ args.tls_certificate_path
+ )
+ self.tls_private_key = self.read_tls_private_key(
+ args.tls_private_key_path
+ )
+ self.tls_dh_params_path = self.abspath(args.tls_dh_params_path)
+
+ @classmethod
+ def add_arguments(cls, parser):
+ super(TlsConfig, cls).add_arguments(parser)
+ tls_group = parser.add_argument_group("tls")
+ tls_group.add_argument("--tls-certificate-path",
+ help="PEM encoded X509 certificate for TLS")
+ tls_group.add_argument("--tls-private-key-path",
+ help="PEM encoded private key for TLS")
+ tls_group.add_argument("--tls-dh-params-path",
+ help="PEM dh parameters for ephemeral keys")
+
+ def read_tls_certificate(self, cert_path):
+ cert_pem = self.read_file(cert_path)
+ return crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
+
+ def read_tls_private_key(self, private_key_path):
+ private_key_pem = self.read_file(private_key_path)
+ return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
+
+ @classmethod
+ def generate_config(cls, args, config_dir_path):
+ super(TlsConfig, cls).generate_config(args, config_dir_path)
+ base_key_name = os.path.join(config_dir_path, args.server_name)
+
+ if args.tls_certificate_path is None:
+ args.tls_certificate_path = base_key_name + ".tls.crt"
+
+ if args.tls_private_key_path is None:
+ args.tls_private_key_path = base_key_name + ".tls.key"
+
+ if args.tls_dh_params_path is None:
+ args.tls_dh_params_path = base_key_name + ".tls.dh"
+
+ if not os.path.exists(args.tls_private_key_path):
+ with open(args.tls_private_key_path, "w") as private_key_file:
+ tls_private_key = crypto.PKey()
+ tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
+ private_key_pem = crypto.dump_privatekey(
+ crypto.FILETYPE_PEM, tls_private_key
+ )
+ private_key_file.write(private_key_pem)
+ else:
+ with open(args.tls_private_key_path) as private_key_file:
+ private_key_pem = private_key_file.read()
+ tls_private_key = crypto.load_privatekey(
+ crypto.FILETYPE_PEM, private_key_pem
+ )
+
+ if not os.path.exists(args.tls_certificate_path):
+ with open(args.tls_certificate_path, "w") as certifcate_file:
+ cert = crypto.X509()
+ subject = cert.get_subject()
+ subject.CN = args.server_name
+
+ cert.set_serial_number(1000)
+ cert.gmtime_adj_notBefore(0)
+ cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
+ cert.set_issuer(cert.get_subject())
+ cert.set_pubkey(tls_private_key)
+
+ cert.sign(tls_private_key, 'sha256')
+
+ cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
+
+ certifcate_file.write(cert_pem)
+
+ if not os.path.exists(args.tls_dh_params_path):
+ if GENERATE_DH_PARAMS:
+ subprocess.check_call([
+ "openssl", "dhparam",
+ "-outform", "PEM",
+ "-out", args.tls_dh_params_path,
+ "2048"
+ ])
+ else:
+ with open(args.tls_dh_params_path, "w") as dh_params_file:
+ dh_params_file.write(
+ "2048-bit DH parameters taken from rfc3526\n"
+ "-----BEGIN DH PARAMETERS-----\n"
+ "MIIBCAKCAQEA///////////JD9qiIWjC"
+ "NMTGYouA3BzRKQJOCIpnzHQCC76mOxOb\n"
+ "IlFKCHmONATd75UZs806QxswKwpt8l8U"
+ "N0/hNW1tUcJF5IW1dmJefsb0TELppjft\n"
+ "awv/XLb0Brft7jhr+1qJn6WunyQRfEsf"
+ "5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT\n"
+ "mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVS"
+ "u57VKQdwlpZtZww1Tkq8mATxdGwIyhgh\n"
+ "fDKQXkYuNs474553LBgOhgObJ4Oi7Aei"
+ "j7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq\n"
+ "5RXSJhiY+gUQFXKOWoqsqmj/////////"
+ "/wIBAg==\n"
+ "-----END DH PARAMETERS-----\n"
+ )
diff --git a/synapse/crypto/config.py b/synapse/crypto/config.py
deleted file mode 100644
index 2330133e71..0000000000
--- a/synapse/crypto/config.py
+++ /dev/null
@@ -1,160 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
-#
-# 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.
-
-
-import ConfigParser as configparser
-import argparse
-import socket
-import sys
-import os
-from OpenSSL import crypto
-import nacl.signing
-from syutil.base64util import encode_base64
-import subprocess
-
-
-def load_config(description, argv):
- config_parser = argparse.ArgumentParser(add_help=False)
- config_parser.add_argument("-c", "--config-path", metavar="CONFIG_FILE",
- help="Specify config file")
- config_args, remaining_args = config_parser.parse_known_args(argv)
- if config_args.config_path:
- config = configparser.SafeConfigParser()
- config.read([config_args.config_path])
- defaults = dict(config.items("KeyServer"))
- else:
- defaults = {}
- parser = argparse.ArgumentParser(
- parents=[config_parser],
- description=description,
- formatter_class=argparse.RawDescriptionHelpFormatter,
- )
- parser.set_defaults(**defaults)
- parser.add_argument("--server-name", default=socket.getfqdn(),
- help="The name of the server")
- parser.add_argument("--signing-key-path",
- help="The signing key to sign responses with")
- parser.add_argument("--tls-certificate-path",
- help="PEM encoded X509 certificate for TLS")
- parser.add_argument("--tls-private-key-path",
- help="PEM encoded private key for TLS")
- parser.add_argument("--tls-dh-params-path",
- help="PEM encoded dh parameters for ephemeral keys")
- parser.add_argument("--bind-port", type=int,
- help="TCP port to listen on")
- parser.add_argument("--bind-host", default="",
- help="Local interface to listen on")
-
- args = parser.parse_args(remaining_args)
-
- server_config = vars(args)
- del server_config["config_path"]
- return server_config
-
-
-def generate_config(argv):
- parser = argparse.ArgumentParser()
- parser.add_argument("-c", "--config-path", help="Specify config file",
- metavar="CONFIG_FILE", required=True)
- parser.add_argument("--server-name", default=socket.getfqdn(),
- help="The name of the server")
- parser.add_argument("--signing-key-path",
- help="The signing key to sign responses with")
- parser.add_argument("--tls-certificate-path",
- help="PEM encoded X509 certificate for TLS")
- parser.add_argument("--tls-private-key-path",
- help="PEM encoded private key for TLS")
- parser.add_argument("--tls-dh-params-path",
- help="PEM encoded dh parameters for ephemeral keys")
- parser.add_argument("--bind-port", type=int, required=True,
- help="TCP port to listen on")
- parser.add_argument("--bind-host", default="",
- help="Local interface to listen on")
-
- args = parser.parse_args(argv)
-
- dir_name = os.path.dirname(args.config_path)
- base_key_name = os.path.join(dir_name, args.server_name)
-
- if args.signing_key_path is None:
- args.signing_key_path = base_key_name + ".signing.key"
-
- if args.tls_certificate_path is None:
- args.tls_certificate_path = base_key_name + ".tls.crt"
-
- if args.tls_private_key_path is None:
- args.tls_private_key_path = base_key_name + ".tls.key"
-
- if args.tls_dh_params_path is None:
- args.tls_dh_params_path = base_key_name + ".tls.dh"
-
- if not os.path.exists(args.signing_key_path):
- with open(args.signing_key_path, "w") as signing_key_file:
- key = nacl.signing.SigningKey.generate()
- signing_key_file.write(encode_base64(key.encode()))
-
- if not os.path.exists(args.tls_private_key_path):
- with open(args.tls_private_key_path, "w") as private_key_file:
- tls_private_key = crypto.PKey()
- tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
- private_key_pem = crypto.dump_privatekey(
- crypto.FILETYPE_PEM, tls_private_key
- )
- private_key_file.write(private_key_pem)
- else:
- with open(args.tls_private_key_path) as private_key_file:
- private_key_pem = private_key_file.read()
- tls_private_key = crypto.load_privatekey(
- crypto.FILETYPE_PEM, private_key_pem
- )
-
- if not os.path.exists(args.tls_certificate_path):
- with open(args.tls_certificate_path, "w") as certifcate_file:
- cert = crypto.X509()
- subject = cert.get_subject()
- subject.CN = args.server_name
-
- cert.set_serial_number(1000)
- cert.gmtime_adj_notBefore(0)
- cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
- cert.set_issuer(cert.get_subject())
- cert.set_pubkey(tls_private_key)
-
- cert.sign(tls_private_key, 'sha256')
-
- cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
-
- certifcate_file.write(cert_pem)
-
- if not os.path.exists(args.tls_dh_params_path):
- subprocess.check_call([
- "openssl", "dhparam",
- "-outform", "PEM",
- "-out", args.tls_dh_params_path,
- "2048"
- ])
-
- config = configparser.SafeConfigParser()
- config.add_section("KeyServer")
- for key, value in vars(args).items():
- if key != "config_path":
- config.set("KeyServer", key, str(value))
-
- with open(args.config_path, "w") as config_file:
- config.write(config_file)
-
-
-if __name__ == "__main__":
- generate_config(sys.argv[1:])
diff --git a/synapse/crypto/context_factory.py b/synapse/crypto/context_factory.py
new file mode 100644
index 0000000000..45958abbf5
--- /dev/null
+++ b/synapse/crypto/context_factory.py
@@ -0,0 +1,29 @@
+from twisted.internet import reactor, ssl
+from OpenSSL import SSL
+from twisted.internet._sslverify import _OpenSSLECCurve, _defaultCurveName
+
+
+class ServerContextFactory(ssl.ContextFactory):
+ """Factory for PyOpenSSL SSL contexts that are used to handle incoming
+ connections and to make connections to remote servers."""
+
+ def __init__(self, config):
+ self._context = SSL.Context(SSL.SSLv23_METHOD)
+ self.configure_context(self._context, config)
+
+ @staticmethod
+ def configure_context(context, config):
+ try:
+ _ecCurve = _OpenSSLECCurve(_defaultCurveName)
+ _ecCurve.addECKeyToContext(context)
+ except:
+ pass
+ context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
+ context.use_certificate(config.tls_certificate)
+ context.use_privatekey(config.tls_private_key)
+ context.load_tmp_dh(config.tls_dh_params_path)
+ context.set_cipher_list("!ADH:HIGH+kEDH:!AECDH:HIGH+kEECDH")
+
+ def getContext(self):
+ return self._context
+
diff --git a/synapse/federation/units.py b/synapse/federation/units.py
index 2b2f11f36a..b468f70546 100644
--- a/synapse/federation/units.py
+++ b/synapse/federation/units.py
@@ -68,6 +68,7 @@ class Pdu(JsonEncodedObject):
"power_level",
"prev_state_id",
"prev_state_origin",
+ "required_power_level",
]
internal_keys = [
diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py
index f55bea58a5..7372a10f9b 100644
--- a/synapse/handlers/presence.py
+++ b/synapse/handlers/presence.py
@@ -150,10 +150,6 @@ class PresenceHandler(BaseHandler):
@defer.inlineCallbacks
def is_presence_visible(self, observer_user, observed_user):
- defer.returnValue(True)
- # return
- # FIXME (erikj): This code path absolutely kills the database.
-
assert(observed_user.is_mine)
if observer_user == observed_user:
@@ -488,10 +484,6 @@ class PresenceHandler(BaseHandler):
def _start_polling_local(self, user, target_user):
target_localpart = target_user.localpart
- if not self.is_presence_visible(observer_user=user,
- observed_user=target_user):
- return
-
if target_localpart not in self._local_pushmap:
self._local_pushmap[target_localpart] = set()
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index c54e0f963b..3551615228 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -17,10 +17,12 @@
from twisted.internet import defer
from synapse.types import UserID, RoomAlias, RoomID
-from synapse.api.constants import Membership
+from synapse.api.constants import Membership, JoinRules
from synapse.api.errors import StoreError, SynapseError
from synapse.api.events.room import (
- RoomMemberEvent, RoomConfigEvent
+ RoomMemberEvent, RoomCreateEvent, RoomPowerLevelsEvent,
+ RoomJoinRulesEvent, RoomAddStateLevelEvent,
+ RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent,
)
from synapse.util import stringutils
from ._base import BaseRoomHandler
@@ -62,6 +64,8 @@ class RoomCreationHandler(BaseRoomHandler):
else:
room_alias = None
+ is_public = config.get("visibility", None) == "public"
+
if room_id:
# Ensure room_id is the correct type
room_id_obj = RoomID.from_string(room_id, self.hs)
@@ -71,7 +75,7 @@ class RoomCreationHandler(BaseRoomHandler):
yield self.store.store_room(
room_id=room_id,
room_creator_user_id=user_id,
- is_public=config["visibility"] == "public"
+ is_public=is_public
)
else:
# autogen room IDs and try to create it. We may clash, so just
@@ -85,7 +89,7 @@ class RoomCreationHandler(BaseRoomHandler):
yield self.store.store_room(
room_id=gen_room_id.to_string(),
room_creator_user_id=user_id,
- is_public=config["visibility"] == "public"
+ is_public=is_public
)
room_id = gen_room_id.to_string()
break
@@ -94,18 +98,10 @@ class RoomCreationHandler(BaseRoomHandler):
if not room_id:
raise StoreError(500, "Couldn't generate a room ID.")
- config_event = self.event_factory.create_event(
- etype=RoomConfigEvent.TYPE,
- room_id=room_id,
- user_id=user_id,
- content=config,
- )
- snapshot = yield self.store.snapshot_room(
- room_id=room_id,
- user_id=user_id,
- state_type=RoomConfigEvent.TYPE,
- state_key="",
+ user = self.hs.parse_userid(user_id)
+ creation_events = self._create_events_for_new_room(
+ user, room_id, is_public=is_public
)
if room_alias:
@@ -115,11 +111,18 @@ class RoomCreationHandler(BaseRoomHandler):
servers=[self.hs.hostname],
)
- yield self.state_handler.handle_new_event(config_event, snapshot)
- # store_id = persist...
-
federation_handler = self.hs.get_handlers().federation_handler
- yield federation_handler.handle_new_event(config_event, snapshot)
+
+ for event in creation_events:
+ snapshot = yield self.store.snapshot_room(
+ room_id=room_id,
+ user_id=user_id,
+ )
+
+ logger.debug("Event: %s", event)
+
+ yield self.state_handler.handle_new_event(event, snapshot)
+ yield self._on_new_room_event(event, snapshot, extra_users=[user])
content = {"membership": Membership.JOIN}
join_event = self.event_factory.create_event(
@@ -142,6 +145,62 @@ class RoomCreationHandler(BaseRoomHandler):
defer.returnValue(result)
+ def _create_events_for_new_room(self, creator, room_id, is_public=False):
+ event_keys = {
+ "room_id": room_id,
+ "user_id": creator.to_string(),
+ "required_power_level": 10,
+ }
+
+ def create(etype, **content):
+ return self.event_factory.create_event(
+ etype=etype,
+ content=content,
+ **event_keys
+ )
+
+ creation_event = create(
+ etype=RoomCreateEvent.TYPE,
+ creator=creator.to_string(),
+ )
+
+ power_levels_event = self.event_factory.create_event(
+ etype=RoomPowerLevelsEvent.TYPE,
+ content={creator.to_string(): 10, "default": 0},
+ **event_keys
+ )
+
+ join_rule = JoinRules.PUBLIC if is_public else JoinRules.INVITE
+ join_rules_event = create(
+ etype=RoomJoinRulesEvent.TYPE,
+ join_rule=join_rule,
+ )
+
+ add_state_event = create(
+ etype=RoomAddStateLevelEvent.TYPE,
+ level=10,
+ )
+
+ send_event = create(
+ etype=RoomSendEventLevelEvent.TYPE,
+ level=0,
+ )
+
+ ops = create(
+ etype=RoomOpsPowerLevelsEvent.TYPE,
+ ban_level=5,
+ kick_level=5,
+ )
+
+ return [
+ creation_event,
+ power_levels_event,
+ join_rules_event,
+ add_state_event,
+ send_event,
+ ops,
+ ]
+
class RoomMemberHandler(BaseRoomHandler):
# TODO(paul): This handler currently contains a messy conflation of
@@ -285,6 +344,16 @@ class RoomMemberHandler(BaseRoomHandler):
if do_auth:
yield self.auth.check(event, snapshot, raises=True)
+ # If we're banning someone, set a req power level
+ if event.membership == Membership.BAN:
+ if not hasattr(event, "required_power_level") or event.required_power_level is None:
+ # Add some default required_power_level
+ user_level = yield self.store.get_power_level(
+ event.room_id,
+ event.user_id,
+ )
+ event.required_power_level = user_level
+
if prev_state and prev_state.membership == event.membership:
# double same action, treat this event as a NOOP.
defer.returnValue({})
@@ -445,10 +514,9 @@ class RoomMemberHandler(BaseRoomHandler):
host = target_user.domain
destinations.append(host)
- # If we are joining a remote HS, include that.
- if membership == Membership.JOIN:
- host = target_user.domain
- destinations.append(host)
+ # Always include target domain
+ host = target_user.domain
+ destinations.append(host)
return self._on_new_room_event(
event, snapshot, extra_destinations=destinations,
diff --git a/synapse/http/client.py b/synapse/http/client.py
index 36ba2c6591..093bdf0e3f 100644
--- a/synapse/http/client.py
+++ b/synapse/http/client.py
@@ -113,8 +113,9 @@ class TwistedHttpClient(HttpClient):
requests.
"""
- def __init__(self):
+ def __init__(self, hs):
self.agent = MatrixHttpAgent(reactor)
+ self.hs = hs
@defer.inlineCallbacks
def put_json(self, destination, path, data):
@@ -177,7 +178,10 @@ class TwistedHttpClient(HttpClient):
retries_left = 5
# TODO: setup and pass in an ssl_context to enable TLS
- endpoint = matrix_endpoint(reactor, destination, timeout=10)
+ endpoint = matrix_endpoint(
+ reactor, destination, timeout=10,
+ ssl_context_factory=self.hs.tls_context_factory
+ )
while True:
try:
diff --git a/synapse/http/endpoint.py b/synapse/http/endpoint.py
index d91500b07d..a6ebe23567 100644
--- a/synapse/http/endpoint.py
+++ b/synapse/http/endpoint.py
@@ -53,7 +53,7 @@ def matrix_endpoint(reactor, destination, ssl_context_factory=None,
default_port = 8080
else:
transport_endpoint = SSL4ClientEndpoint
- endpoint_kw_args.update(ssl_context_factory=ssl_context_factory)
+ endpoint_kw_args.update(sslContextFactory=ssl_context_factory)
default_port = 443
if port is None:
diff --git a/synapse/http/server.py b/synapse/http/server.py
index 243b71744d..0b87718bfa 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -325,7 +325,11 @@ class ContentRepoResource(resource.Resource):
# FIXME (erikj): These should use constants.
file_name = os.path.basename(fname)
- url = "http://%s/_matrix/content/%s" % (
+ # FIXME: we can't assume what the public mounted path of the repo is
+ # ...plus self-signed SSL won't work to remote clients anyway
+ # ...and we can't assume that it's SSL anyway, as we might want to
+ # server it via the non-SSL listener...
+ url = "https://%s/_matrix/content/%s" % (
self.hs.domain_with_port, file_name
)
diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py
index e8faba3eeb..7a441391f0 100644
--- a/synapse/storage/__init__.py
+++ b/synapse/storage/__init__.py
@@ -19,6 +19,10 @@ from synapse.api.events.room import (
RoomMemberEvent, RoomTopicEvent, FeedbackEvent,
# RoomConfigEvent,
RoomNameEvent,
+ RoomJoinRulesEvent,
+ RoomPowerLevelsEvent,
+ RoomAddStateLevelEvent,
+ RoomSendEventLevelEvent,
)
from synapse.util.logutils import log_function
@@ -33,6 +37,7 @@ from .roommember import RoomMemberStore
from .stream import StreamStore
from .pdu import StatePduStore, PduStore
from .transactions import TransactionStore
+from .keys import KeyStore
import json
import logging
@@ -45,7 +50,7 @@ logger = logging.getLogger(__name__)
class DataStore(RoomMemberStore, RoomStore,
RegistrationStore, StreamStore, ProfileStore, FeedbackStore,
PresenceStore, PduStore, StatePduStore, TransactionStore,
- DirectoryStore):
+ DirectoryStore, KeyStore):
def __init__(self, hs):
super(DataStore, self).__init__(hs)
@@ -122,13 +127,19 @@ class DataStore(RoomMemberStore, RoomStore,
if event.type == RoomMemberEvent.TYPE:
self._store_room_member_txn(txn, event)
elif event.type == FeedbackEvent.TYPE:
- self._store_feedback_txn(txn,event)
-# elif event.type == RoomConfigEvent.TYPE:
-# self._store_room_config_txn(txn, event)
+ self._store_feedback_txn(txn, event)
elif event.type == RoomNameEvent.TYPE:
self._store_room_name_txn(txn, event)
elif event.type == RoomTopicEvent.TYPE:
self._store_room_topic_txn(txn, event)
+ elif event.type == RoomJoinRulesEvent.TYPE:
+ self._store_join_rule(txn, event)
+ elif event.type == RoomPowerLevelsEvent.TYPE:
+ self._store_power_levels(txn, event)
+ elif event.type == RoomAddStateLevelEvent.TYPE:
+ self._store_add_state_level(txn, event)
+ elif event.type == RoomSendEventLevelEvent.TYPE:
+ self._store_send_event_level(txn, event)
vals = {
"topological_ordering": event.depth,
@@ -222,7 +233,6 @@ class DataStore(RoomMemberStore, RoomStore,
defer.returnValue(self.min_token)
-
def snapshot_room(self, room_id, user_id, state_type=None, state_key=None):
"""Snapshot the room for an update by a user
Args:
diff --git a/synapse/storage/keys.py b/synapse/storage/keys.py
new file mode 100644
index 0000000000..4d19b9f641
--- /dev/null
+++ b/synapse/storage/keys.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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 SQLBaseStore
+
+from twisted.internet import defer
+
+import OpenSSL
+import nacl.signing
+
+class KeyStore(SQLBaseStore):
+ """Persistence for signature verification keys and tls X.509 certificates
+ """
+
+ @defer.inlineCallbacks
+ def get_server_certificate(self, server_name):
+ """Retrieve the TLS X.509 certificate for the given server
+ Args:
+ server_name (bytes): The name of the server.
+ Returns:
+ (OpenSSL.crypto.X509): The tls certificate.
+ """
+ tls_certificate_bytes, = yield self._simple_select_one(
+ table="server_tls_certificates",
+ keyvalues={"server_name": server_name},
+ retcols=("tls_certificate",),
+ )
+ tls_certificate = OpenSSL.crypto.load_certificate(
+ OpenSSL.crypto.FILETYPE_ASN1, tls_certificate_bytes,
+ )
+ defer.returnValue(tls_certificate)
+
+ def store_server_certificate(self, server_name, key_server, ts_now_ms,
+ tls_certificate):
+ """Stores the TLS X.509 certificate for the given server
+ Args:
+ server_name (bytes): The name of the server.
+ key_server (bytes): Where the certificate was looked up
+ ts_now_ms (int): The time now in milliseconds
+ tls_certificate (OpenSSL.crypto.X509): The X.509 certificate.
+ """
+ tls_certificate_bytes = OpenSSL.crypto.dump_certificate(
+ OpenSSL.crypto.FILETYPE_ASN1, tls_certificate
+ )
+ return self._simple_insert(
+ table="server_tls_certificates",
+ keyvalues={
+ "server_name": server_name,
+ "key_server": key_server,
+ "ts_added_ms": ts_now_ms,
+ "tls_certificate": tls_certificate_bytes,
+ },
+ )
+
+ @defer.inlineCallbacks
+ def get_server_verification_key(self, server_name):
+ """Retrieve the NACL verification key for a given server
+ Args:
+ server_name (bytes): The name of the server.
+ Returns:
+ (nacl.signing.VerifyKey): The verification key.
+ """
+ verification_key_bytes, = yield self._simple_select_one(
+ table="server_signature_keys",
+ key_values={"server_name": server_name},
+ retcols=("tls_certificate",),
+ )
+ verification_key = nacl.signing.VerifyKey(verification_key_bytes)
+ defer.returnValue(verification_key)
+
+ def store_server_verification_key(self, server_name, key_version,
+ key_server, ts_now_ms, verification_key):
+ """Stores a NACL verification key for the given server.
+ Args:
+ server_name (bytes): The name of the server.
+ key_version (bytes): The version of the key for the server.
+ key_server (bytes): Where the verification key was looked up
+ ts_now_ms (int): The time now in milliseconds
+ verification_key (nacl.signing.VerifyKey): The NACL verify key.
+ """
+ verification_key_bytes = verification_key.encode()
+ return self._simple_insert(
+ table="server_signature_keys",
+ key_values={
+ "server_name": server_name,
+ "key_version": key_version,
+ "key_server": key_server,
+ "ts_added_ms": ts_now_ms,
+ "verification_key": verification_key_bytes,
+ },
+ )
diff --git a/synapse/storage/room.py b/synapse/storage/room.py
index d1f1a232f8..3ca07f4350 100644
--- a/synapse/storage/room.py
+++ b/synapse/storage/room.py
@@ -27,6 +27,9 @@ import logging
logger = logging.getLogger(__name__)
+OpsLevel = collections.namedtuple("OpsLevel", ("ban_level", "kick_level"))
+
+
class RoomStore(SQLBaseStore):
@defer.inlineCallbacks
@@ -129,6 +132,98 @@ class RoomStore(SQLBaseStore):
defer.returnValue(ret)
+ @defer.inlineCallbacks
+ def get_room_join_rule(self, room_id):
+ sql = (
+ "SELECT join_rule FROM room_join_rules as r "
+ "INNER JOIN current_state_events as c "
+ "ON r.event_id = c.event_id "
+ "WHERE c.room_id = ? "
+ )
+
+ rows = yield self._execute(None, sql, room_id)
+
+ if len(rows) == 1:
+ defer.returnValue(rows[0][0])
+ else:
+ defer.returnValue(None)
+
+ def get_power_level(self, room_id, user_id):
+ return self._db_pool.runInteraction(
+ self._get_power_level,
+ room_id, user_id,
+ )
+
+ def _get_power_level(self, txn, room_id, user_id):
+ sql = (
+ "SELECT level FROM room_power_levels as r "
+ "INNER JOIN current_state_events as c "
+ "ON r.event_id = c.event_id "
+ "WHERE c.room_id = ? AND r.user_id = ? "
+ )
+
+ rows = txn.execute(sql, (room_id, user_id,)).fetchall()
+
+ if len(rows) == 1:
+ return rows[0][0]
+
+ sql = (
+ "SELECT level FROM room_default_levels as r "
+ "INNER JOIN current_state_events as c "
+ "ON r.event_id = c.event_id "
+ "WHERE c.room_id = ? "
+ )
+
+ rows = txn.execute(sql, (room_id,)).fetchall()
+
+ if len(rows) == 1:
+ return rows[0][0]
+ else:
+ return None
+
+ def get_ops_levels(self, room_id):
+ return self._db_pool.runInteraction(
+ self._get_ops_levels,
+ room_id,
+ )
+
+ def _get_ops_levels(self, txn, room_id):
+ sql = (
+ "SELECT ban_level, kick_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 = ? "
+ )
+
+ rows = txn.execute(sql, (room_id,)).fetchall()
+
+ if len(rows) == 1:
+ return OpsLevel(rows[0][0], rows[0][1])
+ else:
+ return OpsLevel(None, None)
+
+ def get_add_state_level(self, room_id):
+ return self._get_level_from_table("room_add_state_levels", room_id)
+
+ def get_send_event_level(self, room_id):
+ return self._get_level_from_table("room_send_event_levels", room_id)
+
+ @defer.inlineCallbacks
+ def _get_level_from_table(self, table, room_id):
+ sql = (
+ "SELECT level FROM %(table)s as r "
+ "INNER JOIN current_state_events as c "
+ "ON r.event_id = c.event_id "
+ "WHERE c.room_id = ? "
+ ) % {"table": table}
+
+ rows = yield self._execute(None, sql, room_id)
+
+ if len(rows) == 1:
+ defer.returnValue(rows[0][0])
+ else:
+ defer.returnValue(None)
+
def _store_room_topic_txn(self, txn, event):
self._simple_insert_txn(
txn,
@@ -151,6 +246,92 @@ class RoomStore(SQLBaseStore):
}
)
+ def _store_join_rule(self, txn, event):
+ self._simple_insert_txn(
+ txn,
+ "room_join_rules",
+ {
+ "event_id": event.event_id,
+ "room_id": event.room_id,
+ "join_rule": event.content["join_rule"],
+ },
+ )
+
+ def _store_power_levels(self, txn, event):
+ for user_id, level in event.content.items():
+ if user_id == "default":
+ self._simple_insert_txn(
+ txn,
+ "room_default_levels",
+ {
+ "event_id": event.event_id,
+ "room_id": event.room_id,
+ "level": level,
+ },
+ )
+ else:
+ self._simple_insert_txn(
+ txn,
+ "room_power_levels",
+ {
+ "event_id": event.event_id,
+ "room_id": event.room_id,
+ "user_id": user_id,
+ "level": level
+ },
+ )
+
+ def _store_default_level(self, txn, event):
+ self._simple_insert_txn(
+ txn,
+ "room_default_levels",
+ {
+ "event_id": event.event_id,
+ "room_id": event.room_id,
+ "level": event.content["default_level"],
+ },
+ )
+
+ def _store_add_state_level(self, txn, event):
+ self._simple_insert_txn(
+ txn,
+ "room_add_state_levels",
+ {
+ "event_id": event.event_id,
+ "room_id": event.room_id,
+ "level": event.content["level"],
+ },
+ )
+
+ def _store_send_event_level(self, txn, event):
+ self._simple_insert_txn(
+ txn,
+ "room_send_event_levels",
+ {
+ "event_id": event.event_id,
+ "room_id": event.room_id,
+ "level": event.content["level"],
+ },
+ )
+
+ def _store_ops_level(self, txn, event):
+ content = {
+ "event_id": event.event_id,
+ "room_id": event.room_id,
+ }
+
+ if "kick_level" in event.content:
+ content["kick_level"] = event.content["kick_level"]
+
+ if "ban_level" in event.content:
+ content["ban_level"] = event.content["ban_level"]
+
+ self._simple_insert_txn(
+ txn,
+ "room_send_event_levels",
+ content,
+ )
+
class RoomsTable(Table):
table_name = "rooms"
diff --git a/synapse/storage/schema/im.sql b/synapse/storage/schema/im.sql
index e92f21ef3b..dbefbbda31 100644
--- a/synapse/storage/schema/im.sql
+++ b/synapse/storage/schema/im.sql
@@ -96,8 +96,71 @@ CREATE TABLE IF NOT EXISTS rooms(
creator TEXT
);
+CREATE TABLE IF NOT EXISTS room_join_rules(
+ event_id TEXT NOT NULL,
+ room_id TEXT NOT NULL,
+ join_rule TEXT NOT NULL
+);
+CREATE INDEX IF NOT EXISTS room_join_rules_event_id ON room_join_rules(event_id);
+CREATE INDEX IF NOT EXISTS room_join_rules_room_id ON room_join_rules(room_id);
+
+
+CREATE TABLE IF NOT EXISTS room_power_levels(
+ event_id TEXT NOT NULL,
+ room_id TEXT NOT NULL,
+ user_id TEXT NOT NULL,
+ level INTEGER NOT NULL
+);
+CREATE INDEX IF NOT EXISTS room_power_levels_event_id ON room_power_levels(event_id);
+CREATE INDEX IF NOT EXISTS room_power_levels_room_id ON room_power_levels(room_id);
+CREATE INDEX IF NOT EXISTS room_power_levels_room_user ON room_power_levels(room_id, user_id);
+
+
+CREATE TABLE IF NOT EXISTS room_default_levels(
+ event_id TEXT NOT NULL,
+ room_id TEXT NOT NULL,
+ level INTEGER NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS room_default_levels_event_id ON room_default_levels(event_id);
+CREATE INDEX IF NOT EXISTS room_default_levels_room_id ON room_default_levels(room_id);
+
+
+CREATE TABLE IF NOT EXISTS room_add_state_levels(
+ event_id TEXT NOT NULL,
+ room_id TEXT NOT NULL,
+ level INTEGER NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS room_add_state_levels_event_id ON room_add_state_levels(event_id);
+CREATE INDEX IF NOT EXISTS room_add_state_levels_room_id ON room_add_state_levels(room_id);
+
+
+CREATE TABLE IF NOT EXISTS room_send_event_levels(
+ event_id TEXT NOT NULL,
+ room_id TEXT NOT NULL,
+ level INTEGER NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS room_send_event_levels_event_id ON room_send_event_levels(event_id);
+CREATE INDEX IF NOT EXISTS room_send_event_levels_room_id ON room_send_event_levels(room_id);
+
+
+CREATE TABLE IF NOT EXISTS room_ops_levels(
+ event_id TEXT NOT NULL,
+ room_id TEXT NOT NULL,
+ ban_level INTEGER,
+ kick_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_room_id ON room_ops_levels(room_id);
+
+
CREATE TABLE IF NOT EXISTS room_hosts(
room_id TEXT NOT NULL,
host TEXT NOT NULL,
CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE
);
+
+CREATE INDEX IF NOT EXISTS room_hosts_room_id ON room_hosts (room_id);
diff --git a/synapse/storage/schema/keys.sql b/synapse/storage/schema/keys.sql
new file mode 100644
index 0000000000..45cdbcecae
--- /dev/null
+++ b/synapse/storage/schema/keys.sql
@@ -0,0 +1,30 @@
+/* Copyright 2014 matrix.org
+ *
+ * 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.
+ */
+CREATE TABLE IF NOT EXISTS server_tls_certificates(
+ server_name TEXT, -- Server name.
+ key_server TEXT, -- Which key server the certificate was fetched from.
+ ts_added_ms INTEGER, -- When the certifcate was added.
+ tls_certificate BLOB, -- DER encoded x509 certificate.
+ CONSTRAINT uniqueness UNIQUE (server_name)
+);
+
+CREATE TABLE IF NOT EXISTS server_signature_keys(
+ server_name TEXT, -- Server name.
+ key_version TEXT, -- Key version.
+ key_server TEXT, -- Which key server the key was fetched form.
+ ts_added_ms INTEGER, -- When the key was added.
+ verification_key BLOB, -- NACL verification key.
+ CONSTRAINT uniqueness UNIQUE (server_name, key_version)
+);
diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py
index 844c4e3fb7..2eea474391 100644
--- a/tests/handlers/test_presence.py
+++ b/tests/handlers/test_presence.py
@@ -196,8 +196,6 @@ class PresenceStateTestCase(unittest.TestCase):
SynapseError
)
- test_get_disallowed_state.skip = "Presence permissions are disabled"
-
@defer.inlineCallbacks
def test_set_my_state(self):
mocked_set = self.datastore.set_presence_state
diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py
index 09d2a92e16..219a53c426 100644
--- a/tests/handlers/test_room.py
+++ b/tests/handlers/test_room.py
@@ -330,6 +330,8 @@ class RoomCreationTest(unittest.TestCase):
datastore=NonCallableMock(spec_set=[
"store_room",
"snapshot_room",
+ "persist_event",
+ "get_joined_hosts_for_room",
]),
http_client=NonCallableMock(spec_set=[]),
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
@@ -362,6 +364,10 @@ class RoomCreationTest(unittest.TestCase):
])
self.room_member_handler = self.handlers.room_member_handler
+ def hosts(room):
+ return defer.succeed([])
+ self.datastore.get_joined_hosts_for_room.side_effect = hosts
+
@defer.inlineCallbacks
def test_room_creation(self):
user_id = "@foo:red"
@@ -385,9 +391,3 @@ class RoomCreationTest(unittest.TestCase):
self.assertTrue(self.state_handler.handle_new_event.called)
self.assertTrue(self.federation.handle_new_event.called)
- config_event = self.federation.handle_new_event.call_args[0][0]
-
- self.assertEquals(RoomConfigEvent.TYPE, config_event.type)
- self.assertEquals(room_id, config_event.room_id)
- self.assertEquals(user_id, config_event.user_id)
- self.assertEquals(config, config_event.content)
diff --git a/tests/utils.py b/tests/utils.py
index ea7d6893c6..2c146eab55 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -234,6 +234,20 @@ class MemoryDataStore(object):
def get_room_events_max_id(self):
return 0 # TODO (erikj)
+ def get_send_event_level(self, room_id):
+ return defer.succeed(0)
+
+ def get_power_level(self, room_id, user_id):
+ return defer.succeed(0)
+
+ def get_add_state_level(self, room_id):
+ return defer.succeed(0)
+
+ def get_room_join_rule(self, room_id):
+ # TODO (erikj): This should be configurable
+ return defer.succeed("invite")
+
+
def _format_call(args, kwargs):
return ", ".join(
["%r" % (a) for a in args] +
|