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/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/room.py b/synapse/handlers/room.py
index c54e0f963b..f33bec9cc1 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,61 @@ 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(),
+ }
+
+ 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(),
+ default=0,
+ )
+
+ power_levels_event = create(
+ etype=RoomPowerLevelsEvent.TYPE,
+ **{creator.to_string(): 10, "default": 0}
+ )
+
+ 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 +343,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 +513,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/storage/__init__.py b/synapse/storage/__init__.py
index 3ad6f3a4d6..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
@@ -123,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,
@@ -223,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/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);
|