12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019 |
- # Copyright 2017 Vector Creations Ltd
- # Copyright 2018 New Vector Ltd
- # Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
- #
- # 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
- from typing import TYPE_CHECKING, Optional
- from synapse.api.errors import Codes, SynapseError
- from synapse.handlers.groups_local import GroupsLocalHandler
- from synapse.handlers.profile import MAX_AVATAR_URL_LEN, MAX_DISPLAYNAME_LEN
- from synapse.types import GroupID, JsonDict, RoomID, UserID, get_domain_from_id
- from synapse.util.async_helpers import concurrently_execute
- if TYPE_CHECKING:
- from synapse.server import HomeServer
- logger = logging.getLogger(__name__)
- # TODO: Allow users to "knock" or simply join depending on rules
- # TODO: Federation admin APIs
- # TODO: is_privileged 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
- # Note that the maximum lengths are somewhat arbitrary.
- MAX_SHORT_DESC_LEN = 1000
- MAX_LONG_DESC_LEN = 10000
- class GroupsServerWorkerHandler:
- def __init__(self, hs: "HomeServer"):
- self.hs = hs
- self.store = hs.get_datastores().main
- 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
- 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()
- async def check_group_is_ours(
- self,
- group_id: str,
- requester_user_id: str,
- and_exists: bool = False,
- and_is_admin: Optional[str] = None,
- ) -> Optional[dict]:
- """Check that the group is ours, and optionally if it exists.
- If group does exist then return group.
- Args:
- 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
- that is an admin
- """
- if not self.is_mine_id(group_id):
- raise SynapseError(400, "Group not on this server")
- group = await self.store.get_group(group_id)
- 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
- )
- if group and not is_user_in_group and not group["is_public"]:
- raise SynapseError(404, "Unknown group")
- if and_is_admin:
- is_admin = await self.store.is_user_admin_in_group(group_id, and_is_admin)
- if not is_admin:
- raise SynapseError(403, "User is not admin in group")
- return group
- async def get_group_summary(
- self, group_id: str, requester_user_id: str
- ) -> JsonDict:
- """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)
- is_user_in_group = await self.store.is_user_in_group(
- requester_user_id, group_id
- )
- profile = await self.get_group_profile(group_id, requester_user_id)
- users, roles = await self.store.get_users_for_summary_by_role(
- group_id, include_private=is_user_in_group
- )
- # TODO: Add profiles to users
- rooms, categories = await self.store.get_rooms_for_summary_by_category(
- 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
- )
- if entry is None:
- continue
- entry = dict(entry) # so we don't change what's cached
- entry.pop("room_id", None)
- room_entry["profile"] = entry
- rooms.sort(key=lambda e: e.get("order", 0))
- for user in users:
- user_id = user["user_id"]
- if not self.is_mine_id(requester_user_id):
- attestation = await self.store.get_remote_attestation(group_id, user_id)
- if not attestation:
- continue
- user["attestation"] = attestation
- else:
- user["attestation"] = self.attestations.create_attestation(
- group_id, user_id
- )
- user_profile = await self.profile_handler.get_profile_from_cache(user_id)
- user.update(user_profile)
- 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,
- }
- 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}
- 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)
- return await self.store.get_group_category(
- group_id=group_id, category_id=category_id
- )
- 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}
- 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)
- return await self.store.get_group_role(group_id=group_id, role_id=role_id)
- 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")
- 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)}
- 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)}
- 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):
- def __init__(self, hs: "HomeServer"):
- super().__init__(hs)
- # Ensure attestations get renewed
- hs.get_groups_attestation_renewer()
- async def update_group_summary_room(
- self,
- group_id: str,
- requester_user_id: str,
- room_id: str,
- category_id: str,
- content: JsonDict,
- ) -> JsonDict:
- """Add/update a room to the group summary"""
- await self.check_group_is_ours(
- group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
- )
- RoomID.from_string(room_id) # Ensure valid room id
- order = content.get("order", None)
- is_public = _parse_visibility_from_contents(content)
- await self.store.add_room_to_summary(
- group_id=group_id,
- room_id=room_id,
- category_id=category_id,
- order=order,
- is_public=is_public,
- )
- return {}
- async def delete_group_summary_room(
- self, group_id: str, requester_user_id: str, room_id: str, category_id: str
- ) -> JsonDict:
- """Remove a room from the summary"""
- await self.check_group_is_ours(
- group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
- )
- await self.store.remove_room_from_summary(
- group_id=group_id, room_id=room_id, category_id=category_id
- )
- return {}
- 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(
- self, group_id: str, requester_user_id: str, category_id: str, content: JsonDict
- ) -> JsonDict:
- """Add/Update a group category"""
- await self.check_group_is_ours(
- group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
- )
- is_public = _parse_visibility_from_contents(content)
- profile = content.get("profile")
- await self.store.upsert_group_category(
- group_id=group_id,
- category_id=category_id,
- is_public=is_public,
- profile=profile,
- )
- return {}
- async def delete_group_category(
- self, group_id: str, requester_user_id: str, category_id: str
- ) -> JsonDict:
- """Delete a group category"""
- await self.check_group_is_ours(
- group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
- )
- await self.store.remove_group_category(
- group_id=group_id, category_id=category_id
- )
- return {}
- async def update_group_role(
- self, group_id: str, requester_user_id: str, role_id: str, content: JsonDict
- ) -> JsonDict:
- """Add/update a role in a group"""
- await self.check_group_is_ours(
- group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
- )
- is_public = _parse_visibility_from_contents(content)
- profile = content.get("profile")
- await self.store.upsert_group_role(
- group_id=group_id, role_id=role_id, is_public=is_public, profile=profile
- )
- return {}
- async def delete_group_role(
- self, group_id: str, requester_user_id: str, role_id: str
- ) -> JsonDict:
- """Remove role from group"""
- await self.check_group_is_ours(
- group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
- )
- await self.store.remove_group_role(group_id=group_id, role_id=role_id)
- return {}
- async def update_group_summary_user(
- self,
- group_id: str,
- requester_user_id: str,
- user_id: str,
- role_id: str,
- content: JsonDict,
- ) -> JsonDict:
- """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
- )
- order = content.get("order", None)
- is_public = _parse_visibility_from_contents(content)
- await self.store.add_user_to_summary(
- group_id=group_id,
- user_id=user_id,
- role_id=role_id,
- order=order,
- is_public=is_public,
- )
- return {}
- async def delete_group_summary_user(
- self, group_id: str, requester_user_id: str, user_id: str, role_id: str
- ) -> JsonDict:
- """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
- )
- await self.store.remove_user_from_summary(
- group_id=group_id, user_id=user_id, role_id=role_id
- )
- return {}
- async def update_group_profile(
- self, group_id: str, requester_user_id: str, content: JsonDict
- ) -> None:
- """Update the group profile"""
- await self.check_group_is_ours(
- group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
- )
- 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),
- ):
- if keyname in content:
- 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,
- )
- profile[keyname] = value
- await self.store.update_group_profile(group_id, profile)
- async def add_room_to_group(
- self, group_id: str, requester_user_id: str, room_id: str, content: JsonDict
- ) -> JsonDict:
- """Add room to 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
- )
- is_public = _parse_visibility_from_contents(content)
- await self.store.add_room_to_group(group_id, room_id, is_public=is_public)
- return {}
- async def update_room_in_group(
- 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:
- raise SynapseError(400, "Unknown config option")
- return {}
- async def remove_room_from_group(
- self, group_id: str, requester_user_id: str, room_id: str
- ) -> JsonDict:
- """Remove room from group"""
- await self.check_group_is_ours(
- group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
- )
- await self.store.remove_room_from_group(group_id, room_id)
- return {}
- async def invite_to_group(
- self, group_id: str, user_id: str, requester_user_id: str, content: JsonDict
- ) -> JsonDict:
- """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
- )
- if not group:
- raise SynapseError(400, "Group does not exist", errcode=Codes.BAD_STATE)
- # 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")
- content = {
- "profile": {"name": group["name"], "avatar_url": group["avatar_url"]},
- "inviter": requester_user_id,
- }
- if self.hs.is_mine_id(user_id):
- groups_local = self.hs.get_groups_local_handler()
- assert isinstance(
- groups_local, GroupsLocalHandler
- ), "Workers cannot invites users to groups."
- res = await groups_local.on_invite(group_id, user_id, content)
- local_attestation = None
- 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(
- get_domain_from_id(user_id), group_id, user_id, content
- )
- user_profile = res.get("user_profile", {})
- await self.store.add_remote_profile_cache(
- user_id,
- displayname=user_profile.get("displayname"),
- avatar_url=user_profile.get("avatar_url"),
- )
- if res["state"] == "join":
- if not self.hs.is_mine_id(user_id):
- remote_attestation = res["attestation"]
- await self.attestations.verify_attestation(
- remote_attestation, user_id=user_id, group_id=group_id
- )
- else:
- remote_attestation = None
- await 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,
- )
- return {"state": "join"}
- elif res["state"] == "invite":
- await self.store.add_group_invite(group_id, user_id)
- return {"state": "invite"}
- elif res["state"] == "reject":
- return {"state": "reject"}
- else:
- raise SynapseError(502, "Unknown state returned by HS")
- 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.
- See accept_invite, join_group.
- """
- if not self.hs.is_mine_id(user_id):
- local_attestation: Optional[
- JsonDict
- ] = self.attestations.create_attestation(group_id, user_id)
- remote_attestation = content["attestation"]
- await self.attestations.verify_attestation(
- remote_attestation, user_id=user_id, group_id=group_id
- )
- else:
- local_attestation = None
- remote_attestation = None
- is_public = _parse_visibility_from_contents(content)
- await 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,
- )
- return local_attestation
- 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}
- async def join_group(
- self, group_id: str, requester_user_id: str, content: JsonDict
- ) -> JsonDict:
- """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(
- group_id, requester_user_id, and_exists=True
- )
- if not group_info:
- raise SynapseError(404, "Group does not exist", errcode=Codes.NOT_FOUND)
- if group_info["join_policy"] != "open":
- raise SynapseError(403, "Group is not publicly joinable")
- local_attestation = await self._add_user(group_id, requester_user_id, content)
- return {"state": "join", "attestation": local_attestation}
- async def remove_user_from_group(
- 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.
- """
- await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
- is_kick = False
- if requester_user_id != user_id:
- is_admin = await 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
- await self.store.remove_user_from_group(group_id, user_id)
- if is_kick:
- if self.hs.is_mine_id(user_id):
- groups_local = self.hs.get_groups_local_handler()
- assert isinstance(
- groups_local, GroupsLocalHandler
- ), "Workers cannot remove users from groups."
- await groups_local.user_removed_from_group(group_id, user_id, {})
- else:
- await self.transport_client.remove_user_from_group_notification(
- get_domain_from_id(user_id), group_id, user_id, {}
- )
- if not self.hs.is_mine_id(user_id):
- await self.store.maybe_delete_remote_profile_cache(user_id)
- # Delete group if the last user has left
- users = await self.store.get_users_in_group(group_id, include_private=True)
- if not users:
- await self.store.delete_group(group_id)
- return {}
- async def create_group(
- self, group_id: str, requester_user_id: str, content: JsonDict
- ) -> JsonDict:
- 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)
- group = await self.check_group_is_ours(group_id, requester_user_id)
- if group:
- raise SynapseError(400, "Group already exists")
- is_admin = await self.auth.is_server_admin(
- UserID.from_string(requester_user_id)
- )
- if not is_admin:
- if not self.hs.config.groups.enable_group_creation:
- raise SynapseError(
- 403, "Only a server admin can create groups on this server"
- )
- localpart = group_id_obj.localpart
- if not localpart.startswith(self.hs.config.groups.group_creation_prefix):
- raise SynapseError(
- 400,
- "Can only create groups with prefix %r on this server"
- % (self.hs.config.groups.group_creation_prefix,),
- )
- 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")
- user_profile = content.get("user_profile", {})
- await self.store.create_group(
- group_id,
- requester_user_id,
- name=name,
- avatar_url=avatar_url,
- short_description=short_description,
- long_description=long_description,
- )
- if not self.hs.is_mine_id(requester_user_id):
- remote_attestation = content["attestation"]
- await self.attestations.verify_attestation(
- remote_attestation, user_id=requester_user_id, group_id=group_id
- )
- local_attestation: Optional[
- JsonDict
- ] = self.attestations.create_attestation(group_id, requester_user_id)
- else:
- local_attestation = None
- remote_attestation = None
- await self.store.add_user_to_group(
- group_id,
- requester_user_id,
- 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):
- await self.store.add_remote_profile_cache(
- requester_user_id,
- displayname=user_profile.get("displayname"),
- avatar_url=user_profile.get("avatar_url"),
- )
- return {"group_id": group_id}
- async def delete_group(self, group_id: str, requester_user_id: str) -> None:
- """Deletes a group, kicking out all current members.
- Only group admins or server admins can call this request
- Args:
- group_id: The group ID to delete.
- requester_user_id: The user requesting to delete the group.
- """
- await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
- # Only server admins or group admins can delete groups.
- is_admin = await self.store.is_user_admin_in_group(group_id, requester_user_id)
- if not is_admin:
- is_admin = await self.auth.is_server_admin(
- 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
- users = await self.store.get_users_in_group(group_id, include_private=True)
- async def _kick_user_from_group(user_id):
- if self.hs.is_mine_id(user_id):
- groups_local = self.hs.get_groups_local_handler()
- assert isinstance(
- groups_local, GroupsLocalHandler
- ), "Workers cannot kick users from groups."
- await groups_local.user_removed_from_group(group_id, user_id, {})
- else:
- await self.transport_client.remove_user_from_group_notification(
- get_domain_from_id(user_id), group_id, user_id, {}
- )
- await self.store.maybe_delete_remote_profile_cache(user_id)
- # 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"])
- 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)
- await self.store.delete_group(group_id)
- 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
- 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:
- return "invite"
- if join_policy_type not in ("invite", "open"):
- raise SynapseError(400, "Synapse only supports 'invite'/'open' join rule")
- return join_policy_type
- def _parse_visibility_from_contents(content: JsonDict) -> bool:
- """Given a content for a request parse out whether the entity should be
- public or not
- """
- visibility = content.get("m.visibility")
- if visibility:
- return _parse_visibility_dict(visibility)
- else:
- is_public = True
- return is_public
- 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"
|