Initial group server implementation
parent
d4d12daed9
commit
b8ca494ee9
|
@ -471,3 +471,37 @@ class TransportLayerClient(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue(content)
|
defer.returnValue(content)
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
def invite_to_group_notification(self, destination, group_id, user_id, content):
|
||||||
|
path = PREFIX + "/groups/local/%s/users/%s/invite" % (group_id, user_id)
|
||||||
|
|
||||||
|
return self.client.post_json(
|
||||||
|
destination=destination,
|
||||||
|
path=path,
|
||||||
|
data=content,
|
||||||
|
ignore_backoff=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
def remove_user_from_group_notification(self, destination, group_id, user_id,
|
||||||
|
content):
|
||||||
|
path = PREFIX + "/groups/local/%s/users/%s/remove" % (group_id, user_id)
|
||||||
|
|
||||||
|
return self.client.post_json(
|
||||||
|
destination=destination,
|
||||||
|
path=path,
|
||||||
|
data=content,
|
||||||
|
ignore_backoff=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
def renew_group_attestation(self, destination, group_id, user_id, content):
|
||||||
|
path = PREFIX + "/groups/%s/renew_attestation/%s" % (group_id, user_id)
|
||||||
|
|
||||||
|
return self.client.post_json(
|
||||||
|
destination=destination,
|
||||||
|
path=path,
|
||||||
|
data=content,
|
||||||
|
ignore_backoff=True,
|
||||||
|
)
|
||||||
|
|
|
@ -25,7 +25,7 @@ from synapse.http.servlet import (
|
||||||
from synapse.util.ratelimitutils import FederationRateLimiter
|
from synapse.util.ratelimitutils import FederationRateLimiter
|
||||||
from synapse.util.versionstring import get_version_string
|
from synapse.util.versionstring import get_version_string
|
||||||
from synapse.util.logcontext import preserve_fn
|
from synapse.util.logcontext import preserve_fn
|
||||||
from synapse.types import ThirdPartyInstanceID
|
from synapse.types import ThirdPartyInstanceID, get_domain_from_id
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
@ -609,6 +609,115 @@ class FederationVersionServlet(BaseFederationServlet):
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
class FederationGroupsProfileServlet(BaseFederationServlet):
|
||||||
|
PATH = "/groups/(?P<group_id>[^/]*)/profile$"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, origin, content, query, group_id):
|
||||||
|
requester_user_id = content["requester_user_id"]
|
||||||
|
if get_domain_from_id(requester_user_id) != origin:
|
||||||
|
raise SynapseError(403, "requester_user_id doesn't match origin")
|
||||||
|
|
||||||
|
new_content = yield self.handler.get_group_profile(
|
||||||
|
group_id, requester_user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((200, new_content))
|
||||||
|
|
||||||
|
|
||||||
|
class FederationGroupsRoomsServlet(BaseFederationServlet):
|
||||||
|
PATH = "/groups/(?P<group_id>[^/]*)/rooms$"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, origin, content, query, group_id):
|
||||||
|
requester_user_id = content["requester_user_id"]
|
||||||
|
if get_domain_from_id(requester_user_id) != origin:
|
||||||
|
raise SynapseError(403, "requester_user_id doesn't match origin")
|
||||||
|
|
||||||
|
new_content = yield self.handler.get_rooms_in_group(
|
||||||
|
group_id, requester_user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((200, new_content))
|
||||||
|
|
||||||
|
|
||||||
|
class FederationGroupsUsersServlet(BaseFederationServlet):
|
||||||
|
PATH = "/groups/(?P<group_id>[^/]*)/users$"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, origin, content, query, group_id):
|
||||||
|
requester_user_id = content["requester_user_id"]
|
||||||
|
if get_domain_from_id(requester_user_id) != origin:
|
||||||
|
raise SynapseError(403, "requester_user_id doesn't match origin")
|
||||||
|
|
||||||
|
new_content = yield self.handler.get_users_in_group(
|
||||||
|
group_id, requester_user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((200, new_content))
|
||||||
|
|
||||||
|
|
||||||
|
class FederationGroupsInviteServlet(BaseFederationServlet):
|
||||||
|
PATH = "/groups/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/invite$"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, origin, content, query, group_id, user_id):
|
||||||
|
requester_user_id = content["requester_user_id"]
|
||||||
|
if get_domain_from_id(requester_user_id) != origin:
|
||||||
|
raise SynapseError(403, "requester_user_id doesn't match origin")
|
||||||
|
|
||||||
|
new_content = yield self.handler.invite_to_group(
|
||||||
|
group_id, user_id, requester_user_id, content,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((200, new_content))
|
||||||
|
|
||||||
|
|
||||||
|
class FederationGroupsAcceptInviteServlet(BaseFederationServlet):
|
||||||
|
PATH = "/groups/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/accept_invite$"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, origin, content, query, group_id, user_id):
|
||||||
|
if get_domain_from_id(user_id) != origin:
|
||||||
|
raise SynapseError(403, "user_id doesn't match origin")
|
||||||
|
|
||||||
|
new_content = yield self.handler.accept_invite(
|
||||||
|
group_id, user_id, content,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((200, new_content))
|
||||||
|
|
||||||
|
|
||||||
|
class FederationGroupsRemoveUserServlet(BaseFederationServlet):
|
||||||
|
PATH = "/groups/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/remove$"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, origin, content, query, group_id, user_id):
|
||||||
|
requester_user_id = content["requester_user_id"]
|
||||||
|
if get_domain_from_id(requester_user_id) != origin:
|
||||||
|
raise SynapseError(403, "requester_user_id doesn't match origin")
|
||||||
|
|
||||||
|
new_content = yield self.handler.remove_user_from_group(
|
||||||
|
group_id, user_id, requester_user_id, content,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((200, new_content))
|
||||||
|
|
||||||
|
|
||||||
|
class FederationGroupsRenewAttestaionServlet(BaseFederationServlet):
|
||||||
|
PATH = "/groups/(?P<group_id>[^/]*)/renew_attestation/(?P<user_id>[^/]*)$"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, origin, content, query, group_id, user_id):
|
||||||
|
# We don't need to check auth here as we check the attestation signatures
|
||||||
|
|
||||||
|
new_content = yield self.handler.on_renew_group_attestation(
|
||||||
|
origin, content, group_id, user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((200, new_content))
|
||||||
|
|
||||||
|
|
||||||
FEDERATION_SERVLET_CLASSES = (
|
FEDERATION_SERVLET_CLASSES = (
|
||||||
FederationSendServlet,
|
FederationSendServlet,
|
||||||
FederationPullServlet,
|
FederationPullServlet,
|
||||||
|
@ -635,11 +744,27 @@ FEDERATION_SERVLET_CLASSES = (
|
||||||
FederationVersionServlet,
|
FederationVersionServlet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
ROOM_LIST_CLASSES = (
|
ROOM_LIST_CLASSES = (
|
||||||
PublicRoomList,
|
PublicRoomList,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
GROUP_SERVER_SERVLET_CLASSES = (
|
||||||
|
FederationGroupsProfileServlet,
|
||||||
|
FederationGroupsRoomsServlet,
|
||||||
|
FederationGroupsUsersServlet,
|
||||||
|
FederationGroupsInviteServlet,
|
||||||
|
FederationGroupsAcceptInviteServlet,
|
||||||
|
FederationGroupsRemoveUserServlet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
GROUP_ATTESTATION_SERVLET_CLASSES = (
|
||||||
|
FederationGroupsRenewAttestaionServlet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_servlets(hs, resource, authenticator, ratelimiter):
|
def register_servlets(hs, resource, authenticator, ratelimiter):
|
||||||
for servletclass in FEDERATION_SERVLET_CLASSES:
|
for servletclass in FEDERATION_SERVLET_CLASSES:
|
||||||
servletclass(
|
servletclass(
|
||||||
|
@ -656,3 +781,19 @@ def register_servlets(hs, resource, authenticator, ratelimiter):
|
||||||
ratelimiter=ratelimiter,
|
ratelimiter=ratelimiter,
|
||||||
server_name=hs.hostname,
|
server_name=hs.hostname,
|
||||||
).register(resource)
|
).register(resource)
|
||||||
|
|
||||||
|
for servletclass in GROUP_SERVER_SERVLET_CLASSES:
|
||||||
|
servletclass(
|
||||||
|
handler=hs.get_groups_server_handler(),
|
||||||
|
authenticator=authenticator,
|
||||||
|
ratelimiter=ratelimiter,
|
||||||
|
server_name=hs.hostname,
|
||||||
|
).register(resource)
|
||||||
|
|
||||||
|
for servletclass in GROUP_ATTESTATION_SERVLET_CLASSES:
|
||||||
|
servletclass(
|
||||||
|
handler=hs.get_groups_attestation_renewer(),
|
||||||
|
authenticator=authenticator,
|
||||||
|
ratelimiter=ratelimiter,
|
||||||
|
server_name=hs.hostname,
|
||||||
|
).register(resource)
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 Vector Creations Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
from synapse.api.errors import SynapseError
|
||||||
|
from synapse.types import get_domain_from_id
|
||||||
|
from synapse.util.logcontext import preserve_fn
|
||||||
|
|
||||||
|
from signedjson.sign import sign_json
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ATTESTATION_LENGTH_MS = 3 * 24 * 60 * 60 * 1000
|
||||||
|
MIN_ATTESTATION_LENGTH_MS = 1 * 60 * 60 * 1000
|
||||||
|
UPDATE_ATTESTATION_TIME_MS = 1 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
|
||||||
|
class GroupAttestationSigning(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.keyring = hs.get_keyring()
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
self.server_name = hs.hostname
|
||||||
|
self.signing_key = hs.config.signing_key[0]
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def verify_attestation(self, attestation, group_id, user_id, server_name=None):
|
||||||
|
if not server_name:
|
||||||
|
if get_domain_from_id(group_id) == self.server_name:
|
||||||
|
server_name = get_domain_from_id(user_id)
|
||||||
|
else:
|
||||||
|
server_name = get_domain_from_id(group_id)
|
||||||
|
|
||||||
|
if user_id != attestation["user_id"]:
|
||||||
|
raise SynapseError(400, "Attestation has incorrect user_id")
|
||||||
|
|
||||||
|
if group_id != attestation["group_id"]:
|
||||||
|
raise SynapseError(400, "Attestation has incorrect group_id")
|
||||||
|
|
||||||
|
valid_until_ms = attestation["valid_until_ms"]
|
||||||
|
if valid_until_ms - self.clock.time_msec() < MIN_ATTESTATION_LENGTH_MS:
|
||||||
|
raise SynapseError(400, "Attestation not valid for long enough")
|
||||||
|
|
||||||
|
yield self.keyring.verify_json_for_server(server_name, attestation)
|
||||||
|
|
||||||
|
def create_attestation(self, group_id, user_id):
|
||||||
|
return sign_json({
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"valid_until_ms": self.clock.time_msec() + DEFAULT_ATTESTATION_LENGTH_MS,
|
||||||
|
}, self.server_name, self.signing_key)
|
||||||
|
|
||||||
|
|
||||||
|
class GroupAttestionRenewer(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
self.assestations = hs.get_groups_attestation_signing()
|
||||||
|
self.transport_client = hs.get_federation_transport_client()
|
||||||
|
|
||||||
|
self._renew_attestations_loop = self.clock.looping_call(
|
||||||
|
self._renew_attestations, 30 * 60 * 1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_renew_attestation(self, group_id, user_id, content):
|
||||||
|
attestation = content["attestation"]
|
||||||
|
|
||||||
|
yield self.attestations.verify_attestation(
|
||||||
|
attestation,
|
||||||
|
user_id=user_id,
|
||||||
|
group_id=group_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.store.update_remote_attestion(group_id, user_id, attestation)
|
||||||
|
|
||||||
|
defer.returnValue({})
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _renew_attestations(self):
|
||||||
|
now = self.clock.time_msec()
|
||||||
|
|
||||||
|
rows = yield self.store.get_attestations_need_renewals(
|
||||||
|
now + UPDATE_ATTESTATION_TIME_MS
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _renew_attestation(self, group_id, user_id):
|
||||||
|
attestation = self.attestations.create_attestation(group_id, user_id)
|
||||||
|
|
||||||
|
if self.hs.is_mine_id(group_id):
|
||||||
|
destination = get_domain_from_id(user_id)
|
||||||
|
else:
|
||||||
|
destination = get_domain_from_id(group_id)
|
||||||
|
|
||||||
|
yield self.transport_client.renew_group_attestation(
|
||||||
|
destination, group_id, user_id,
|
||||||
|
content={"attestation": attestation},
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.store.update_attestation_renewal(
|
||||||
|
group_id, user_id, attestation
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
group_id = row["group_id"]
|
||||||
|
user_id = row["user_id"]
|
||||||
|
|
||||||
|
preserve_fn(_renew_attestation)(group_id, user_id)
|
|
@ -0,0 +1,382 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 Vector Creations Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
from synapse.api.errors import SynapseError
|
||||||
|
from synapse.types import UserID, get_domain_from_id
|
||||||
|
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Allow users to "knock" or simpkly join depending on rules
|
||||||
|
# TODO: Federation admin APIs
|
||||||
|
# TODO: is_priveged flag to users and is_public to users and rooms
|
||||||
|
# TODO: Audit log for admins (profile updates, membership changes, users who tried
|
||||||
|
# to join but were rejected, etc)
|
||||||
|
# TODO: Flairs
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE_ATTESTATION_TIME_MS = 1 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
|
||||||
|
def check_group_is_ours(and_exists=False):
|
||||||
|
def g(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def h(self, group_id, *args, **kwargs):
|
||||||
|
if not self.is_mine_id(group_id):
|
||||||
|
raise SynapseError(400, "Group not on this server")
|
||||||
|
if and_exists:
|
||||||
|
group = yield self.store.get_group(group_id)
|
||||||
|
if not group:
|
||||||
|
raise SynapseError(404, "Unknown group")
|
||||||
|
|
||||||
|
res = yield func(self, group_id, *args, **kwargs)
|
||||||
|
defer.returnValue(res)
|
||||||
|
|
||||||
|
return h
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
class GroupsServerHandler(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.hs = hs
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
self.room_list_handler = hs.get_room_list_handler()
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
self.keyring = hs.get_keyring()
|
||||||
|
self.is_mine_id = hs.is_mine_id
|
||||||
|
self.signing_key = hs.config.signing_key[0]
|
||||||
|
self.server_name = hs.hostname
|
||||||
|
self.attestations = hs.get_groups_attestation_signing()
|
||||||
|
self.transport_client = hs.get_federation_transport_client()
|
||||||
|
|
||||||
|
# Ensure attestations get renewed
|
||||||
|
hs.get_groups_attestation_renewer()
|
||||||
|
|
||||||
|
@check_group_is_ours()
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_group_profile(self, group_id, requester_user_id):
|
||||||
|
group_description = yield self.store.get_group(group_id)
|
||||||
|
|
||||||
|
if group_description:
|
||||||
|
defer.returnValue(group_description)
|
||||||
|
else:
|
||||||
|
raise SynapseError(404, "Unknown group")
|
||||||
|
|
||||||
|
@check_group_is_ours(and_exists=True)
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_users_in_group(self, group_id, requester_user_id):
|
||||||
|
is_user_in_group = yield self.store.is_user_in_group(requester_user_id, group_id)
|
||||||
|
|
||||||
|
user_results = yield self.store.get_users_in_group(
|
||||||
|
group_id, include_private=is_user_in_group,
|
||||||
|
)
|
||||||
|
|
||||||
|
chunk = []
|
||||||
|
for user_result in user_results:
|
||||||
|
g_user_id = user_result["user_id"]
|
||||||
|
is_public = user_result["is_public"]
|
||||||
|
|
||||||
|
entry = {"user_id": g_user_id}
|
||||||
|
|
||||||
|
# TODO: Get profile information
|
||||||
|
|
||||||
|
if not is_public:
|
||||||
|
entry["is_public"] = False
|
||||||
|
|
||||||
|
if not self.is_mine_id(requester_user_id):
|
||||||
|
attestation = yield self.store.get_remote_attestation(group_id, g_user_id)
|
||||||
|
if not attestation:
|
||||||
|
continue
|
||||||
|
|
||||||
|
entry["attestation"] = attestation
|
||||||
|
else:
|
||||||
|
entry["attestation"] = self.attestations.create_attestation(
|
||||||
|
group_id, g_user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
chunk.append(entry)
|
||||||
|
|
||||||
|
# TODO: If admin add lists of users whose attestations have timed out
|
||||||
|
|
||||||
|
defer.returnValue({
|
||||||
|
"chunk": chunk,
|
||||||
|
"total_user_count_estimate": len(user_results),
|
||||||
|
})
|
||||||
|
|
||||||
|
@check_group_is_ours(and_exists=True)
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_rooms_in_group(self, group_id, requester_user_id):
|
||||||
|
is_user_in_group = yield self.store.is_user_in_group(requester_user_id, group_id)
|
||||||
|
|
||||||
|
room_results = yield self.store.get_rooms_in_group(
|
||||||
|
group_id, include_private=is_user_in_group,
|
||||||
|
)
|
||||||
|
|
||||||
|
chunk = []
|
||||||
|
for room_result in room_results:
|
||||||
|
room_id = room_result["room_id"]
|
||||||
|
is_public = room_result["is_public"]
|
||||||
|
|
||||||
|
joined_users = yield self.store.get_users_in_room(room_id)
|
||||||
|
entry = yield self.room_list_handler.generate_room_entry(
|
||||||
|
room_id, len(joined_users),
|
||||||
|
with_alias=False, allow_private=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not entry:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not is_public:
|
||||||
|
entry["is_public"] = False
|
||||||
|
|
||||||
|
chunk.append(entry)
|
||||||
|
|
||||||
|
chunk.sort(key=lambda e: -e["num_joined_members"])
|
||||||
|
|
||||||
|
defer.returnValue({
|
||||||
|
"chunk": chunk,
|
||||||
|
"total_room_count_estimate": len(room_results),
|
||||||
|
})
|
||||||
|
|
||||||
|
@check_group_is_ours(and_exists=True)
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def add_room(self, group_id, requester_user_id, room_id, content):
|
||||||
|
is_admin = yield self.store.is_user_admin_in_group(group_id, requester_user_id)
|
||||||
|
if not is_admin:
|
||||||
|
raise SynapseError(403, "User is not admin in group")
|
||||||
|
|
||||||
|
# TODO: Check if room has already been added
|
||||||
|
|
||||||
|
visibility = content.get("visibility")
|
||||||
|
if visibility:
|
||||||
|
vis_type = visibility["type"]
|
||||||
|
if vis_type not in ("public", "private"):
|
||||||
|
raise SynapseError(
|
||||||
|
400, "Synapse only supports 'public'/'private' visibility"
|
||||||
|
)
|
||||||
|
is_public = vis_type == "public"
|
||||||
|
else:
|
||||||
|
is_public = True
|
||||||
|
|
||||||
|
yield self.store.add_room_to_group(group_id, room_id, is_public=is_public)
|
||||||
|
|
||||||
|
defer.returnValue({})
|
||||||
|
|
||||||
|
@check_group_is_ours(and_exists=True)
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def invite_to_group(self, group_id, user_id, requester_user_id, content):
|
||||||
|
is_admin = yield self.store.is_user_admin_in_group(
|
||||||
|
group_id, requester_user_id
|
||||||
|
)
|
||||||
|
if not is_admin:
|
||||||
|
raise SynapseError(403, "User is not admin in group")
|
||||||
|
|
||||||
|
# TODO: Check if user knocked
|
||||||
|
# TODO: Check if user is already invited
|
||||||
|
|
||||||
|
group = yield self.store.get_group(group_id)
|
||||||
|
content = {
|
||||||
|
"profile": {
|
||||||
|
"name": group["name"],
|
||||||
|
"avatar_url": group["avatar_url"],
|
||||||
|
},
|
||||||
|
"inviter": requester_user_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.hs.is_mine_id(user_id):
|
||||||
|
raise NotImplementedError()
|
||||||
|
else:
|
||||||
|
local_attestation = self.attestations.create_attestation(group_id, user_id)
|
||||||
|
content.update({
|
||||||
|
"attestation": local_attestation,
|
||||||
|
})
|
||||||
|
|
||||||
|
res = yield self.transport_client.invite_to_group_notification(
|
||||||
|
get_domain_from_id(user_id), group_id, user_id, content
|
||||||
|
)
|
||||||
|
|
||||||
|
if res["state"] == "join":
|
||||||
|
if not self.hs.is_mine_id(user_id):
|
||||||
|
remote_attestation = res["attestation"]
|
||||||
|
|
||||||
|
yield self.attestations.verify_attestation(
|
||||||
|
remote_attestation,
|
||||||
|
user_id=user_id,
|
||||||
|
group_id=group_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
remote_attestation = None
|
||||||
|
|
||||||
|
yield self.store.add_user_to_group(
|
||||||
|
group_id, user_id,
|
||||||
|
is_admin=False,
|
||||||
|
is_public=False, # TODO
|
||||||
|
local_attestation=local_attestation,
|
||||||
|
remote_attestation=remote_attestation,
|
||||||
|
)
|
||||||
|
elif res["state"] == "invite":
|
||||||
|
yield self.store.add_group_invite(
|
||||||
|
group_id, user_id,
|
||||||
|
)
|
||||||
|
defer.returnValue({
|
||||||
|
"state": "invite"
|
||||||
|
})
|
||||||
|
elif res["state"] == "reject":
|
||||||
|
defer.returnValue({
|
||||||
|
"state": "reject"
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
raise SynapseError(502, "Unknown state returned by HS")
|
||||||
|
|
||||||
|
@check_group_is_ours(and_exists=True)
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def accept_invite(self, group_id, user_id, content):
|
||||||
|
if not self.store.is_user_invited_to_local_group(group_id, user_id):
|
||||||
|
raise SynapseError(403, "User not invited to group")
|
||||||
|
|
||||||
|
if not self.hs.is_mine_id(user_id):
|
||||||
|
remote_attestation = content["attestation"]
|
||||||
|
|
||||||
|
yield self.attestations.verify_attestation(
|
||||||
|
remote_attestation,
|
||||||
|
user_id=user_id,
|
||||||
|
group_id=group_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
remote_attestation = None
|
||||||
|
|
||||||
|
local_attestation = self.attestations.create_attestation(group_id, user_id)
|
||||||
|
|
||||||
|
visibility = content.get("visibility")
|
||||||
|
if visibility:
|
||||||
|
vis_type = visibility["type"]
|
||||||
|
if vis_type not in ("public", "private"):
|
||||||
|
raise SynapseError(
|
||||||
|
400, "Synapse only supports 'public'/'private' visibility"
|
||||||
|
)
|
||||||
|
is_public = vis_type == "public"
|
||||||
|
else:
|
||||||
|
is_public = True
|
||||||
|
|
||||||
|
yield self.store.add_user_to_group(
|
||||||
|
group_id, user_id,
|
||||||
|
is_admin=False,
|
||||||
|
is_public=is_public,
|
||||||
|
local_attestation=local_attestation,
|
||||||
|
remote_attestation=remote_attestation,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue({
|
||||||
|
"state": "join",
|
||||||
|
"attestation": local_attestation,
|
||||||
|
})
|
||||||
|
|
||||||
|
@check_group_is_ours(and_exists=True)
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def knock(self, group_id, user_id, content):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@check_group_is_ours(and_exists=True)
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def accept_knock(self, group_id, user_id, content):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@check_group_is_ours(and_exists=True)
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def remove_user_from_group(self, group_id, user_id, requester_user_id, content):
|
||||||
|
is_kick = False
|
||||||
|
if requester_user_id != user_id:
|
||||||
|
is_admin = yield self.store.is_user_admin_in_group(
|
||||||
|
group_id, requester_user_id
|
||||||
|
)
|
||||||
|
if not is_admin:
|
||||||
|
raise SynapseError(403, "User is not admin in group")
|
||||||
|
|
||||||
|
is_kick = True
|
||||||
|
|
||||||
|
yield self.store.remove_user_to_group(
|
||||||
|
group_id, user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_kick:
|
||||||
|
if self.hs.is_mine_id(user_id):
|
||||||
|
raise NotImplementedError()
|
||||||
|
else:
|
||||||
|
yield self.transport_client.remove_user_from_group_notification(
|
||||||
|
get_domain_from_id(user_id), group_id, user_id, {}
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue({})
|
||||||
|
|
||||||
|
@check_group_is_ours()
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def create_group(self, group_id, user_id, content):
|
||||||
|
logger.info("Attempting to create group with ID: %r", group_id)
|
||||||
|
group = yield self.store.get_group(group_id)
|
||||||
|
if group:
|
||||||
|
raise SynapseError(400, "Group already exists")
|
||||||
|
|
||||||
|
is_admin = yield self.auth.is_server_admin(UserID.from_string(user_id))
|
||||||
|
if not is_admin and not group_id.startswith("+u/"):
|
||||||
|
raise SynapseError(403, "Group ID must start with '+u/' or be a server admin")
|
||||||
|
|
||||||
|
profile = content.get("profile", {})
|
||||||
|
name = profile.get("name")
|
||||||
|
avatar_url = profile.get("avatar_url")
|
||||||
|
short_description = profile.get("short_description")
|
||||||
|
long_description = profile.get("long_description")
|
||||||
|
|
||||||
|
yield self.store.create_group(
|
||||||
|
group_id,
|
||||||
|
user_id,
|
||||||
|
name=name,
|
||||||
|
avatar_url=avatar_url,
|
||||||
|
short_description=short_description,
|
||||||
|
long_description=long_description,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.hs.is_mine_id(user_id):
|
||||||
|
remote_attestation = content["attestation"]
|
||||||
|
|
||||||
|
yield self.attestations.verify_attestation(
|
||||||
|
remote_attestation,
|
||||||
|
user_id=user_id,
|
||||||
|
group_id=group_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
local_attestation = self.attestations.create_attestation(group_id, user_id)
|
||||||
|
else:
|
||||||
|
local_attestation = None
|
||||||
|
remote_attestation = None
|
||||||
|
|
||||||
|
yield self.store.add_user_to_group(
|
||||||
|
group_id, user_id,
|
||||||
|
is_admin=True,
|
||||||
|
is_public=True, # TODO
|
||||||
|
local_attestation=local_attestation,
|
||||||
|
remote_attestation=remote_attestation,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue({
|
||||||
|
"group_id": group_id,
|
||||||
|
})
|
|
@ -276,13 +276,14 @@ class RoomListHandler(BaseHandler):
|
||||||
# We've already got enough, so lets just drop it.
|
# We've already got enough, so lets just drop it.
|
||||||
return
|
return
|
||||||
|
|
||||||
result = yield self._generate_room_entry(room_id, num_joined_users)
|
result = yield self.generate_room_entry(room_id, num_joined_users)
|
||||||
|
|
||||||
if result and _matches_room_entry(result, search_filter):
|
if result and _matches_room_entry(result, search_filter):
|
||||||
chunk.append(result)
|
chunk.append(result)
|
||||||
|
|
||||||
@cachedInlineCallbacks(num_args=1, cache_context=True)
|
@cachedInlineCallbacks(num_args=1, cache_context=True)
|
||||||
def _generate_room_entry(self, room_id, num_joined_users, cache_context):
|
def generate_room_entry(self, room_id, num_joined_users, cache_context,
|
||||||
|
with_alias=True, allow_private=False):
|
||||||
"""Returns the entry for a room
|
"""Returns the entry for a room
|
||||||
"""
|
"""
|
||||||
result = {
|
result = {
|
||||||
|
@ -316,14 +317,15 @@ class RoomListHandler(BaseHandler):
|
||||||
join_rules_event = current_state.get((EventTypes.JoinRules, ""))
|
join_rules_event = current_state.get((EventTypes.JoinRules, ""))
|
||||||
if join_rules_event:
|
if join_rules_event:
|
||||||
join_rule = join_rules_event.content.get("join_rule", None)
|
join_rule = join_rules_event.content.get("join_rule", None)
|
||||||
if join_rule and join_rule != JoinRules.PUBLIC:
|
if not allow_private and join_rule and join_rule != JoinRules.PUBLIC:
|
||||||
defer.returnValue(None)
|
defer.returnValue(None)
|
||||||
|
|
||||||
aliases = yield self.store.get_aliases_for_room(
|
if with_alias:
|
||||||
room_id, on_invalidate=cache_context.invalidate
|
aliases = yield self.store.get_aliases_for_room(
|
||||||
)
|
room_id, on_invalidate=cache_context.invalidate
|
||||||
if aliases:
|
)
|
||||||
result["aliases"] = aliases
|
if aliases:
|
||||||
|
result["aliases"] = aliases
|
||||||
|
|
||||||
name_event = yield current_state.get((EventTypes.Name, ""))
|
name_event = yield current_state.get((EventTypes.Name, ""))
|
||||||
if name_event:
|
if name_event:
|
||||||
|
|
|
@ -145,7 +145,9 @@ def wrap_request_handler(request_handler, include_metrics=False):
|
||||||
"error": "Internal server error",
|
"error": "Internal server error",
|
||||||
"errcode": Codes.UNKNOWN,
|
"errcode": Codes.UNKNOWN,
|
||||||
},
|
},
|
||||||
send_cors=True
|
send_cors=True,
|
||||||
|
pretty_print=_request_user_agent_is_curl(request),
|
||||||
|
version_string=self.version_string,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -50,6 +50,8 @@ from synapse.handlers.initial_sync import InitialSyncHandler
|
||||||
from synapse.handlers.receipts import ReceiptsHandler
|
from synapse.handlers.receipts import ReceiptsHandler
|
||||||
from synapse.handlers.read_marker import ReadMarkerHandler
|
from synapse.handlers.read_marker import ReadMarkerHandler
|
||||||
from synapse.handlers.user_directory import UserDirectoyHandler
|
from synapse.handlers.user_directory import UserDirectoyHandler
|
||||||
|
from synapse.groups.groups_server import GroupsServerHandler
|
||||||
|
from synapse.groups.attestations import GroupAttestionRenewer, GroupAttestationSigning
|
||||||
from synapse.http.client import SimpleHttpClient, InsecureInterceptableContextFactory
|
from synapse.http.client import SimpleHttpClient, InsecureInterceptableContextFactory
|
||||||
from synapse.http.matrixfederationclient import MatrixFederationHttpClient
|
from synapse.http.matrixfederationclient import MatrixFederationHttpClient
|
||||||
from synapse.notifier import Notifier
|
from synapse.notifier import Notifier
|
||||||
|
@ -139,6 +141,9 @@ class HomeServer(object):
|
||||||
'read_marker_handler',
|
'read_marker_handler',
|
||||||
'action_generator',
|
'action_generator',
|
||||||
'user_directory_handler',
|
'user_directory_handler',
|
||||||
|
'groups_server_handler',
|
||||||
|
'groups_attestation_signing',
|
||||||
|
'groups_attestation_renewer',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, hostname, **kwargs):
|
def __init__(self, hostname, **kwargs):
|
||||||
|
@ -309,6 +314,15 @@ class HomeServer(object):
|
||||||
def build_user_directory_handler(self):
|
def build_user_directory_handler(self):
|
||||||
return UserDirectoyHandler(self)
|
return UserDirectoyHandler(self)
|
||||||
|
|
||||||
|
def build_groups_server_handler(self):
|
||||||
|
return GroupsServerHandler(self)
|
||||||
|
|
||||||
|
def build_groups_attestation_signing(self):
|
||||||
|
return GroupAttestationSigning(self)
|
||||||
|
|
||||||
|
def build_groups_attestation_renewer(self):
|
||||||
|
return GroupAttestionRenewer(self)
|
||||||
|
|
||||||
def remove_pusher(self, app_id, push_key, user_id):
|
def remove_pusher(self, app_id, push_key, user_id):
|
||||||
return self.get_pusherpool().remove_pusher(app_id, push_key, user_id)
|
return self.get_pusherpool().remove_pusher(app_id, push_key, user_id)
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ from .media_repository import MediaRepositoryStore
|
||||||
from .rejections import RejectionsStore
|
from .rejections import RejectionsStore
|
||||||
from .event_push_actions import EventPushActionsStore
|
from .event_push_actions import EventPushActionsStore
|
||||||
from .deviceinbox import DeviceInboxStore
|
from .deviceinbox import DeviceInboxStore
|
||||||
|
from .group_server import GroupServerStore
|
||||||
from .state import StateStore
|
from .state import StateStore
|
||||||
from .signatures import SignatureStore
|
from .signatures import SignatureStore
|
||||||
from .filtering import FilteringStore
|
from .filtering import FilteringStore
|
||||||
|
@ -88,6 +88,7 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||||
DeviceStore,
|
DeviceStore,
|
||||||
DeviceInboxStore,
|
DeviceInboxStore,
|
||||||
UserDirectoryStore,
|
UserDirectoryStore,
|
||||||
|
GroupServerStore,
|
||||||
):
|
):
|
||||||
|
|
||||||
def __init__(self, db_conn, hs):
|
def __init__(self, db_conn, hs):
|
||||||
|
|
|
@ -0,0 +1,280 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 Vector Creations Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
from ._base import SQLBaseStore
|
||||||
|
|
||||||
|
import ujson as json
|
||||||
|
|
||||||
|
|
||||||
|
class GroupServerStore(SQLBaseStore):
|
||||||
|
def get_group(self, group_id):
|
||||||
|
return self._simple_select_one(
|
||||||
|
table="groups",
|
||||||
|
keyvalues={
|
||||||
|
"group_id": group_id,
|
||||||
|
},
|
||||||
|
retcols=("name", "short_description", "long_description", "avatar_url",),
|
||||||
|
allow_none=True,
|
||||||
|
desc="is_user_in_group",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_users_in_group(self, group_id, include_private=False):
|
||||||
|
# TODO: Pagination
|
||||||
|
|
||||||
|
keyvalues = {
|
||||||
|
"group_id": group_id,
|
||||||
|
}
|
||||||
|
if not include_private:
|
||||||
|
keyvalues["is_public"] = True
|
||||||
|
|
||||||
|
return self._simple_select_list(
|
||||||
|
table="group_users",
|
||||||
|
keyvalues=keyvalues,
|
||||||
|
retcols=("user_id", "is_public",),
|
||||||
|
desc="get_users_in_group",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_rooms_in_group(self, group_id, include_private=False):
|
||||||
|
# TODO: Pagination
|
||||||
|
|
||||||
|
keyvalues = {
|
||||||
|
"group_id": group_id,
|
||||||
|
}
|
||||||
|
if not include_private:
|
||||||
|
keyvalues["is_public"] = True
|
||||||
|
|
||||||
|
return self._simple_select_list(
|
||||||
|
table="group_rooms",
|
||||||
|
keyvalues=keyvalues,
|
||||||
|
retcols=("room_id", "is_public",),
|
||||||
|
desc="get_rooms_in_group",
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_user_in_group(self, user_id, group_id):
|
||||||
|
return self._simple_select_one_onecol(
|
||||||
|
table="group_users",
|
||||||
|
keyvalues={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
retcol="user_id",
|
||||||
|
allow_none=True,
|
||||||
|
desc="is_user_in_group",
|
||||||
|
).addCallback(lambda r: bool(r))
|
||||||
|
|
||||||
|
def is_user_admin_in_group(self, group_id, user_id):
|
||||||
|
return self._simple_select_one_onecol(
|
||||||
|
table="group_users",
|
||||||
|
keyvalues={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
retcol="is_admin",
|
||||||
|
allow_none=True,
|
||||||
|
desc="is_user_adim_in_group",
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_group_invite(self, group_id, user_id):
|
||||||
|
return self._simple_insert(
|
||||||
|
table="group_invites",
|
||||||
|
values={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
desc="add_group_invite",
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_user_invited_to_local_group(self, group_id, user_id):
|
||||||
|
return self._simple_select_one_onecol(
|
||||||
|
table="group_invites",
|
||||||
|
keyvalues={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
retcol="user_id",
|
||||||
|
desc="is_user_invited_to_local_group",
|
||||||
|
allow_none=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_user_to_group(self, group_id, user_id, is_admin=False, is_public=True,
|
||||||
|
local_attestation=None, remote_attestation=None):
|
||||||
|
def _add_user_to_group_txn(txn):
|
||||||
|
self._simple_insert_txn(
|
||||||
|
txn,
|
||||||
|
table="group_users",
|
||||||
|
values={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"is_admin": is_admin,
|
||||||
|
"is_public": is_public,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self._simple_delete_txn(
|
||||||
|
txn,
|
||||||
|
table="group_invites",
|
||||||
|
keyvalues={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if local_attestation:
|
||||||
|
self._simple_insert_txn(
|
||||||
|
txn,
|
||||||
|
table="group_attestations_renewals",
|
||||||
|
values={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"valid_until_ms": local_attestation["valid_until_ms"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if remote_attestation:
|
||||||
|
self._simple_insert_txn(
|
||||||
|
txn,
|
||||||
|
table="group_attestations_remote",
|
||||||
|
values={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"valid_until_ms": remote_attestation["valid_until_ms"],
|
||||||
|
"attestation": json.dumps(remote_attestation),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.runInteraction(
|
||||||
|
"add_user_to_group", _add_user_to_group_txn
|
||||||
|
)
|
||||||
|
|
||||||
|
def remove_user_to_group(self, group_id, user_id):
|
||||||
|
def _remove_user_to_group_txn(txn):
|
||||||
|
self._simple_delete_txn(
|
||||||
|
txn,
|
||||||
|
table="group_users",
|
||||||
|
keyvalues={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self._simple_delete_txn(
|
||||||
|
txn,
|
||||||
|
table="group_invites",
|
||||||
|
keyvalues={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self._simple_delete_txn(
|
||||||
|
txn,
|
||||||
|
table="group_attestations_renewals",
|
||||||
|
keyvalues={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self._simple_delete_txn(
|
||||||
|
txn,
|
||||||
|
table="group_attestations_remote",
|
||||||
|
keyvalues={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return self.runInteraction("remove_user_to_group", _remove_user_to_group_txn)
|
||||||
|
|
||||||
|
def add_room_to_group(self, group_id, room_id, is_public):
|
||||||
|
return self._simple_insert(
|
||||||
|
table="group_rooms",
|
||||||
|
values={
|
||||||
|
"group_id": group_id,
|
||||||
|
"room_id": room_id,
|
||||||
|
"is_public": is_public,
|
||||||
|
},
|
||||||
|
desc="add_room_to_group",
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def create_group(self, group_id, user_id, name, avatar_url, short_description,
|
||||||
|
long_description,):
|
||||||
|
yield self._simple_insert(
|
||||||
|
table="groups",
|
||||||
|
values={
|
||||||
|
"group_id": group_id,
|
||||||
|
"name": name,
|
||||||
|
"avatar_url": avatar_url,
|
||||||
|
"short_description": short_description,
|
||||||
|
"long_description": long_description,
|
||||||
|
},
|
||||||
|
desc="create_group",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_attestations_need_renewals(self, valid_until_ms):
|
||||||
|
def _get_attestations_need_renewals_txn(txn):
|
||||||
|
sql = """
|
||||||
|
SELECT group_id, user_id FROM group_attestations_renewals
|
||||||
|
WHERE valid_until_ms <= ?
|
||||||
|
"""
|
||||||
|
txn.execute(sql, (valid_until_ms,))
|
||||||
|
return self.cursor_to_dict(txn)
|
||||||
|
return self.runInteraction(
|
||||||
|
"get_attestations_need_renewals", _get_attestations_need_renewals_txn
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_attestation_renewal(self, group_id, user_id, attestation):
|
||||||
|
return self._simple_update_one(
|
||||||
|
table="group_attestations_renewals",
|
||||||
|
keyvalues={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
updatevalues={
|
||||||
|
"valid_until_ms": attestation["valid_until_ms"],
|
||||||
|
},
|
||||||
|
desc="update_attestation_renewal",
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_remote_attestion(self, group_id, user_id, attestation):
|
||||||
|
return self._simple_update_one(
|
||||||
|
table="group_attestations_remote",
|
||||||
|
keyvalues={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
updatevalues={
|
||||||
|
"valid_until_ms": attestation["valid_until_ms"],
|
||||||
|
"attestation": json.dumps(attestation)
|
||||||
|
},
|
||||||
|
desc="update_remote_attestion",
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_remote_attestation(self, group_id, user_id):
|
||||||
|
row = yield self._simple_select_one(
|
||||||
|
table="group_attestations_remote",
|
||||||
|
keyvalues={
|
||||||
|
"group_id": group_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
retcols=("valid_until_ms", "attestation"),
|
||||||
|
desc="get_remote_attestation",
|
||||||
|
allow_none=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
now = int(self._clock.time_msec())
|
||||||
|
if row and now < row["valid_until_ms"]:
|
||||||
|
defer.returnValue(json.loads(row["attestation"]))
|
||||||
|
|
||||||
|
defer.returnValue(None)
|
|
@ -0,0 +1,77 @@
|
||||||
|
/* Copyright 2017 Vector Creations 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
CREATE TABLE groups (
|
||||||
|
group_id TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
avatar_url TEXT,
|
||||||
|
short_description TEXT,
|
||||||
|
long_description TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX groups_idx ON groups(group_id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE group_users (
|
||||||
|
group_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
is_admin BOOLEAN NOT NULL,
|
||||||
|
is_public BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE INDEX groups_users_g_idx ON group_users(group_id, user_id);
|
||||||
|
CREATE INDEX groups_users_u_idx ON group_users(user_id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE group_invites (
|
||||||
|
group_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX groups_invites_g_idx ON group_invites(group_id, user_id);
|
||||||
|
CREATE INDEX groups_invites_u_idx ON group_invites(user_id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE group_rooms (
|
||||||
|
group_id TEXT NOT NULL,
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
is_public BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX groups_rooms_g_idx ON group_rooms(group_id, room_id);
|
||||||
|
CREATE INDEX groups_rooms_r_idx ON group_rooms(room_id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE group_attestations_renewals (
|
||||||
|
group_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
valid_until_ms BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX group_attestations_renewals_g_idx ON group_attestations_renewals(group_id, user_id);
|
||||||
|
CREATE INDEX group_attestations_renewals_u_idx ON group_attestations_renewals(user_id);
|
||||||
|
CREATE INDEX group_attestations_renewals_v_idx ON group_attestations_renewals(valid_until_ms);
|
||||||
|
|
||||||
|
CREATE TABLE group_attestations_remote (
|
||||||
|
group_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
valid_until_ms BIGINT NOT NULL,
|
||||||
|
attestation TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX group_attestations_remote_g_idx ON group_attestations_remote(group_id, user_id);
|
||||||
|
CREATE INDEX group_attestations_remote_u_idx ON group_attestations_remote(user_id);
|
||||||
|
CREATE INDEX group_attestations_remote_v_idx ON group_attestations_remote(valid_until_ms);
|
Loading…
Reference in New Issue