groups_server.py 32 KB

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