groups_server.py 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019
  1. # Copyright 2017 Vector Creations Ltd
  2. # Copyright 2018 New Vector Ltd
  3. # Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import logging
  17. from typing import TYPE_CHECKING, Optional
  18. from synapse.api.errors import Codes, SynapseError
  19. from synapse.handlers.groups_local import GroupsLocalHandler
  20. from synapse.handlers.profile import MAX_AVATAR_URL_LEN, MAX_DISPLAYNAME_LEN
  21. from synapse.types import GroupID, JsonDict, RoomID, UserID, get_domain_from_id
  22. from synapse.util.async_helpers import concurrently_execute
  23. if TYPE_CHECKING:
  24. from synapse.server import HomeServer
  25. logger = logging.getLogger(__name__)
  26. # TODO: Allow users to "knock" or simply join depending on rules
  27. # TODO: Federation admin APIs
  28. # TODO: is_privileged flag to users and is_public to users and rooms
  29. # TODO: Audit log for admins (profile updates, membership changes, users who tried
  30. # to join but were rejected, etc)
  31. # TODO: Flairs
  32. # Note that the maximum lengths are somewhat arbitrary.
  33. MAX_SHORT_DESC_LEN = 1000
  34. MAX_LONG_DESC_LEN = 10000
  35. class GroupsServerWorkerHandler:
  36. def __init__(self, hs: "HomeServer"):
  37. self.hs = hs
  38. self.store = hs.get_datastores().main
  39. self.room_list_handler = hs.get_room_list_handler()
  40. self.auth = hs.get_auth()
  41. self.clock = hs.get_clock()
  42. self.keyring = hs.get_keyring()
  43. self.is_mine_id = hs.is_mine_id
  44. self.signing_key = hs.signing_key
  45. self.server_name = hs.hostname
  46. self.attestations = hs.get_groups_attestation_signing()
  47. self.transport_client = hs.get_federation_transport_client()
  48. self.profile_handler = hs.get_profile_handler()
  49. async def check_group_is_ours(
  50. self,
  51. group_id: str,
  52. requester_user_id: str,
  53. and_exists: bool = False,
  54. and_is_admin: Optional[str] = None,
  55. ) -> Optional[dict]:
  56. """Check that the group is ours, and optionally if it exists.
  57. If group does exist then return group.
  58. Args:
  59. group_id: The group ID to check.
  60. requester_user_id: The user ID of the requester.
  61. and_exists: whether to also check if group exists
  62. and_is_admin: whether to also check if given str is a user_id
  63. that is an admin
  64. """
  65. if not self.is_mine_id(group_id):
  66. raise SynapseError(400, "Group not on this server")
  67. group = await self.store.get_group(group_id)
  68. if and_exists and not group:
  69. raise SynapseError(404, "Unknown group")
  70. is_user_in_group = await self.store.is_user_in_group(
  71. requester_user_id, group_id
  72. )
  73. if group and not is_user_in_group and not group["is_public"]:
  74. raise SynapseError(404, "Unknown group")
  75. if and_is_admin:
  76. is_admin = await self.store.is_user_admin_in_group(group_id, and_is_admin)
  77. if not is_admin:
  78. raise SynapseError(403, "User is not admin in group")
  79. return group
  80. async def get_group_summary(
  81. self, group_id: str, requester_user_id: str
  82. ) -> JsonDict:
  83. """Get the summary for a group as seen by requester_user_id.
  84. The group summary consists of the profile of the room, and a curated
  85. list of users and rooms. These list *may* be organised by role/category.
  86. The roles/categories are ordered, and so are the users/rooms within them.
  87. A user/room may appear in multiple roles/categories.
  88. """
  89. await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
  90. is_user_in_group = await self.store.is_user_in_group(
  91. requester_user_id, group_id
  92. )
  93. profile = await self.get_group_profile(group_id, requester_user_id)
  94. users, roles = await self.store.get_users_for_summary_by_role(
  95. group_id, include_private=is_user_in_group
  96. )
  97. # TODO: Add profiles to users
  98. rooms, categories = await self.store.get_rooms_for_summary_by_category(
  99. group_id, include_private=is_user_in_group
  100. )
  101. for room_entry in rooms:
  102. room_id = room_entry["room_id"]
  103. joined_users = await self.store.get_users_in_room(room_id)
  104. entry = await self.room_list_handler.generate_room_entry(
  105. room_id, len(joined_users), with_alias=False, allow_private=True
  106. )
  107. if entry is None:
  108. continue
  109. entry = dict(entry) # so we don't change what's cached
  110. entry.pop("room_id", None)
  111. room_entry["profile"] = entry
  112. rooms.sort(key=lambda e: e.get("order", 0))
  113. for user in users:
  114. user_id = user["user_id"]
  115. if not self.is_mine_id(requester_user_id):
  116. attestation = await self.store.get_remote_attestation(group_id, user_id)
  117. if not attestation:
  118. continue
  119. user["attestation"] = attestation
  120. else:
  121. user["attestation"] = self.attestations.create_attestation(
  122. group_id, user_id
  123. )
  124. user_profile = await self.profile_handler.get_profile_from_cache(user_id)
  125. user.update(user_profile)
  126. users.sort(key=lambda e: e.get("order", 0))
  127. membership_info = await self.store.get_users_membership_info_in_group(
  128. group_id, requester_user_id
  129. )
  130. return {
  131. "profile": profile,
  132. "users_section": {
  133. "users": users,
  134. "roles": roles,
  135. "total_user_count_estimate": 0, # TODO
  136. },
  137. "rooms_section": {
  138. "rooms": rooms,
  139. "categories": categories,
  140. "total_room_count_estimate": 0, # TODO
  141. },
  142. "user": membership_info,
  143. }
  144. async def get_group_categories(
  145. self, group_id: str, requester_user_id: str
  146. ) -> JsonDict:
  147. """Get all categories in a group (as seen by user)"""
  148. await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
  149. categories = await self.store.get_group_categories(group_id=group_id)
  150. return {"categories": categories}
  151. async def get_group_category(
  152. self, group_id: str, requester_user_id: str, category_id: str
  153. ) -> JsonDict:
  154. """Get a specific category in a group (as seen by user)"""
  155. await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
  156. return await self.store.get_group_category(
  157. group_id=group_id, category_id=category_id
  158. )
  159. async def get_group_roles(self, group_id: str, requester_user_id: str) -> JsonDict:
  160. """Get all roles in a group (as seen by user)"""
  161. await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
  162. roles = await self.store.get_group_roles(group_id=group_id)
  163. return {"roles": roles}
  164. async def get_group_role(
  165. self, group_id: str, requester_user_id: str, role_id: str
  166. ) -> JsonDict:
  167. """Get a specific role in a group (as seen by user)"""
  168. await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
  169. return await self.store.get_group_role(group_id=group_id, role_id=role_id)
  170. async def get_group_profile(
  171. self, group_id: str, requester_user_id: str
  172. ) -> JsonDict:
  173. """Get the group profile as seen by requester_user_id"""
  174. await self.check_group_is_ours(group_id, requester_user_id)
  175. group = await self.store.get_group(group_id)
  176. if group:
  177. cols = [
  178. "name",
  179. "short_description",
  180. "long_description",
  181. "avatar_url",
  182. "is_public",
  183. ]
  184. group_description = {key: group[key] for key in cols}
  185. group_description["is_openly_joinable"] = group["join_policy"] == "open"
  186. return group_description
  187. else:
  188. raise SynapseError(404, "Unknown group")
  189. async def get_users_in_group(
  190. self, group_id: str, requester_user_id: str
  191. ) -> JsonDict:
  192. """Get the users in group as seen by requester_user_id.
  193. The ordering is arbitrary at the moment
  194. """
  195. await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
  196. is_user_in_group = await self.store.is_user_in_group(
  197. requester_user_id, group_id
  198. )
  199. user_results = await self.store.get_users_in_group(
  200. group_id, include_private=is_user_in_group
  201. )
  202. chunk = []
  203. for user_result in user_results:
  204. g_user_id = user_result["user_id"]
  205. is_public = user_result["is_public"]
  206. is_privileged = user_result["is_admin"]
  207. entry = {"user_id": g_user_id}
  208. profile = await self.profile_handler.get_profile_from_cache(g_user_id)
  209. entry.update(profile)
  210. entry["is_public"] = bool(is_public)
  211. entry["is_privileged"] = bool(is_privileged)
  212. if not self.is_mine_id(g_user_id):
  213. attestation = await self.store.get_remote_attestation(
  214. group_id, g_user_id
  215. )
  216. if not attestation:
  217. continue
  218. entry["attestation"] = attestation
  219. else:
  220. entry["attestation"] = self.attestations.create_attestation(
  221. group_id, g_user_id
  222. )
  223. chunk.append(entry)
  224. # TODO: If admin add lists of users whose attestations have timed out
  225. return {"chunk": chunk, "total_user_count_estimate": len(user_results)}
  226. async def get_invited_users_in_group(
  227. self, group_id: str, requester_user_id: str
  228. ) -> JsonDict:
  229. """Get the users that have been invited to a group as seen by requester_user_id.
  230. The ordering is arbitrary at the moment
  231. """
  232. await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
  233. is_user_in_group = await self.store.is_user_in_group(
  234. requester_user_id, group_id
  235. )
  236. if not is_user_in_group:
  237. raise SynapseError(403, "User not in group")
  238. invited_users = await self.store.get_invited_users_in_group(group_id)
  239. user_profiles = []
  240. for user_id in invited_users:
  241. user_profile = {"user_id": user_id}
  242. try:
  243. profile = await self.profile_handler.get_profile_from_cache(user_id)
  244. user_profile.update(profile)
  245. except Exception as e:
  246. logger.warning("Error getting profile for %s: %s", user_id, e)
  247. user_profiles.append(user_profile)
  248. return {"chunk": user_profiles, "total_user_count_estimate": len(invited_users)}
  249. async def get_rooms_in_group(
  250. self, group_id: str, requester_user_id: str
  251. ) -> JsonDict:
  252. """Get the rooms in group as seen by requester_user_id
  253. This returns rooms in order of decreasing number of joined users
  254. """
  255. await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
  256. is_user_in_group = await self.store.is_user_in_group(
  257. requester_user_id, group_id
  258. )
  259. # Note! room_results["is_public"] is about whether the room is considered
  260. # public from the group's point of view. (i.e. whether non-group members
  261. # should be able to see the room is in the group).
  262. # This is not the same as whether the room itself is public (in the sense
  263. # of being visible in the room directory).
  264. # As such, room_results["is_public"] itself is not sufficient to determine
  265. # whether any given user is permitted to see the room's metadata.
  266. room_results = await self.store.get_rooms_in_group(
  267. group_id, include_private=is_user_in_group
  268. )
  269. chunk = []
  270. for room_result in room_results:
  271. room_id = room_result["room_id"]
  272. joined_users = await self.store.get_users_in_room(room_id)
  273. # check the user is actually allowed to see the room before showing it to them
  274. allow_private = requester_user_id in joined_users
  275. entry = await self.room_list_handler.generate_room_entry(
  276. room_id,
  277. len(joined_users),
  278. with_alias=False,
  279. allow_private=allow_private,
  280. )
  281. if not entry:
  282. continue
  283. entry["is_public"] = bool(room_result["is_public"])
  284. chunk.append(entry)
  285. chunk.sort(key=lambda e: -e["num_joined_members"])
  286. return {"chunk": chunk, "total_room_count_estimate": len(chunk)}
  287. class GroupsServerHandler(GroupsServerWorkerHandler):
  288. def __init__(self, hs: "HomeServer"):
  289. super().__init__(hs)
  290. # Ensure attestations get renewed
  291. hs.get_groups_attestation_renewer()
  292. async def update_group_summary_room(
  293. self,
  294. group_id: str,
  295. requester_user_id: str,
  296. room_id: str,
  297. category_id: str,
  298. content: JsonDict,
  299. ) -> JsonDict:
  300. """Add/update a room to the group summary"""
  301. await self.check_group_is_ours(
  302. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  303. )
  304. RoomID.from_string(room_id) # Ensure valid room id
  305. order = content.get("order", None)
  306. is_public = _parse_visibility_from_contents(content)
  307. await self.store.add_room_to_summary(
  308. group_id=group_id,
  309. room_id=room_id,
  310. category_id=category_id,
  311. order=order,
  312. is_public=is_public,
  313. )
  314. return {}
  315. async def delete_group_summary_room(
  316. self, group_id: str, requester_user_id: str, room_id: str, category_id: str
  317. ) -> JsonDict:
  318. """Remove a room from the summary"""
  319. await self.check_group_is_ours(
  320. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  321. )
  322. await self.store.remove_room_from_summary(
  323. group_id=group_id, room_id=room_id, category_id=category_id
  324. )
  325. return {}
  326. async def set_group_join_policy(
  327. self, group_id: str, requester_user_id: str, content: JsonDict
  328. ) -> JsonDict:
  329. """Sets the group join policy.
  330. Currently supported policies are:
  331. - "invite": an invite must be received and accepted in order to join.
  332. - "open": anyone can join.
  333. """
  334. await self.check_group_is_ours(
  335. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  336. )
  337. join_policy = _parse_join_policy_from_contents(content)
  338. if join_policy is None:
  339. raise SynapseError(400, "No value specified for 'm.join_policy'")
  340. await self.store.set_group_join_policy(group_id, join_policy=join_policy)
  341. return {}
  342. async def update_group_category(
  343. self, group_id: str, requester_user_id: str, category_id: str, content: JsonDict
  344. ) -> JsonDict:
  345. """Add/Update a group category"""
  346. await self.check_group_is_ours(
  347. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  348. )
  349. is_public = _parse_visibility_from_contents(content)
  350. profile = content.get("profile")
  351. await self.store.upsert_group_category(
  352. group_id=group_id,
  353. category_id=category_id,
  354. is_public=is_public,
  355. profile=profile,
  356. )
  357. return {}
  358. async def delete_group_category(
  359. self, group_id: str, requester_user_id: str, category_id: str
  360. ) -> JsonDict:
  361. """Delete a group category"""
  362. await self.check_group_is_ours(
  363. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  364. )
  365. await self.store.remove_group_category(
  366. group_id=group_id, category_id=category_id
  367. )
  368. return {}
  369. async def update_group_role(
  370. self, group_id: str, requester_user_id: str, role_id: str, content: JsonDict
  371. ) -> JsonDict:
  372. """Add/update a role in a group"""
  373. await self.check_group_is_ours(
  374. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  375. )
  376. is_public = _parse_visibility_from_contents(content)
  377. profile = content.get("profile")
  378. await self.store.upsert_group_role(
  379. group_id=group_id, role_id=role_id, is_public=is_public, profile=profile
  380. )
  381. return {}
  382. async def delete_group_role(
  383. self, group_id: str, requester_user_id: str, role_id: str
  384. ) -> JsonDict:
  385. """Remove role from group"""
  386. await self.check_group_is_ours(
  387. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  388. )
  389. await self.store.remove_group_role(group_id=group_id, role_id=role_id)
  390. return {}
  391. async def update_group_summary_user(
  392. self,
  393. group_id: str,
  394. requester_user_id: str,
  395. user_id: str,
  396. role_id: str,
  397. content: JsonDict,
  398. ) -> JsonDict:
  399. """Add/update a users entry in the group summary"""
  400. await self.check_group_is_ours(
  401. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  402. )
  403. order = content.get("order", None)
  404. is_public = _parse_visibility_from_contents(content)
  405. await self.store.add_user_to_summary(
  406. group_id=group_id,
  407. user_id=user_id,
  408. role_id=role_id,
  409. order=order,
  410. is_public=is_public,
  411. )
  412. return {}
  413. async def delete_group_summary_user(
  414. self, group_id: str, requester_user_id: str, user_id: str, role_id: str
  415. ) -> JsonDict:
  416. """Remove a user from the group summary"""
  417. await self.check_group_is_ours(
  418. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  419. )
  420. await self.store.remove_user_from_summary(
  421. group_id=group_id, user_id=user_id, role_id=role_id
  422. )
  423. return {}
  424. async def update_group_profile(
  425. self, group_id: str, requester_user_id: str, content: JsonDict
  426. ) -> None:
  427. """Update the group profile"""
  428. await self.check_group_is_ours(
  429. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  430. )
  431. profile = {}
  432. for keyname, max_length in (
  433. ("name", MAX_DISPLAYNAME_LEN),
  434. ("avatar_url", MAX_AVATAR_URL_LEN),
  435. ("short_description", MAX_SHORT_DESC_LEN),
  436. ("long_description", MAX_LONG_DESC_LEN),
  437. ):
  438. if keyname in content:
  439. value = content[keyname]
  440. if not isinstance(value, str):
  441. raise SynapseError(
  442. 400,
  443. "%r value is not a string" % (keyname,),
  444. errcode=Codes.INVALID_PARAM,
  445. )
  446. if len(value) > max_length:
  447. raise SynapseError(
  448. 400,
  449. "Invalid %s parameter" % (keyname,),
  450. errcode=Codes.INVALID_PARAM,
  451. )
  452. profile[keyname] = value
  453. await self.store.update_group_profile(group_id, profile)
  454. async def add_room_to_group(
  455. self, group_id: str, requester_user_id: str, room_id: str, content: JsonDict
  456. ) -> JsonDict:
  457. """Add room to group"""
  458. RoomID.from_string(room_id) # Ensure valid room id
  459. await self.check_group_is_ours(
  460. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  461. )
  462. is_public = _parse_visibility_from_contents(content)
  463. await self.store.add_room_to_group(group_id, room_id, is_public=is_public)
  464. return {}
  465. async def update_room_in_group(
  466. self,
  467. group_id: str,
  468. requester_user_id: str,
  469. room_id: str,
  470. config_key: str,
  471. content: JsonDict,
  472. ) -> JsonDict:
  473. """Update room in group"""
  474. RoomID.from_string(room_id) # Ensure valid room id
  475. await self.check_group_is_ours(
  476. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  477. )
  478. if config_key == "m.visibility":
  479. is_public = _parse_visibility_dict(content)
  480. await self.store.update_room_in_group_visibility(
  481. group_id, room_id, is_public=is_public
  482. )
  483. else:
  484. raise SynapseError(400, "Unknown config option")
  485. return {}
  486. async def remove_room_from_group(
  487. self, group_id: str, requester_user_id: str, room_id: str
  488. ) -> JsonDict:
  489. """Remove room from group"""
  490. await self.check_group_is_ours(
  491. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  492. )
  493. await self.store.remove_room_from_group(group_id, room_id)
  494. return {}
  495. async def invite_to_group(
  496. self, group_id: str, user_id: str, requester_user_id: str, content: JsonDict
  497. ) -> JsonDict:
  498. """Invite user to group"""
  499. group = await self.check_group_is_ours(
  500. group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
  501. )
  502. if not group:
  503. raise SynapseError(400, "Group does not exist", errcode=Codes.BAD_STATE)
  504. # TODO: Check if user knocked
  505. invited_users = await self.store.get_invited_users_in_group(group_id)
  506. if user_id in invited_users:
  507. raise SynapseError(
  508. 400, "User already invited to group", errcode=Codes.BAD_STATE
  509. )
  510. user_results = await self.store.get_users_in_group(
  511. group_id, include_private=True
  512. )
  513. if user_id in (user_result["user_id"] for user_result in user_results):
  514. raise SynapseError(400, "User already in group")
  515. content = {
  516. "profile": {"name": group["name"], "avatar_url": group["avatar_url"]},
  517. "inviter": requester_user_id,
  518. }
  519. if self.hs.is_mine_id(user_id):
  520. groups_local = self.hs.get_groups_local_handler()
  521. assert isinstance(
  522. groups_local, GroupsLocalHandler
  523. ), "Workers cannot invites users to groups."
  524. res = await groups_local.on_invite(group_id, user_id, content)
  525. local_attestation = None
  526. else:
  527. local_attestation = self.attestations.create_attestation(group_id, user_id)
  528. content.update({"attestation": local_attestation})
  529. res = await self.transport_client.invite_to_group_notification(
  530. get_domain_from_id(user_id), group_id, user_id, content
  531. )
  532. user_profile = res.get("user_profile", {})
  533. await self.store.add_remote_profile_cache(
  534. user_id,
  535. displayname=user_profile.get("displayname"),
  536. avatar_url=user_profile.get("avatar_url"),
  537. )
  538. if res["state"] == "join":
  539. if not self.hs.is_mine_id(user_id):
  540. remote_attestation = res["attestation"]
  541. await self.attestations.verify_attestation(
  542. remote_attestation, user_id=user_id, group_id=group_id
  543. )
  544. else:
  545. remote_attestation = None
  546. await self.store.add_user_to_group(
  547. group_id,
  548. user_id,
  549. is_admin=False,
  550. is_public=False, # TODO
  551. local_attestation=local_attestation,
  552. remote_attestation=remote_attestation,
  553. )
  554. return {"state": "join"}
  555. elif res["state"] == "invite":
  556. await self.store.add_group_invite(group_id, user_id)
  557. return {"state": "invite"}
  558. elif res["state"] == "reject":
  559. return {"state": "reject"}
  560. else:
  561. raise SynapseError(502, "Unknown state returned by HS")
  562. async def _add_user(
  563. self, group_id: str, user_id: str, content: JsonDict
  564. ) -> Optional[JsonDict]:
  565. """Add a user to a group based on a content dict.
  566. See accept_invite, join_group.
  567. """
  568. if not self.hs.is_mine_id(user_id):
  569. local_attestation: Optional[
  570. JsonDict
  571. ] = self.attestations.create_attestation(group_id, user_id)
  572. remote_attestation = content["attestation"]
  573. await self.attestations.verify_attestation(
  574. remote_attestation, user_id=user_id, group_id=group_id
  575. )
  576. else:
  577. local_attestation = None
  578. remote_attestation = None
  579. is_public = _parse_visibility_from_contents(content)
  580. await self.store.add_user_to_group(
  581. group_id,
  582. user_id,
  583. is_admin=False,
  584. is_public=is_public,
  585. local_attestation=local_attestation,
  586. remote_attestation=remote_attestation,
  587. )
  588. return local_attestation
  589. async def accept_invite(
  590. self, group_id: str, requester_user_id: str, content: JsonDict
  591. ) -> JsonDict:
  592. """User tries to accept an invite to the group.
  593. This is different from them asking to join, and so should error if no
  594. invite exists (and they're not a member of the group)
  595. """
  596. await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
  597. is_invited = await self.store.is_user_invited_to_local_group(
  598. group_id, requester_user_id
  599. )
  600. if not is_invited:
  601. raise SynapseError(403, "User not invited to group")
  602. local_attestation = await self._add_user(group_id, requester_user_id, content)
  603. return {"state": "join", "attestation": local_attestation}
  604. async def join_group(
  605. self, group_id: str, requester_user_id: str, content: JsonDict
  606. ) -> JsonDict:
  607. """User tries to join the group.
  608. This will error if the group requires an invite/knock to join
  609. """
  610. group_info = await self.check_group_is_ours(
  611. group_id, requester_user_id, and_exists=True
  612. )
  613. if not group_info:
  614. raise SynapseError(404, "Group does not exist", errcode=Codes.NOT_FOUND)
  615. if group_info["join_policy"] != "open":
  616. raise SynapseError(403, "Group is not publicly joinable")
  617. local_attestation = await self._add_user(group_id, requester_user_id, content)
  618. return {"state": "join", "attestation": local_attestation}
  619. async def remove_user_from_group(
  620. self, group_id: str, user_id: str, requester_user_id: str, content: JsonDict
  621. ) -> JsonDict:
  622. """Remove a user from the group; either a user is leaving or an admin
  623. kicked them.
  624. """
  625. await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
  626. is_kick = False
  627. if requester_user_id != user_id:
  628. is_admin = await self.store.is_user_admin_in_group(
  629. group_id, requester_user_id
  630. )
  631. if not is_admin:
  632. raise SynapseError(403, "User is not admin in group")
  633. is_kick = True
  634. await self.store.remove_user_from_group(group_id, user_id)
  635. if is_kick:
  636. if self.hs.is_mine_id(user_id):
  637. groups_local = self.hs.get_groups_local_handler()
  638. assert isinstance(
  639. groups_local, GroupsLocalHandler
  640. ), "Workers cannot remove users from groups."
  641. await groups_local.user_removed_from_group(group_id, user_id, {})
  642. else:
  643. await self.transport_client.remove_user_from_group_notification(
  644. get_domain_from_id(user_id), group_id, user_id, {}
  645. )
  646. if not self.hs.is_mine_id(user_id):
  647. await self.store.maybe_delete_remote_profile_cache(user_id)
  648. # Delete group if the last user has left
  649. users = await self.store.get_users_in_group(group_id, include_private=True)
  650. if not users:
  651. await self.store.delete_group(group_id)
  652. return {}
  653. async def create_group(
  654. self, group_id: str, requester_user_id: str, content: JsonDict
  655. ) -> JsonDict:
  656. logger.info("Attempting to create group with ID: %r", group_id)
  657. # parsing the id into a GroupID validates it.
  658. group_id_obj = GroupID.from_string(group_id)
  659. group = await self.check_group_is_ours(group_id, requester_user_id)
  660. if group:
  661. raise SynapseError(400, "Group already exists")
  662. is_admin = await self.auth.is_server_admin(
  663. UserID.from_string(requester_user_id)
  664. )
  665. if not is_admin:
  666. if not self.hs.config.groups.enable_group_creation:
  667. raise SynapseError(
  668. 403, "Only a server admin can create groups on this server"
  669. )
  670. localpart = group_id_obj.localpart
  671. if not localpart.startswith(self.hs.config.groups.group_creation_prefix):
  672. raise SynapseError(
  673. 400,
  674. "Can only create groups with prefix %r on this server"
  675. % (self.hs.config.groups.group_creation_prefix,),
  676. )
  677. profile = content.get("profile", {})
  678. name = profile.get("name")
  679. avatar_url = profile.get("avatar_url")
  680. short_description = profile.get("short_description")
  681. long_description = profile.get("long_description")
  682. user_profile = content.get("user_profile", {})
  683. await self.store.create_group(
  684. group_id,
  685. requester_user_id,
  686. name=name,
  687. avatar_url=avatar_url,
  688. short_description=short_description,
  689. long_description=long_description,
  690. )
  691. if not self.hs.is_mine_id(requester_user_id):
  692. remote_attestation = content["attestation"]
  693. await self.attestations.verify_attestation(
  694. remote_attestation, user_id=requester_user_id, group_id=group_id
  695. )
  696. local_attestation: Optional[
  697. JsonDict
  698. ] = self.attestations.create_attestation(group_id, requester_user_id)
  699. else:
  700. local_attestation = None
  701. remote_attestation = None
  702. await self.store.add_user_to_group(
  703. group_id,
  704. requester_user_id,
  705. is_admin=True,
  706. is_public=True, # TODO
  707. local_attestation=local_attestation,
  708. remote_attestation=remote_attestation,
  709. )
  710. if not self.hs.is_mine_id(requester_user_id):
  711. await self.store.add_remote_profile_cache(
  712. requester_user_id,
  713. displayname=user_profile.get("displayname"),
  714. avatar_url=user_profile.get("avatar_url"),
  715. )
  716. return {"group_id": group_id}
  717. async def delete_group(self, group_id: str, requester_user_id: str) -> None:
  718. """Deletes a group, kicking out all current members.
  719. Only group admins or server admins can call this request
  720. Args:
  721. group_id: The group ID to delete.
  722. requester_user_id: The user requesting to delete the group.
  723. """
  724. await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
  725. # Only server admins or group admins can delete groups.
  726. is_admin = await self.store.is_user_admin_in_group(group_id, requester_user_id)
  727. if not is_admin:
  728. is_admin = await self.auth.is_server_admin(
  729. UserID.from_string(requester_user_id)
  730. )
  731. if not is_admin:
  732. raise SynapseError(403, "User is not an admin")
  733. # Before deleting the group lets kick everyone out of it
  734. users = await self.store.get_users_in_group(group_id, include_private=True)
  735. async def _kick_user_from_group(user_id):
  736. if self.hs.is_mine_id(user_id):
  737. groups_local = self.hs.get_groups_local_handler()
  738. assert isinstance(
  739. groups_local, GroupsLocalHandler
  740. ), "Workers cannot kick users from groups."
  741. await groups_local.user_removed_from_group(group_id, user_id, {})
  742. else:
  743. await self.transport_client.remove_user_from_group_notification(
  744. get_domain_from_id(user_id), group_id, user_id, {}
  745. )
  746. await self.store.maybe_delete_remote_profile_cache(user_id)
  747. # We kick users out in the order of:
  748. # 1. Non-admins
  749. # 2. Other admins
  750. # 3. The requester
  751. #
  752. # This is so that if the deletion fails for some reason other admins or
  753. # the requester still has auth to retry.
  754. non_admins = []
  755. admins = []
  756. for u in users:
  757. if u["user_id"] == requester_user_id:
  758. continue
  759. if u["is_admin"]:
  760. admins.append(u["user_id"])
  761. else:
  762. non_admins.append(u["user_id"])
  763. await concurrently_execute(_kick_user_from_group, non_admins, 10)
  764. await concurrently_execute(_kick_user_from_group, admins, 10)
  765. await _kick_user_from_group(requester_user_id)
  766. await self.store.delete_group(group_id)
  767. def _parse_join_policy_from_contents(content: JsonDict) -> Optional[str]:
  768. """Given a content for a request, return the specified join policy or None"""
  769. join_policy_dict = content.get("m.join_policy")
  770. if join_policy_dict:
  771. return _parse_join_policy_dict(join_policy_dict)
  772. else:
  773. return None
  774. def _parse_join_policy_dict(join_policy_dict: JsonDict) -> str:
  775. """Given a dict for the "m.join_policy" config return the join policy specified"""
  776. join_policy_type = join_policy_dict.get("type")
  777. if not join_policy_type:
  778. return "invite"
  779. if join_policy_type not in ("invite", "open"):
  780. raise SynapseError(400, "Synapse only supports 'invite'/'open' join rule")
  781. return join_policy_type
  782. def _parse_visibility_from_contents(content: JsonDict) -> bool:
  783. """Given a content for a request parse out whether the entity should be
  784. public or not
  785. """
  786. visibility = content.get("m.visibility")
  787. if visibility:
  788. return _parse_visibility_dict(visibility)
  789. else:
  790. is_public = True
  791. return is_public
  792. def _parse_visibility_dict(visibility: JsonDict) -> bool:
  793. """Given a dict for the "m.visibility" config return if the entity should
  794. be public or not
  795. """
  796. vis_type = visibility.get("type")
  797. if not vis_type:
  798. return True
  799. if vis_type not in ("public", "private"):
  800. raise SynapseError(400, "Synapse only supports 'public'/'private' visibility")
  801. return vis_type == "public"