Initial group server implementation
parent
d4d12daed9
commit
b8ca494ee9
|
@ -471,3 +471,37 @@ class TransportLayerClient(object):
|
|||
)
|
||||
|
||||
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.versionstring import get_version_string
|
||||
from synapse.util.logcontext import preserve_fn
|
||||
from synapse.types import ThirdPartyInstanceID
|
||||
from synapse.types import ThirdPartyInstanceID, get_domain_from_id
|
||||
|
||||
import functools
|
||||
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 = (
|
||||
FederationSendServlet,
|
||||
FederationPullServlet,
|
||||
|
@ -635,11 +744,27 @@ FEDERATION_SERVLET_CLASSES = (
|
|||
FederationVersionServlet,
|
||||
)
|
||||
|
||||
|
||||
ROOM_LIST_CLASSES = (
|
||||
PublicRoomList,
|
||||
)
|
||||
|
||||
|
||||
GROUP_SERVER_SERVLET_CLASSES = (
|
||||
FederationGroupsProfileServlet,
|
||||
FederationGroupsRoomsServlet,
|
||||
FederationGroupsUsersServlet,
|
||||
FederationGroupsInviteServlet,
|
||||
FederationGroupsAcceptInviteServlet,
|
||||
FederationGroupsRemoveUserServlet,
|
||||
)
|
||||
|
||||
|
||||
GROUP_ATTESTATION_SERVLET_CLASSES = (
|
||||
FederationGroupsRenewAttestaionServlet,
|
||||
)
|
||||
|
||||
|
||||
def register_servlets(hs, resource, authenticator, ratelimiter):
|
||||
for servletclass in FEDERATION_SERVLET_CLASSES:
|
||||
servletclass(
|
||||
|
@ -656,3 +781,19 @@ def register_servlets(hs, resource, authenticator, ratelimiter):
|
|||
ratelimiter=ratelimiter,
|
||||
server_name=hs.hostname,
|
||||
).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.
|
||||
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):
|
||||
chunk.append(result)
|
||||
|
||||
@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
|
||||
"""
|
||||
result = {
|
||||
|
@ -316,14 +317,15 @@ class RoomListHandler(BaseHandler):
|
|||
join_rules_event = current_state.get((EventTypes.JoinRules, ""))
|
||||
if join_rules_event:
|
||||
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)
|
||||
|
||||
aliases = yield self.store.get_aliases_for_room(
|
||||
room_id, on_invalidate=cache_context.invalidate
|
||||
)
|
||||
if aliases:
|
||||
result["aliases"] = aliases
|
||||
if with_alias:
|
||||
aliases = yield self.store.get_aliases_for_room(
|
||||
room_id, on_invalidate=cache_context.invalidate
|
||||
)
|
||||
if aliases:
|
||||
result["aliases"] = aliases
|
||||
|
||||
name_event = yield current_state.get((EventTypes.Name, ""))
|
||||
if name_event:
|
||||
|
|
|
@ -145,7 +145,9 @@ def wrap_request_handler(request_handler, include_metrics=False):
|
|||
"error": "Internal server error",
|
||||
"errcode": Codes.UNKNOWN,
|
||||
},
|
||||
send_cors=True
|
||||
send_cors=True,
|
||||
pretty_print=_request_user_agent_is_curl(request),
|
||||
version_string=self.version_string,
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
|
|
|
@ -50,6 +50,8 @@ from synapse.handlers.initial_sync import InitialSyncHandler
|
|||
from synapse.handlers.receipts import ReceiptsHandler
|
||||
from synapse.handlers.read_marker import ReadMarkerHandler
|
||||
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.matrixfederationclient import MatrixFederationHttpClient
|
||||
from synapse.notifier import Notifier
|
||||
|
@ -139,6 +141,9 @@ class HomeServer(object):
|
|||
'read_marker_handler',
|
||||
'action_generator',
|
||||
'user_directory_handler',
|
||||
'groups_server_handler',
|
||||
'groups_attestation_signing',
|
||||
'groups_attestation_renewer',
|
||||
]
|
||||
|
||||
def __init__(self, hostname, **kwargs):
|
||||
|
@ -309,6 +314,15 @@ class HomeServer(object):
|
|||
def build_user_directory_handler(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):
|
||||
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 .event_push_actions import EventPushActionsStore
|
||||
from .deviceinbox import DeviceInboxStore
|
||||
|
||||
from .group_server import GroupServerStore
|
||||
from .state import StateStore
|
||||
from .signatures import SignatureStore
|
||||
from .filtering import FilteringStore
|
||||
|
@ -88,6 +88,7 @@ class DataStore(RoomMemberStore, RoomStore,
|
|||
DeviceStore,
|
||||
DeviceInboxStore,
|
||||
UserDirectoryStore,
|
||||
GroupServerStore,
|
||||
):
|
||||
|
||||
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