groups_server.py 32 KB

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