forked-synapse/synapse/groups/groups_server.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1020 lines
35 KiB
Python
Raw Normal View History

2017-07-10 14:44:15 +00:00
# Copyright 2017 Vector Creations Ltd
# Copyright 2018 New Vector Ltd
# Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
2017-07-10 14:44:15 +00:00
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
2021-02-17 13:41:47 +00:00
from typing import TYPE_CHECKING, Optional
2017-07-10 14:44:15 +00:00
from synapse.api.errors import Codes, SynapseError
2021-02-17 13:41:47 +00:00
from synapse.handlers.groups_local import GroupsLocalHandler
from synapse.handlers.profile import MAX_AVATAR_URL_LEN, MAX_DISPLAYNAME_LEN
2021-02-17 13:41:47 +00:00
from synapse.types import GroupID, JsonDict, RoomID, UserID, get_domain_from_id
2019-04-03 15:00:44 +00:00
from synapse.util.async_helpers import concurrently_execute
2021-02-17 13:41:47 +00:00
if TYPE_CHECKING:
from synapse.server import HomeServer
2021-02-17 13:41:47 +00:00
2017-07-10 14:44:15 +00:00
logger = logging.getLogger(__name__)
# TODO: Allow users to "knock" or simply join depending on rules
2017-07-10 14:44:15 +00:00
# TODO: Federation admin APIs
# TODO: is_privileged flag to users and is_public to users and rooms
2017-07-10 14:44:15 +00:00
# TODO: Audit log for admins (profile updates, membership changes, users who tried
# to join but were rejected, etc)
# TODO: Flairs
# Note that the maximum lengths are somewhat arbitrary.
MAX_SHORT_DESC_LEN = 1000
MAX_LONG_DESC_LEN = 10000
2020-09-04 10:54:56 +00:00
class GroupsServerWorkerHandler:
2021-02-17 13:41:47 +00:00
def __init__(self, hs: "HomeServer"):
2017-07-10 14:44:15 +00:00
self.hs = hs
self.store = hs.get_datastores().main
2017-07-10 14:44:15 +00:00
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.signing_key
2017-07-10 14:44:15 +00:00
self.server_name = hs.hostname
self.attestations = hs.get_groups_attestation_signing()
self.transport_client = hs.get_federation_transport_client()
self.profile_handler = hs.get_profile_handler()
2017-07-10 14:44:15 +00:00
async def check_group_is_ours(
2021-02-17 13:41:47 +00:00
self,
group_id: str,
requester_user_id: str,
and_exists: bool = False,
and_is_admin: Optional[str] = None,
) -> Optional[dict]:
2017-07-11 08:58:59 +00:00
"""Check that the group is ours, and optionally if it exists.
If group does exist then return group.
2017-07-12 13:11:59 +00:00
Args:
2021-02-17 13:41:47 +00:00
group_id: The group ID to check.
requester_user_id: The user ID of the requester.
and_exists: whether to also check if group exists
and_is_admin: whether to also check if given str is a user_id
2017-07-12 13:11:59 +00:00
that is an admin
2017-07-11 08:58:59 +00:00
"""
if not self.is_mine_id(group_id):
raise SynapseError(400, "Group not on this server")
group = await self.store.get_group(group_id)
2017-07-11 08:58:59 +00:00
if and_exists and not group:
raise SynapseError(404, "Unknown group")
is_user_in_group = await self.store.is_user_in_group(
requester_user_id, group_id
)
2017-10-27 10:08:19 +00:00
if group and not is_user_in_group and not group["is_public"]:
raise SynapseError(404, "Unknown group")
2017-07-12 10:43:39 +00:00
if and_is_admin:
is_admin = await self.store.is_user_admin_in_group(group_id, and_is_admin)
2017-07-12 10:43:39 +00:00
if not is_admin:
raise SynapseError(403, "User is not admin in group")
return group
2017-07-11 08:58:59 +00:00
2021-02-17 13:41:47 +00:00
async def get_group_summary(
self, group_id: str, requester_user_id: str
) -> JsonDict:
2017-07-12 13:11:59 +00:00
"""Get the summary for a group as seen by requester_user_id.
The group summary consists of the profile of the room, and a curated
list of users and rooms. These list *may* be organised by role/category.
The roles/categories are ordered, and so are the users/rooms within them.
A user/room may appear in multiple roles/categories.
"""
await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
2017-07-10 14:44:40 +00:00
is_user_in_group = await self.store.is_user_in_group(
2017-07-10 14:44:40 +00:00
requester_user_id, group_id
)
profile = await self.get_group_profile(group_id, requester_user_id)
2017-07-10 14:44:40 +00:00
users, roles = await self.store.get_users_for_summary_by_role(
2017-07-10 14:44:40 +00:00
group_id, include_private=is_user_in_group
)
# TODO: Add profiles to users
rooms, categories = await self.store.get_rooms_for_summary_by_category(
2017-07-10 14:44:40 +00:00
group_id, include_private=is_user_in_group
)
for room_entry in rooms:
room_id = room_entry["room_id"]
joined_users = await self.store.get_users_in_room(room_id)
entry = await self.room_list_handler.generate_room_entry(
room_id, len(joined_users), with_alias=False, allow_private=True
2017-07-10 14:44:40 +00:00
)
2021-02-17 13:41:47 +00:00
if entry is None:
continue
2020-10-23 16:38:40 +00:00
entry = dict(entry) # so we don't change what's cached
entry.pop("room_id", None)
2017-07-10 14:44:40 +00:00
room_entry["profile"] = entry
rooms.sort(key=lambda e: e.get("order", 0))
2021-02-17 13:41:47 +00:00
for user in users:
user_id = user["user_id"]
2017-07-10 14:44:40 +00:00
if not self.is_mine_id(requester_user_id):
attestation = await self.store.get_remote_attestation(group_id, user_id)
2017-07-10 14:44:40 +00:00
if not attestation:
continue
2021-02-17 13:41:47 +00:00
user["attestation"] = attestation
2017-07-10 14:44:40 +00:00
else:
2021-02-17 13:41:47 +00:00
user["attestation"] = self.attestations.create_attestation(
2017-07-10 14:44:40 +00:00
group_id, user_id
)
user_profile = await self.profile_handler.get_profile_from_cache(user_id)
2021-02-17 13:41:47 +00:00
user.update(user_profile)
2017-07-10 14:44:40 +00:00
users.sort(key=lambda e: e.get("order", 0))
membership_info = await self.store.get_users_membership_info_in_group(
group_id, requester_user_id
)
return {
"profile": profile,
"users_section": {
"users": users,
"roles": roles,
"total_user_count_estimate": 0, # TODO
},
"rooms_section": {
"rooms": rooms,
"categories": categories,
"total_room_count_estimate": 0, # TODO
},
"user": membership_info,
}
2017-07-10 14:44:40 +00:00
2021-02-17 13:41:47 +00:00
async def get_group_categories(
self, group_id: str, requester_user_id: str
) -> JsonDict:
"""Get all categories in a group (as seen by user)"""
await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
categories = await self.store.get_group_categories(group_id=group_id)
return {"categories": categories}
2021-02-17 13:41:47 +00:00
async def get_group_category(
self, group_id: str, requester_user_id: str, category_id: str
) -> JsonDict:
"""Get a specific category in a group (as seen by user)"""
await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
2021-02-17 13:41:47 +00:00
return await self.store.get_group_category(
group_id=group_id, category_id=category_id
)
2021-02-17 13:41:47 +00:00
async def get_group_roles(self, group_id: str, requester_user_id: str) -> JsonDict:
"""Get all roles in a group (as seen by user)"""
await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
roles = await self.store.get_group_roles(group_id=group_id)
return {"roles": roles}
2021-02-17 13:41:47 +00:00
async def get_group_role(
self, group_id: str, requester_user_id: str, role_id: str
) -> JsonDict:
"""Get a specific role in a group (as seen by user)"""
await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
2021-02-17 13:41:47 +00:00
return await self.store.get_group_role(group_id=group_id, role_id=role_id)
2021-02-17 13:41:47 +00:00
async def get_group_profile(
self, group_id: str, requester_user_id: str
) -> JsonDict:
"""Get the group profile as seen by requester_user_id"""
await self.check_group_is_ours(group_id, requester_user_id)
group = await self.store.get_group(group_id)
if group:
cols = [
"name",
"short_description",
"long_description",
"avatar_url",
"is_public",
]
group_description = {key: group[key] for key in cols}
group_description["is_openly_joinable"] = group["join_policy"] == "open"
return group_description
else:
raise SynapseError(404, "Unknown group")
2021-02-17 13:41:47 +00:00
async def get_users_in_group(
self, group_id: str, requester_user_id: str
) -> JsonDict:
"""Get the users in group as seen by requester_user_id.
The ordering is arbitrary at the moment
"""
await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
is_user_in_group = await self.store.is_user_in_group(
requester_user_id, group_id
)
user_results = await 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"]
is_privileged = user_result["is_admin"]
entry = {"user_id": g_user_id}
profile = await self.profile_handler.get_profile_from_cache(g_user_id)
entry.update(profile)
entry["is_public"] = bool(is_public)
entry["is_privileged"] = bool(is_privileged)
if not self.is_mine_id(g_user_id):
attestation = await 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
return {"chunk": chunk, "total_user_count_estimate": len(user_results)}
2021-02-17 13:41:47 +00:00
async def get_invited_users_in_group(
self, group_id: str, requester_user_id: str
) -> JsonDict:
"""Get the users that have been invited to a group as seen by requester_user_id.
The ordering is arbitrary at the moment
"""
await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
is_user_in_group = await self.store.is_user_in_group(
requester_user_id, group_id
)
if not is_user_in_group:
raise SynapseError(403, "User not in group")
invited_users = await self.store.get_invited_users_in_group(group_id)
user_profiles = []
for user_id in invited_users:
user_profile = {"user_id": user_id}
try:
profile = await self.profile_handler.get_profile_from_cache(user_id)
user_profile.update(profile)
except Exception as e:
logger.warning("Error getting profile for %s: %s", user_id, e)
user_profiles.append(user_profile)
return {"chunk": user_profiles, "total_user_count_estimate": len(invited_users)}
2021-02-17 13:41:47 +00:00
async def get_rooms_in_group(
self, group_id: str, requester_user_id: str
) -> JsonDict:
"""Get the rooms in group as seen by requester_user_id
This returns rooms in order of decreasing number of joined users
"""
await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
is_user_in_group = await self.store.is_user_in_group(
requester_user_id, group_id
)
# Note! room_results["is_public"] is about whether the room is considered
# public from the group's point of view. (i.e. whether non-group members
# should be able to see the room is in the group).
# This is not the same as whether the room itself is public (in the sense
# of being visible in the room directory).
# As such, room_results["is_public"] itself is not sufficient to determine
# whether any given user is permitted to see the room's metadata.
room_results = await 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"]
joined_users = await self.store.get_users_in_room(room_id)
# check the user is actually allowed to see the room before showing it to them
allow_private = requester_user_id in joined_users
entry = await self.room_list_handler.generate_room_entry(
room_id,
len(joined_users),
with_alias=False,
allow_private=allow_private,
)
if not entry:
continue
entry["is_public"] = bool(room_result["is_public"])
chunk.append(entry)
chunk.sort(key=lambda e: -e["num_joined_members"])
return {"chunk": chunk, "total_room_count_estimate": len(chunk)}
class GroupsServerHandler(GroupsServerWorkerHandler):
2021-02-17 13:41:47 +00:00
def __init__(self, hs: "HomeServer"):
super().__init__(hs)
# Ensure attestations get renewed
hs.get_groups_attestation_renewer()
async def update_group_summary_room(
2021-02-17 13:41:47 +00:00
self,
group_id: str,
requester_user_id: str,
room_id: str,
category_id: str,
content: JsonDict,
) -> JsonDict:
2017-07-12 13:11:59 +00:00
"""Add/update a room to the group summary"""
await self.check_group_is_ours(
2017-10-26 16:20:24 +00:00
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
)
2017-07-10 14:44:40 +00:00
2017-10-11 12:15:44 +00:00
RoomID.from_string(room_id) # Ensure valid room id
2017-07-10 14:44:40 +00:00
order = content.get("order", None)
is_public = _parse_visibility_from_contents(content)
await self.store.add_room_to_summary(
2017-07-10 14:44:40 +00:00
group_id=group_id,
room_id=room_id,
category_id=category_id,
order=order,
is_public=is_public,
)
return {}
2017-07-10 14:44:40 +00:00
async def delete_group_summary_room(
2021-02-17 13:41:47 +00:00
self, group_id: str, requester_user_id: str, room_id: str, category_id: str
) -> JsonDict:
2017-07-12 13:11:59 +00:00
"""Remove a room from the summary"""
await self.check_group_is_ours(
2017-10-26 16:20:24 +00:00
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
)
2017-07-10 14:44:40 +00:00
await self.store.remove_room_from_summary(
2017-07-10 14:44:40 +00:00
group_id=group_id, room_id=room_id, category_id=category_id
)
return {}
2017-07-10 14:44:40 +00:00
2021-02-17 13:41:47 +00:00
async def set_group_join_policy(
self, group_id: str, requester_user_id: str, content: JsonDict
) -> JsonDict:
"""Sets the group join policy.
Currently supported policies are:
- "invite": an invite must be received and accepted in order to join.
- "open": anyone can join.
"""
await self.check_group_is_ours(
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
)
join_policy = _parse_join_policy_from_contents(content)
if join_policy is None:
raise SynapseError(400, "No value specified for 'm.join_policy'")
await self.store.set_group_join_policy(group_id, join_policy=join_policy)
return {}
async def update_group_category(
2021-02-17 13:41:47 +00:00
self, group_id: str, requester_user_id: str, category_id: str, content: JsonDict
) -> JsonDict:
2017-07-12 13:11:59 +00:00
"""Add/Update a group category"""
await self.check_group_is_ours(
2017-10-26 16:20:24 +00:00
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
)
2017-07-10 14:44:40 +00:00
is_public = _parse_visibility_from_contents(content)
profile = content.get("profile")
await self.store.upsert_group_category(
2017-07-10 14:44:40 +00:00
group_id=group_id,
category_id=category_id,
is_public=is_public,
profile=profile,
)
return {}
2017-07-10 14:44:40 +00:00
2021-02-17 13:41:47 +00:00
async def delete_group_category(
self, group_id: str, requester_user_id: str, category_id: str
) -> JsonDict:
2017-07-12 13:11:59 +00:00
"""Delete a group category"""
await self.check_group_is_ours(
2017-10-26 16:20:24 +00:00
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
)
2017-07-10 14:44:40 +00:00
await self.store.remove_group_category(
2017-07-10 14:44:40 +00:00
group_id=group_id, category_id=category_id
)
return {}
2017-07-10 14:44:40 +00:00
2021-02-17 13:41:47 +00:00
async def update_group_role(
self, group_id: str, requester_user_id: str, role_id: str, content: JsonDict
) -> JsonDict:
2017-07-12 13:11:59 +00:00
"""Add/update a role in a group"""
await self.check_group_is_ours(
2017-10-26 16:20:24 +00:00
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
)
2017-07-10 14:44:40 +00:00
is_public = _parse_visibility_from_contents(content)
profile = content.get("profile")
await self.store.upsert_group_role(
2017-07-10 14:44:40 +00:00
group_id=group_id, role_id=role_id, is_public=is_public, profile=profile
)
return {}
2017-07-10 14:44:40 +00:00
2021-02-17 13:41:47 +00:00
async def delete_group_role(
self, group_id: str, requester_user_id: str, role_id: str
) -> JsonDict:
2017-07-12 13:11:59 +00:00
"""Remove role from group"""
await self.check_group_is_ours(
2017-10-26 16:20:24 +00:00
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
)
2017-07-10 14:44:40 +00:00
await self.store.remove_group_role(group_id=group_id, role_id=role_id)
2017-07-10 14:44:40 +00:00
return {}
2017-07-10 14:44:40 +00:00
async def update_group_summary_user(
2021-02-17 13:41:47 +00:00
self,
group_id: str,
requester_user_id: str,
user_id: str,
role_id: str,
content: JsonDict,
) -> JsonDict:
2017-07-12 13:11:59 +00:00
"""Add/update a users entry in the group summary"""
await self.check_group_is_ours(
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
2017-07-18 15:51:25 +00:00
)
2017-07-10 14:44:40 +00:00
order = content.get("order", None)
is_public = _parse_visibility_from_contents(content)
await self.store.add_user_to_summary(
2017-07-10 14:44:40 +00:00
group_id=group_id,
user_id=user_id,
role_id=role_id,
order=order,
is_public=is_public,
)
return {}
2017-07-10 14:44:40 +00:00
async def delete_group_summary_user(
2021-02-17 13:41:47 +00:00
self, group_id: str, requester_user_id: str, user_id: str, role_id: str
) -> JsonDict:
2017-07-12 13:11:59 +00:00
"""Remove a user from the group summary"""
await self.check_group_is_ours(
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
2017-07-18 15:51:25 +00:00
)
2017-07-10 14:44:40 +00:00
await self.store.remove_user_from_summary(
2017-07-10 14:44:40 +00:00
group_id=group_id, user_id=user_id, role_id=role_id
)
return {}
2017-07-10 14:44:40 +00:00
2021-02-17 13:41:47 +00:00
async def update_group_profile(
self, group_id: str, requester_user_id: str, content: JsonDict
) -> None:
2017-07-20 08:46:33 +00:00
"""Update the group profile"""
await self.check_group_is_ours(
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
2017-07-20 08:46:33 +00:00
)
profile = {}
for keyname, max_length in (
("name", MAX_DISPLAYNAME_LEN),
("avatar_url", MAX_AVATAR_URL_LEN),
("short_description", MAX_SHORT_DESC_LEN),
("long_description", MAX_LONG_DESC_LEN),
):
2017-07-20 08:46:33 +00:00
if keyname in content:
2017-07-20 15:24:18 +00:00
value = content[keyname]
if not isinstance(value, str):
raise SynapseError(
400,
"%r value is not a string" % (keyname,),
errcode=Codes.INVALID_PARAM,
)
if len(value) > max_length:
raise SynapseError(
400,
"Invalid %s parameter" % (keyname,),
errcode=Codes.INVALID_PARAM,
)
2017-07-20 15:24:18 +00:00
profile[keyname] = value
2017-07-20 08:46:33 +00:00
await self.store.update_group_profile(group_id, profile)
2017-07-20 08:46:33 +00:00
2021-02-17 13:41:47 +00:00
async def add_room_to_group(
self, group_id: str, requester_user_id: str, room_id: str, content: JsonDict
) -> JsonDict:
"""Add room to group"""
2017-10-11 12:15:44 +00:00
RoomID.from_string(room_id) # Ensure valid room id
await self.check_group_is_ours(
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
2017-07-12 10:43:39 +00:00
)
2017-07-10 14:44:15 +00:00
2017-07-11 10:44:08 +00:00
is_public = _parse_visibility_from_contents(content)
2017-07-10 14:44:15 +00:00
await self.store.add_room_to_group(group_id, room_id, is_public=is_public)
2017-07-10 14:44:15 +00:00
return {}
2017-07-10 14:44:15 +00:00
async def update_room_in_group(
2021-02-17 13:41:47 +00:00
self,
group_id: str,
requester_user_id: str,
room_id: str,
config_key: str,
content: JsonDict,
) -> JsonDict:
"""Update room in group"""
RoomID.from_string(room_id) # Ensure valid room id
await self.check_group_is_ours(
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
)
if config_key == "m.visibility":
is_public = _parse_visibility_dict(content)
await self.store.update_room_in_group_visibility(
group_id, room_id, is_public=is_public
)
else:
2020-10-23 16:38:40 +00:00
raise SynapseError(400, "Unknown config option")
return {}
2021-02-17 13:41:47 +00:00
async def remove_room_from_group(
self, group_id: str, requester_user_id: str, room_id: str
) -> JsonDict:
2017-09-26 14:52:41 +00:00
"""Remove room from group"""
await self.check_group_is_ours(
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
2017-09-26 14:52:41 +00:00
)
await self.store.remove_room_from_group(group_id, room_id)
2017-09-26 14:52:41 +00:00
return {}
2017-09-26 14:52:41 +00:00
2021-02-17 13:41:47 +00:00
async def invite_to_group(
self, group_id: str, user_id: str, requester_user_id: str, content: JsonDict
) -> JsonDict:
2017-07-11 08:58:59 +00:00
"""Invite user to group"""
group = await self.check_group_is_ours(
group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
2017-07-10 14:44:15 +00:00
)
2021-02-17 13:41:47 +00:00
if not group:
raise SynapseError(400, "Group does not exist", errcode=Codes.BAD_STATE)
2017-07-10 14:44:15 +00:00
# TODO: Check if user knocked
invited_users = await self.store.get_invited_users_in_group(group_id)
if user_id in invited_users:
raise SynapseError(
400, "User already invited to group", errcode=Codes.BAD_STATE
)
user_results = await self.store.get_users_in_group(
group_id, include_private=True
)
if user_id in (user_result["user_id"] for user_result in user_results):
raise SynapseError(400, "User already in group")
2017-07-10 14:44:15 +00:00
content = {
"profile": {"name": group["name"], "avatar_url": group["avatar_url"]},
"inviter": requester_user_id,
}
if self.hs.is_mine_id(user_id):
2017-07-10 13:52:27 +00:00
groups_local = self.hs.get_groups_local_handler()
2021-02-17 13:41:47 +00:00
assert isinstance(
groups_local, GroupsLocalHandler
), "Workers cannot invites users to groups."
res = await groups_local.on_invite(group_id, user_id, content)
2017-07-10 13:52:27 +00:00
local_attestation = None
2017-07-10 14:44:15 +00:00
else:
local_attestation = self.attestations.create_attestation(group_id, user_id)
content.update({"attestation": local_attestation})
res = await self.transport_client.invite_to_group_notification(
2017-07-10 14:44:15 +00:00
get_domain_from_id(user_id), group_id, user_id, content
)
2017-08-25 10:21:34 +00:00
user_profile = res.get("user_profile", {})
await self.store.add_remote_profile_cache(
2017-08-25 10:21:34 +00:00
user_id,
displayname=user_profile.get("displayname"),
avatar_url=user_profile.get("avatar_url"),
)
2017-07-10 14:44:15 +00:00
if res["state"] == "join":
if not self.hs.is_mine_id(user_id):
remote_attestation = res["attestation"]
await self.attestations.verify_attestation(
2017-07-10 14:44:15 +00:00
remote_attestation, user_id=user_id, group_id=group_id
)
else:
remote_attestation = None
await self.store.add_user_to_group(
2017-07-10 14:44:15 +00:00
group_id,
user_id,
is_admin=False,
is_public=False, # TODO
local_attestation=local_attestation,
remote_attestation=remote_attestation,
)
2021-02-17 13:41:47 +00:00
return {"state": "join"}
2017-07-10 14:44:15 +00:00
elif res["state"] == "invite":
await self.store.add_group_invite(group_id, user_id)
return {"state": "invite"}
2017-07-10 14:44:15 +00:00
elif res["state"] == "reject":
return {"state": "reject"}
2017-07-10 14:44:15 +00:00
else:
raise SynapseError(502, "Unknown state returned by HS")
2021-02-17 13:41:47 +00:00
async def _add_user(
self, group_id: str, user_id: str, content: JsonDict
) -> Optional[JsonDict]:
"""Add a user to a group based on a content dict.
2017-07-11 08:58:59 +00:00
See accept_invite, join_group.
2017-07-11 08:58:59 +00:00
"""
if not self.hs.is_mine_id(user_id):
local_attestation: Optional[
JsonDict
] = self.attestations.create_attestation(group_id, user_id)
2017-07-10 14:44:15 +00:00
remote_attestation = content["attestation"]
await self.attestations.verify_attestation(
2017-07-10 14:44:15 +00:00
remote_attestation, user_id=user_id, group_id=group_id
)
else:
local_attestation = None
2017-07-10 14:44:15 +00:00
remote_attestation = None
2017-07-11 10:44:08 +00:00
is_public = _parse_visibility_from_contents(content)
2017-07-10 14:44:15 +00:00
await self.store.add_user_to_group(
group_id,
user_id,
2017-07-10 14:44:15 +00:00
is_admin=False,
is_public=is_public,
local_attestation=local_attestation,
remote_attestation=remote_attestation,
)
return local_attestation
2021-02-17 13:41:47 +00:00
async def accept_invite(
self, group_id: str, requester_user_id: str, content: JsonDict
) -> JsonDict:
"""User tries to accept an invite to the group.
This is different from them asking to join, and so should error if no
invite exists (and they're not a member of the group)
"""
await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
is_invited = await self.store.is_user_invited_to_local_group(
group_id, requester_user_id
)
if not is_invited:
raise SynapseError(403, "User not invited to group")
local_attestation = await self._add_user(group_id, requester_user_id, content)
return {"state": "join", "attestation": local_attestation}
2017-07-10 14:44:15 +00:00
2021-02-17 13:41:47 +00:00
async def join_group(
self, group_id: str, requester_user_id: str, content: JsonDict
) -> JsonDict:
2018-03-28 16:18:02 +00:00
"""User tries to join the group.
This will error if the group requires an invite/knock to join
"""
group_info = await self.check_group_is_ours(
2018-04-05 16:32:20 +00:00
group_id, requester_user_id, and_exists=True
)
2021-02-17 13:41:47 +00:00
if not group_info:
raise SynapseError(404, "Group does not exist", errcode=Codes.NOT_FOUND)
2018-04-05 15:31:57 +00:00
if group_info["join_policy"] != "open":
2018-03-28 16:18:02 +00:00
raise SynapseError(403, "Group is not publicly joinable")
local_attestation = await self._add_user(group_id, requester_user_id, content)
2018-03-28 16:18:02 +00:00
return {"state": "join", "attestation": local_attestation}
2018-03-28 16:18:02 +00:00
2020-05-01 14:15:36 +00:00
async def remove_user_from_group(
2021-02-17 13:41:47 +00:00
self, group_id: str, user_id: str, requester_user_id: str, content: JsonDict
) -> JsonDict:
"""Remove a user from the group; either a user is leaving or an admin
kicked them.
2017-07-11 08:58:59 +00:00
"""
2020-05-01 14:15:36 +00:00
await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
2017-07-11 08:58:59 +00:00
2017-07-10 14:44:15 +00:00
is_kick = False
if requester_user_id != user_id:
2020-05-01 14:15:36 +00:00
is_admin = await self.store.is_user_admin_in_group(
2017-07-10 14:44:15 +00:00
group_id, requester_user_id
)
if not is_admin:
raise SynapseError(403, "User is not admin in group")
is_kick = True
2020-05-01 14:15:36 +00:00
await self.store.remove_user_from_group(group_id, user_id)
2017-07-10 14:44:15 +00:00
if is_kick:
if self.hs.is_mine_id(user_id):
2017-07-10 13:52:27 +00:00
groups_local = self.hs.get_groups_local_handler()
2021-02-17 13:41:47 +00:00
assert isinstance(
groups_local, GroupsLocalHandler
), "Workers cannot remove users from groups."
2020-05-01 14:15:36 +00:00
await groups_local.user_removed_from_group(group_id, user_id, {})
2017-07-10 14:44:15 +00:00
else:
2020-05-01 14:15:36 +00:00
await self.transport_client.remove_user_from_group_notification(
2017-07-10 14:44:15 +00:00
get_domain_from_id(user_id), group_id, user_id, {}
)
2017-08-25 10:21:34 +00:00
if not self.hs.is_mine_id(user_id):
2020-05-01 14:15:36 +00:00
await self.store.maybe_delete_remote_profile_cache(user_id)
2017-08-25 10:21:34 +00:00
# Delete group if the last user has left
2020-05-01 14:15:36 +00:00
users = await self.store.get_users_in_group(group_id, include_private=True)
if not users:
2020-05-01 14:15:36 +00:00
await self.store.delete_group(group_id)
return {}
2017-07-10 14:44:15 +00:00
2021-02-17 13:41:47 +00:00
async def create_group(
self, group_id: str, requester_user_id: str, content: JsonDict
) -> JsonDict:
2017-07-10 14:44:15 +00:00
logger.info("Attempting to create group with ID: %r", group_id)
# parsing the id into a GroupID validates it.
group_id_obj = GroupID.from_string(group_id)
2021-02-17 13:41:47 +00:00
group = await self.check_group_is_ours(group_id, requester_user_id)
2017-07-10 14:44:15 +00:00
if group:
raise SynapseError(400, "Group already exists")
2020-05-01 14:15:36 +00:00
is_admin = await self.auth.is_server_admin(
UserID.from_string(requester_user_id)
2019-06-20 09:32:02 +00:00
)
2017-07-11 10:45:32 +00:00
if not is_admin:
if not self.hs.config.groups.enable_group_creation:
2017-10-19 12:36:06 +00:00
raise SynapseError(
2017-10-30 15:17:23 +00:00
403, "Only a server admin can create groups on this server"
2017-10-19 12:36:06 +00:00
)
localpart = group_id_obj.localpart
if not localpart.startswith(self.hs.config.groups.group_creation_prefix):
2017-10-19 11:13:44 +00:00
raise SynapseError(
400,
"Can only create groups with prefix %r on this server"
% (self.hs.config.groups.group_creation_prefix,),
2017-10-19 12:36:06 +00:00
)
2017-07-10 14:44:15 +00:00
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")
2017-08-25 10:21:34 +00:00
user_profile = content.get("user_profile", {})
2017-07-10 14:44:15 +00:00
2020-05-01 14:15:36 +00:00
await self.store.create_group(
2017-07-10 14:44:15 +00:00
group_id,
requester_user_id,
2017-07-10 14:44:15 +00:00
name=name,
avatar_url=avatar_url,
short_description=short_description,
long_description=long_description,
)
if not self.hs.is_mine_id(requester_user_id):
2017-07-10 14:44:15 +00:00
remote_attestation = content["attestation"]
2020-05-01 14:15:36 +00:00
await self.attestations.verify_attestation(
remote_attestation, user_id=requester_user_id, group_id=group_id
2017-07-10 14:44:15 +00:00
)
local_attestation: Optional[
JsonDict
] = self.attestations.create_attestation(group_id, requester_user_id)
2017-07-10 14:44:15 +00:00
else:
local_attestation = None
remote_attestation = None
2020-05-01 14:15:36 +00:00
await self.store.add_user_to_group(
group_id,
requester_user_id,
2017-07-10 14:44:15 +00:00
is_admin=True,
is_public=True, # TODO
local_attestation=local_attestation,
remote_attestation=remote_attestation,
)
if not self.hs.is_mine_id(requester_user_id):
2020-05-01 14:15:36 +00:00
await self.store.add_remote_profile_cache(
requester_user_id,
2017-08-25 10:21:34 +00:00
displayname=user_profile.get("displayname"),
avatar_url=user_profile.get("avatar_url"),
)
return {"group_id": group_id}
2017-07-11 10:44:08 +00:00
2021-02-17 13:41:47 +00:00
async def delete_group(self, group_id: str, requester_user_id: str) -> None:
2019-04-03 15:00:44 +00:00
"""Deletes a group, kicking out all current members.
Only group admins or server admins can call this request
Args:
2021-02-17 13:41:47 +00:00
group_id: The group ID to delete.
requester_user_id: The user requesting to delete the group.
2019-04-03 15:00:44 +00:00
"""
2020-05-01 14:15:36 +00:00
await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
2019-04-03 15:00:44 +00:00
# Only server admins or group admins can delete groups.
2020-05-01 14:15:36 +00:00
is_admin = await self.store.is_user_admin_in_group(group_id, requester_user_id)
2019-04-03 15:00:44 +00:00
if not is_admin:
2020-05-01 14:15:36 +00:00
is_admin = await self.auth.is_server_admin(
2019-04-03 15:00:44 +00:00
UserID.from_string(requester_user_id)
)
if not is_admin:
raise SynapseError(403, "User is not an admin")
# Before deleting the group lets kick everyone out of it
2020-05-01 14:15:36 +00:00
users = await self.store.get_users_in_group(group_id, include_private=True)
2019-04-03 15:00:44 +00:00
2020-05-01 14:15:36 +00:00
async def _kick_user_from_group(user_id):
2019-04-03 15:00:44 +00:00
if self.hs.is_mine_id(user_id):
groups_local = self.hs.get_groups_local_handler()
2021-02-17 13:41:47 +00:00
assert isinstance(
groups_local, GroupsLocalHandler
), "Workers cannot kick users from groups."
2020-05-01 14:15:36 +00:00
await groups_local.user_removed_from_group(group_id, user_id, {})
2019-04-03 15:00:44 +00:00
else:
2020-05-01 14:15:36 +00:00
await self.transport_client.remove_user_from_group_notification(
2019-04-03 15:00:44 +00:00
get_domain_from_id(user_id), group_id, user_id, {}
)
2020-05-01 14:15:36 +00:00
await self.store.maybe_delete_remote_profile_cache(user_id)
2019-04-03 15:00:44 +00:00
# We kick users out in the order of:
# 1. Non-admins
# 2. Other admins
# 3. The requester
#
# This is so that if the deletion fails for some reason other admins or
# the requester still has auth to retry.
non_admins = []
admins = []
for u in users:
if u["user_id"] == requester_user_id:
continue
if u["is_admin"]:
admins.append(u["user_id"])
else:
non_admins.append(u["user_id"])
2020-05-01 14:15:36 +00:00
await concurrently_execute(_kick_user_from_group, non_admins, 10)
await concurrently_execute(_kick_user_from_group, admins, 10)
await _kick_user_from_group(requester_user_id)
2019-04-03 15:00:44 +00:00
2020-05-01 14:15:36 +00:00
await self.store.delete_group(group_id)
2019-04-03 15:00:44 +00:00
2017-07-11 10:44:08 +00:00
2021-02-17 13:41:47 +00:00
def _parse_join_policy_from_contents(content: JsonDict) -> Optional[str]:
"""Given a content for a request, return the specified join policy or None"""
join_policy_dict = content.get("m.join_policy")
if join_policy_dict:
return _parse_join_policy_dict(join_policy_dict)
else:
return None
2021-02-17 13:41:47 +00:00
def _parse_join_policy_dict(join_policy_dict: JsonDict) -> str:
"""Given a dict for the "m.join_policy" config return the join policy specified"""
join_policy_type = join_policy_dict.get("type")
if not join_policy_type:
2018-04-06 10:44:18 +00:00
return "invite"
if join_policy_type not in ("invite", "open"):
raise SynapseError(400, "Synapse only supports 'invite'/'open' join rule")
return join_policy_type
2021-02-17 13:41:47 +00:00
def _parse_visibility_from_contents(content: JsonDict) -> bool:
2017-07-11 10:44:08 +00:00
"""Given a content for a request parse out whether the entity should be
public or not
"""
visibility = content.get("m.visibility")
2017-07-11 10:44:08 +00:00
if visibility:
return _parse_visibility_dict(visibility)
2017-07-11 10:44:08 +00:00
else:
is_public = True
return is_public
2021-02-17 13:41:47 +00:00
def _parse_visibility_dict(visibility: JsonDict) -> bool:
"""Given a dict for the "m.visibility" config return if the entity should
be public or not
"""
vis_type = visibility.get("type")
if not vis_type:
return True
if vis_type not in ("public", "private"):
raise SynapseError(400, "Synapse only supports 'public'/'private' visibility")
return vis_type == "public"