Merge pull request #2363 from matrix-org/erikj/group_server_summary

Add group summary APIs
This commit is contained in:
Erik Johnston 2017-07-17 11:54:14 +01:00 committed by GitHub
commit b3de67234e
4 changed files with 1131 additions and 22 deletions

View File

@ -615,8 +615,8 @@ class FederationGroupsProfileServlet(BaseFederationServlet):
PATH = "/groups/(?P<group_id>[^/]*)/profile$" PATH = "/groups/(?P<group_id>[^/]*)/profile$"
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, origin, content, query, group_id): def on_GET(self, origin, content, query, group_id):
requester_user_id = content["requester_user_id"] requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin: if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin") raise SynapseError(403, "requester_user_id doesn't match origin")
@ -627,14 +627,30 @@ class FederationGroupsProfileServlet(BaseFederationServlet):
defer.returnValue((200, new_content)) defer.returnValue((200, new_content))
class FederationGroupsSummaryServlet(BaseFederationServlet):
PATH = "/groups/(?P<group_id>[^/]*)/summary$"
@defer.inlineCallbacks
def on_GET(self, origin, content, query, group_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
new_content = yield self.handler.get_group_summary(
group_id, requester_user_id
)
defer.returnValue((200, new_content))
class FederationGroupsRoomsServlet(BaseFederationServlet): class FederationGroupsRoomsServlet(BaseFederationServlet):
"""Get the rooms in a group on behalf of a user """Get the rooms in a group on behalf of a user
""" """
PATH = "/groups/(?P<group_id>[^/]*)/rooms$" PATH = "/groups/(?P<group_id>[^/]*)/rooms$"
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, origin, content, query, group_id): def on_GET(self, origin, content, query, group_id):
requester_user_id = content["requester_user_id"] requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin: if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin") raise SynapseError(403, "requester_user_id doesn't match origin")
@ -652,7 +668,7 @@ class FederationGroupsAddRoomsServlet(BaseFederationServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, origin, content, query, group_id, room_id): def on_POST(self, origin, content, query, group_id, room_id):
requester_user_id = content["requester_user_id"] requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin: if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin") raise SynapseError(403, "requester_user_id doesn't match origin")
@ -669,8 +685,8 @@ class FederationGroupsUsersServlet(BaseFederationServlet):
PATH = "/groups/(?P<group_id>[^/]*)/users$" PATH = "/groups/(?P<group_id>[^/]*)/users$"
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, origin, content, query, group_id): def on_GET(self, origin, content, query, group_id):
requester_user_id = content["requester_user_id"] requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin: if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin") raise SynapseError(403, "requester_user_id doesn't match origin")
@ -688,7 +704,7 @@ class FederationGroupsInviteServlet(BaseFederationServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, origin, content, query, group_id, user_id): def on_POST(self, origin, content, query, group_id, user_id):
requester_user_id = content["requester_user_id"] requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin: if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin") raise SynapseError(403, "requester_user_id doesn't match origin")
@ -723,7 +739,7 @@ class FederationGroupsRemoveUserServlet(BaseFederationServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, origin, content, query, group_id, user_id): def on_POST(self, origin, content, query, group_id, user_id):
requester_user_id = content["requester_user_id"] requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin: if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin") raise SynapseError(403, "requester_user_id doesn't match origin")
@ -750,6 +766,244 @@ class FederationGroupsRenewAttestaionServlet(BaseFederationServlet):
defer.returnValue((200, new_content)) defer.returnValue((200, new_content))
class FederationGroupsSummaryRoomsServlet(BaseFederationServlet):
"""Add/remove a room from the group summary, with optional category.
Matches both:
- /groups/:group/summary/rooms/:room_id
- /groups/:group/summary/categories/:category/rooms/:room_id
"""
PATH = (
"/groups/(?P<group_id>[^/]*)/summary"
"(/categories/(?P<category_id>[^/]+))?"
"/rooms/(?P<room_id>[^/]*)$"
)
@defer.inlineCallbacks
def on_POST(self, origin, content, query, group_id, category_id, room_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
if category_id == "":
raise SynapseError(400, "category_id cannot be empty string")
resp = yield self.handler.update_group_summary_room(
group_id, requester_user_id,
room_id=room_id,
category_id=category_id,
content=content,
)
defer.returnValue((200, resp))
@defer.inlineCallbacks
def on_DELETE(self, origin, content, query, group_id, category_id, room_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
if category_id == "":
raise SynapseError(400, "category_id cannot be empty string")
resp = yield self.handler.delete_group_summary_room(
group_id, requester_user_id,
room_id=room_id,
category_id=category_id,
)
defer.returnValue((200, resp))
class FederationGroupsCategoriesServlet(BaseFederationServlet):
"""Get all categories for a group
"""
PATH = (
"/groups/(?P<group_id>[^/]*)/categories/$"
)
@defer.inlineCallbacks
def on_GET(self, origin, content, query, group_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
resp = yield self.handler.get_group_categories(
group_id, requester_user_id,
)
defer.returnValue((200, resp))
class FederationGroupsCategoryServlet(BaseFederationServlet):
"""Add/remove/get a category in a group
"""
PATH = (
"/groups/(?P<group_id>[^/]*)/categories/(?P<category_id>[^/]+)$"
)
@defer.inlineCallbacks
def on_GET(self, origin, content, query, group_id, category_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
resp = yield self.handler.get_group_category(
group_id, requester_user_id, category_id
)
defer.returnValue((200, resp))
@defer.inlineCallbacks
def on_POST(self, origin, content, query, group_id, category_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
if category_id == "":
raise SynapseError(400, "category_id cannot be empty string")
resp = yield self.handler.upsert_group_category(
group_id, requester_user_id, category_id, content,
)
defer.returnValue((200, resp))
@defer.inlineCallbacks
def on_DELETE(self, origin, content, query, group_id, category_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
if category_id == "":
raise SynapseError(400, "category_id cannot be empty string")
resp = yield self.handler.delete_group_category(
group_id, requester_user_id, category_id,
)
defer.returnValue((200, resp))
class FederationGroupsRolesServlet(BaseFederationServlet):
"""Get roles in a group
"""
PATH = (
"/groups/(?P<group_id>[^/]*)/roles/$"
)
@defer.inlineCallbacks
def on_GET(self, origin, content, query, group_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
resp = yield self.handler.get_group_roles(
group_id, requester_user_id,
)
defer.returnValue((200, resp))
class FederationGroupsRoleServlet(BaseFederationServlet):
"""Add/remove/get a role in a group
"""
PATH = (
"/groups/(?P<group_id>[^/]*)/roles/(?P<role_id>[^/]+)$"
)
@defer.inlineCallbacks
def on_GET(self, origin, content, query, group_id, role_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
resp = yield self.handler.get_group_role(
group_id, requester_user_id, role_id
)
defer.returnValue((200, resp))
@defer.inlineCallbacks
def on_POST(self, origin, content, query, group_id, role_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
if role_id == "":
raise SynapseError(400, "role_id cannot be empty string")
resp = yield self.handler.update_group_role(
group_id, requester_user_id, role_id, content,
)
defer.returnValue((200, resp))
@defer.inlineCallbacks
def on_DELETE(self, origin, content, query, group_id, role_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
if role_id == "":
raise SynapseError(400, "role_id cannot be empty string")
resp = yield self.handler.delete_group_role(
group_id, requester_user_id, role_id,
)
defer.returnValue((200, resp))
class FederationGroupsSummaryUsersServlet(BaseFederationServlet):
"""Add/remove a user from the group summary, with optional role.
Matches both:
- /groups/:group/summary/users/:user_id
- /groups/:group/summary/roles/:role/users/:user_id
"""
PATH = (
"/groups/(?P<group_id>[^/]*)/summary"
"(/roles/(?P<role_id>[^/]+))?"
"/users/(?P<user_id>[^/]*)$"
)
@defer.inlineCallbacks
def on_POST(self, origin, content, query, group_id, role_id, user_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
if role_id == "":
raise SynapseError(400, "role_id cannot be empty string")
resp = yield self.handler.update_group_summary_user(
group_id, requester_user_id,
user_id=user_id,
role_id=role_id,
content=content,
)
defer.returnValue((200, resp))
@defer.inlineCallbacks
def on_DELETE(self, origin, content, query, group_id, role_id, user_id):
requester_user_id = query["requester_user_id"]
if get_domain_from_id(requester_user_id) != origin:
raise SynapseError(403, "requester_user_id doesn't match origin")
if role_id == "":
raise SynapseError(400, "role_id cannot be empty string")
resp = yield self.handler.delete_group_summary_user(
group_id, requester_user_id,
user_id=user_id,
role_id=role_id,
)
defer.returnValue((200, resp))
FEDERATION_SERVLET_CLASSES = ( FEDERATION_SERVLET_CLASSES = (
FederationSendServlet, FederationSendServlet,
FederationPullServlet, FederationPullServlet,
@ -784,11 +1038,18 @@ ROOM_LIST_CLASSES = (
GROUP_SERVER_SERVLET_CLASSES = ( GROUP_SERVER_SERVLET_CLASSES = (
FederationGroupsProfileServlet, FederationGroupsProfileServlet,
FederationGroupsSummaryServlet,
FederationGroupsRoomsServlet, FederationGroupsRoomsServlet,
FederationGroupsUsersServlet, FederationGroupsUsersServlet,
FederationGroupsInviteServlet, FederationGroupsInviteServlet,
FederationGroupsAcceptInviteServlet, FederationGroupsAcceptInviteServlet,
FederationGroupsRemoveUserServlet, FederationGroupsRemoveUserServlet,
FederationGroupsSummaryRoomsServlet,
FederationGroupsCategoriesServlet,
FederationGroupsCategoryServlet,
FederationGroupsRolesServlet,
FederationGroupsRoleServlet,
FederationGroupsSummaryUsersServlet,
) )

View File

@ -50,10 +50,16 @@ class GroupsServerHandler(object):
hs.get_groups_attestation_renewer() hs.get_groups_attestation_renewer()
@defer.inlineCallbacks @defer.inlineCallbacks
def check_group_is_ours(self, group_id, and_exists=False): def check_group_is_ours(self, group_id, and_exists=False, and_is_admin=None):
"""Check that the group is ours, and optionally if it exists. """Check that the group is ours, and optionally if it exists.
If group does exist then return group. If group does exist then return group.
Args:
group_id (str)
and_exists (bool): whether to also check if group exists
and_is_admin (str): whether to also check if given str is a user_id
that is an admin
""" """
if not self.is_mine_id(group_id): if not self.is_mine_id(group_id):
raise SynapseError(400, "Group not on this server") raise SynapseError(400, "Group not on this server")
@ -62,8 +68,261 @@ class GroupsServerHandler(object):
if and_exists and not group: if and_exists and not group:
raise SynapseError(404, "Unknown group") raise SynapseError(404, "Unknown group")
if and_is_admin:
is_admin = yield self.store.is_user_admin_in_group(group_id, and_is_admin)
if not is_admin:
raise SynapseError(403, "User is not admin in group")
defer.returnValue(group) defer.returnValue(group)
@defer.inlineCallbacks
def get_group_summary(self, group_id, requester_user_id):
"""Get the summary for a group as seen by requester_user_id.
The group summary consists of the profile of the room, and a curated
list of users and rooms. These list *may* be organised by role/category.
The roles/categories are ordered, and so are the users/rooms within them.
A user/room may appear in multiple roles/categories.
"""
yield self.check_group_is_ours(group_id, and_exists=True)
is_user_in_group = yield self.store.is_user_in_group(requester_user_id, group_id)
profile = yield self.get_group_profile(group_id, requester_user_id)
users, roles = yield self.store.get_users_for_summary_by_role(
group_id, include_private=is_user_in_group,
)
# TODO: Add profiles to users
rooms, categories = yield self.store.get_rooms_for_summary_by_category(
group_id, include_private=is_user_in_group,
)
for room_entry in rooms:
room_id = room_entry["room_id"]
joined_users = yield self.store.get_users_in_room(room_id)
entry = yield self.room_list_handler.generate_room_entry(
room_id, len(joined_users),
with_alias=False, allow_private=True,
)
entry.pop("room_id", None)
room_entry["profile"] = entry
rooms.sort(key=lambda e: e.get("order", 0))
for entry in users:
user_id = entry["user_id"]
if not self.is_mine_id(requester_user_id):
attestation = yield self.store.get_remote_attestation(group_id, user_id)
if not attestation:
continue
entry["attestation"] = attestation
else:
entry["attestation"] = self.attestations.create_attestation(
group_id, user_id,
)
users.sort(key=lambda e: e.get("order", 0))
defer.returnValue({
"profile": profile,
"users_section": {
"users": users,
"roles": roles,
"total_user_count_estimate": 0, # TODO
},
"rooms_section": {
"rooms": rooms,
"categories": categories,
"total_room_count_estimate": 0, # TODO
},
})
@defer.inlineCallbacks
def update_group_summary_room(self, group_id, user_id, room_id, category_id, content):
"""Add/update a room to the group summary
"""
yield self.check_group_is_ours(group_id, and_exists=True, and_is_admin=user_id)
order = content.get("order", None)
is_public = _parse_visibility_from_contents(content)
yield self.store.add_room_to_summary(
group_id=group_id,
room_id=room_id,
category_id=category_id,
order=order,
is_public=is_public,
)
defer.returnValue({})
@defer.inlineCallbacks
def delete_group_summary_room(self, group_id, user_id, room_id, category_id):
"""Remove a room from the summary
"""
yield self.check_group_is_ours(group_id, and_exists=True, and_is_admin=user_id)
yield self.store.remove_room_from_summary(
group_id=group_id,
room_id=room_id,
category_id=category_id,
)
defer.returnValue({})
@defer.inlineCallbacks
def get_group_categories(self, group_id, user_id):
"""Get all categories in a group (as seen by user)
"""
yield self.check_group_is_ours(group_id, and_exists=True)
categories = yield self.store.get_group_categories(
group_id=group_id,
)
defer.returnValue({"categories": categories})
@defer.inlineCallbacks
def get_group_category(self, group_id, user_id, category_id):
"""Get a specific category in a group (as seen by user)
"""
yield self.check_group_is_ours(group_id, and_exists=True)
res = yield self.store.get_group_category(
group_id=group_id,
category_id=category_id,
)
defer.returnValue(res)
@defer.inlineCallbacks
def update_group_category(self, group_id, user_id, category_id, content):
"""Add/Update a group category
"""
yield self.check_group_is_ours(group_id, and_exists=True, and_is_admin=user_id)
is_public = _parse_visibility_from_contents(content)
profile = content.get("profile")
yield self.store.upsert_group_category(
group_id=group_id,
category_id=category_id,
is_public=is_public,
profile=profile,
)
defer.returnValue({})
@defer.inlineCallbacks
def delete_group_category(self, group_id, user_id, category_id):
"""Delete a group category
"""
yield self.check_group_is_ours(group_id, and_exists=True, and_is_admin=user_id)
yield self.store.remove_group_category(
group_id=group_id,
category_id=category_id,
)
defer.returnValue({})
@defer.inlineCallbacks
def get_group_roles(self, group_id, user_id):
"""Get all roles in a group (as seen by user)
"""
yield self.check_group_is_ours(group_id, and_exists=True)
roles = yield self.store.get_group_roles(
group_id=group_id,
)
defer.returnValue({"roles": roles})
@defer.inlineCallbacks
def get_group_role(self, group_id, user_id, role_id):
"""Get a specific role in a group (as seen by user)
"""
yield self.check_group_is_ours(group_id, and_exists=True)
res = yield self.store.get_group_role(
group_id=group_id,
role_id=role_id,
)
defer.returnValue(res)
@defer.inlineCallbacks
def update_group_role(self, group_id, user_id, role_id, content):
"""Add/update a role in a group
"""
yield self.check_group_is_ours(group_id, and_exists=True, and_is_admin=user_id)
is_public = _parse_visibility_from_contents(content)
profile = content.get("profile")
yield self.store.upsert_group_role(
group_id=group_id,
role_id=role_id,
is_public=is_public,
profile=profile,
)
defer.returnValue({})
@defer.inlineCallbacks
def delete_group_role(self, group_id, user_id, role_id):
"""Remove role from group
"""
yield self.check_group_is_ours(group_id, and_exists=True, and_is_admin=user_id)
yield self.store.remove_group_role(
group_id=group_id,
role_id=role_id,
)
defer.returnValue({})
@defer.inlineCallbacks
def update_group_summary_user(self, group_id, requester_user_id, user_id, role_id,
content):
"""Add/update a users entry in the group summary
"""
yield self.check_group_is_ours(group_id, and_exists=True, and_is_admin=user_id)
order = content.get("order", None)
is_public = _parse_visibility_from_contents(content)
yield self.store.add_user_to_summary(
group_id=group_id,
user_id=user_id,
role_id=role_id,
order=order,
is_public=is_public,
)
defer.returnValue({})
@defer.inlineCallbacks
def delete_group_summary_user(self, group_id, requester_user_id, user_id, role_id):
"""Remove a user from the group summary
"""
yield self.check_group_is_ours(group_id, and_exists=True, and_is_admin=user_id)
yield self.store.remove_user_from_summary(
group_id=group_id,
user_id=user_id,
role_id=role_id,
)
defer.returnValue({})
@defer.inlineCallbacks @defer.inlineCallbacks
def get_group_profile(self, group_id, requester_user_id): def get_group_profile(self, group_id, requester_user_id):
"""Get the group profile as seen by requester_user_id """Get the group profile as seen by requester_user_id
@ -170,12 +429,9 @@ class GroupsServerHandler(object):
def add_room(self, group_id, requester_user_id, room_id, content): def add_room(self, group_id, requester_user_id, room_id, content):
"""Add room to group """Add room to group
""" """
yield self.check_group_is_ours(
yield self.check_group_is_ours(group_id, and_exists=True) group_id, and_exists=True, and_is_admin=requester_user_id
)
is_admin = yield self.store.is_user_admin_in_group(group_id, requester_user_id)
if not is_admin:
raise SynapseError(403, "User is not admin in group")
# TODO: Check if room has already been added # TODO: Check if room has already been added
@ -190,13 +446,9 @@ class GroupsServerHandler(object):
"""Invite user to group """Invite user to group
""" """
group = yield self.check_group_is_ours(group_id, and_exists=True) group = yield self.check_group_is_ours(
group_id, and_exists=True, and_is_admin=requester_user_id
is_admin = yield self.store.is_user_admin_in_group(
group_id, requester_user_id
) )
if not is_admin:
raise SynapseError(403, "User is not admin in group")
# TODO: Check if user knocked # TODO: Check if user knocked
# TODO: Check if user is already invited # TODO: Check if user is already invited

View File

@ -15,11 +15,19 @@
from twisted.internet import defer from twisted.internet import defer
from synapse.api.errors import SynapseError
from ._base import SQLBaseStore from ._base import SQLBaseStore
import ujson as json import ujson as json
# The category ID for the "default" category. We don't store as null in the
# database to avoid the fun of null != null
_DEFAULT_CATEGORY_ID = ""
_DEFAULT_ROLE_ID = ""
class GroupServerStore(SQLBaseStore): class GroupServerStore(SQLBaseStore):
def get_group(self, group_id): def get_group(self, group_id):
return self._simple_select_one( return self._simple_select_one(
@ -64,6 +72,531 @@ class GroupServerStore(SQLBaseStore):
desc="get_rooms_in_group", desc="get_rooms_in_group",
) )
def get_rooms_for_summary_by_category(self, group_id, include_private=False):
"""Get the rooms and categories that should be included in a summary request
Returns ([rooms], [categories])
"""
def _get_rooms_for_summary_txn(txn):
keyvalues = {
"group_id": group_id,
}
if not include_private:
keyvalues["is_public"] = True
sql = """
SELECT room_id, is_public, category_id, room_order
FROM group_summary_rooms
WHERE group_id = ?
"""
if not include_private:
sql += " AND is_public = ?"
txn.execute(sql, (group_id, True))
else:
txn.execute(sql, (group_id,))
rooms = [
{
"room_id": row[0],
"is_public": row[1],
"category_id": row[2] if row[2] != _DEFAULT_CATEGORY_ID else None,
"order": row[3],
}
for row in txn
]
sql = """
SELECT category_id, is_public, profile, cat_order
FROM group_summary_room_categories
INNER JOIN group_room_categories USING (group_id, category_id)
WHERE group_id = ?
"""
if not include_private:
sql += " AND is_public = ?"
txn.execute(sql, (group_id, True))
else:
txn.execute(sql, (group_id,))
categories = {
row[0]: {
"is_public": row[1],
"profile": json.loads(row[2]),
"order": row[3],
}
for row in txn
}
return rooms, categories
return self.runInteraction(
"get_rooms_for_summary", _get_rooms_for_summary_txn
)
def add_room_to_summary(self, group_id, room_id, category_id, order, is_public):
return self.runInteraction(
"add_room_to_summary", self._add_room_to_summary_txn,
group_id, room_id, category_id, order, is_public,
)
def _add_room_to_summary_txn(self, txn, group_id, room_id, category_id, order,
is_public):
"""Add (or update) room's entry in summary.
Args:
group_id (str)
room_id (str)
category_id (str): If not None then adds the category to the end of
the summary if its not already there. [Optional]
order (int): If not None inserts the room at that position, e.g.
an order of 1 will put the room first. Otherwise, the room gets
added to the end.
"""
if category_id is None:
category_id = _DEFAULT_CATEGORY_ID
else:
cat_exists = self._simple_select_one_onecol_txn(
txn,
table="group_room_categories",
keyvalues={
"group_id": group_id,
"category_id": category_id,
},
retcol="group_id",
allow_none=True,
)
if not cat_exists:
raise SynapseError(400, "Category doesn't exist")
# TODO: Check category is part of summary already
cat_exists = self._simple_select_one_onecol_txn(
txn,
table="group_summary_room_categories",
keyvalues={
"group_id": group_id,
"category_id": category_id,
},
retcol="group_id",
allow_none=True,
)
if not cat_exists:
# If not, add it with an order larger than all others
txn.execute("""
INSERT INTO group_summary_room_categories
(group_id, category_id, cat_order)
SELECT ?, ?, COALESCE(MAX(cat_order), 0) + 1
FROM group_summary_room_categories
WHERE group_id = ? AND category_id = ?
""", (group_id, category_id, group_id, category_id))
existing = self._simple_select_one_txn(
txn,
table="group_summary_rooms",
keyvalues={
"group_id": group_id,
"room_id": room_id,
"category_id": category_id,
},
retcols=("room_order", "is_public",),
allow_none=True,
)
if order is not None:
# Shuffle other room orders that come after the given order
sql = """
UPDATE group_summary_rooms SET room_order = room_order + 1
WHERE group_id = ? AND category_id = ? AND room_order >= ?
"""
txn.execute(sql, (group_id, category_id, order,))
elif not existing:
sql = """
SELECT COALESCE(MAX(room_order), 0) + 1 FROM group_summary_rooms
WHERE group_id = ? AND category_id = ?
"""
txn.execute(sql, (group_id, category_id,))
order, = txn.fetchone()
if existing:
to_update = {}
if order is not None:
to_update["room_order"] = order
if is_public is not None:
to_update["is_public"] = is_public
self._simple_update_txn(
txn,
table="group_summary_rooms",
keyvalues={
"group_id": group_id,
"category_id": category_id,
"room_id": room_id,
},
values=to_update,
)
else:
if is_public is None:
is_public = True
self._simple_insert_txn(
txn,
table="group_summary_rooms",
values={
"group_id": group_id,
"category_id": category_id,
"room_id": room_id,
"room_order": order,
"is_public": is_public,
},
)
def remove_room_from_summary(self, group_id, room_id, category_id):
if category_id is None:
category_id = _DEFAULT_CATEGORY_ID
return self._simple_delete(
table="group_summary_rooms",
keyvalues={
"group_id": group_id,
"category_id": category_id,
"room_id": room_id,
},
desc="remove_room_from_summary",
)
@defer.inlineCallbacks
def get_group_categories(self, group_id):
rows = yield self._simple_select_list(
table="group_room_categories",
keyvalues={
"group_id": group_id,
},
retcols=("category_id", "is_public", "profile"),
desc="get_group_categories",
)
defer.returnValue({
row["category_id"]: {
"is_public": row["is_public"],
"profile": json.loads(row["profile"]),
}
for row in rows
})
@defer.inlineCallbacks
def get_group_category(self, group_id, category_id):
category = yield self._simple_select_one(
table="group_room_categories",
keyvalues={
"group_id": group_id,
"category_id": category_id,
},
retcols=("is_public", "profile"),
desc="get_group_category",
)
category["profile"] = json.loads(category["profile"])
defer.returnValue(category)
def upsert_group_category(self, group_id, category_id, profile, is_public):
"""Add/update room category for group
"""
insertion_values = {}
update_values = {"category_id": category_id} # This cannot be empty
if profile is None:
insertion_values["profile"] = "{}"
else:
update_values["profile"] = json.dumps(profile)
if is_public is None:
insertion_values["is_public"] = True
else:
update_values["is_public"] = is_public
return self._simple_upsert(
table="group_room_categories",
keyvalues={
"group_id": group_id,
"category_id": category_id,
},
values=update_values,
insertion_values=insertion_values,
desc="upsert_group_category",
)
def remove_group_category(self, group_id, category_id):
return self._simple_delete(
table="group_room_categories",
keyvalues={
"group_id": group_id,
"category_id": category_id,
},
desc="remove_group_category",
)
@defer.inlineCallbacks
def get_group_roles(self, group_id):
rows = yield self._simple_select_list(
table="group_roles",
keyvalues={
"group_id": group_id,
},
retcols=("role_id", "is_public", "profile"),
desc="get_group_roles",
)
defer.returnValue({
row["role_id"]: {
"is_public": row["is_public"],
"profile": json.loads(row["profile"]),
}
for row in rows
})
@defer.inlineCallbacks
def get_group_role(self, group_id, role_id):
role = yield self._simple_select_one(
table="group_roles",
keyvalues={
"group_id": group_id,
"role_id": role_id,
},
retcols=("is_public", "profile"),
desc="get_group_role",
)
role["profile"] = json.loads(role["profile"])
defer.returnValue(role)
def upsert_group_role(self, group_id, role_id, profile, is_public):
"""Add/remove user role
"""
insertion_values = {}
update_values = {"role_id": role_id} # This cannot be empty
if profile is None:
insertion_values["profile"] = "{}"
else:
update_values["profile"] = json.dumps(profile)
if is_public is None:
insertion_values["is_public"] = True
else:
update_values["is_public"] = is_public
return self._simple_upsert(
table="group_roles",
keyvalues={
"group_id": group_id,
"role_id": role_id,
},
values=update_values,
insertion_values=insertion_values,
desc="upsert_group_role",
)
def remove_group_role(self, group_id, role_id):
return self._simple_delete(
table="group_roles",
keyvalues={
"group_id": group_id,
"role_id": role_id,
},
desc="remove_group_role",
)
def add_user_to_summary(self, group_id, user_id, role_id, order, is_public):
return self.runInteraction(
"add_user_to_summary", self._add_user_to_summary_txn,
group_id, user_id, role_id, order, is_public,
)
def _add_user_to_summary_txn(self, txn, group_id, user_id, role_id, order,
is_public):
"""Add (or update) user's entry in summary.
Args:
group_id (str)
user_id (str)
role_id (str): If not None then adds the role to the end of
the summary if its not already there. [Optional]
order (int): If not None inserts the user at that position, e.g.
an order of 1 will put the user first. Otherwise, the user gets
added to the end.
"""
if role_id is None:
role_id = _DEFAULT_ROLE_ID
else:
role_exists = self._simple_select_one_onecol_txn(
txn,
table="group_roles",
keyvalues={
"group_id": group_id,
"role_id": role_id,
},
retcol="group_id",
allow_none=True,
)
if not role_exists:
raise SynapseError(400, "Role doesn't exist")
# TODO: Check role is part of the summary already
role_exists = self._simple_select_one_onecol_txn(
txn,
table="group_summary_roles",
keyvalues={
"group_id": group_id,
"role_id": role_id,
},
retcol="group_id",
allow_none=True,
)
if not role_exists:
# If not, add it with an order larger than all others
txn.execute("""
INSERT INTO group_summary_roles
(group_id, role_id, role_order)
SELECT ?, ?, COALESCE(MAX(role_order), 0) + 1
FROM group_summary_roles
WHERE group_id = ? AND role_id = ?
""", (group_id, role_id, group_id, role_id))
existing = self._simple_select_one_txn(
txn,
table="group_summary_users",
keyvalues={
"group_id": group_id,
"user_id": user_id,
"role_id": role_id,
},
retcols=("user_order", "is_public",),
allow_none=True,
)
if order is not None:
# Shuffle other users orders that come after the given order
sql = """
UPDATE group_summary_users SET user_order = user_order + 1
WHERE group_id = ? AND role_id = ? AND user_order >= ?
"""
txn.execute(sql, (group_id, role_id, order,))
elif not existing:
sql = """
SELECT COALESCE(MAX(user_order), 0) + 1 FROM group_summary_users
WHERE group_id = ? AND role_id = ?
"""
txn.execute(sql, (group_id, role_id,))
order, = txn.fetchone()
if existing:
to_update = {}
if order is not None:
to_update["user_order"] = order
if is_public is not None:
to_update["is_public"] = is_public
self._simple_update_txn(
txn,
table="group_summary_users",
keyvalues={
"group_id": group_id,
"role_id": role_id,
"user_id": user_id,
},
values=to_update,
)
else:
if is_public is None:
is_public = True
self._simple_insert_txn(
txn,
table="group_summary_users",
values={
"group_id": group_id,
"role_id": role_id,
"user_id": user_id,
"user_order": order,
"is_public": is_public,
},
)
def remove_user_from_summary(self, group_id, user_id, role_id):
if role_id is None:
role_id = _DEFAULT_ROLE_ID
return self._simple_delete(
table="group_summary_users",
keyvalues={
"group_id": group_id,
"role_id": role_id,
"user_id": user_id,
},
desc="remove_user_from_summary",
)
def get_users_for_summary_by_role(self, group_id, include_private=False):
"""Get the users and roles that should be included in a summary request
Returns ([users], [roles])
"""
def _get_users_for_summary_txn(txn):
keyvalues = {
"group_id": group_id,
}
if not include_private:
keyvalues["is_public"] = True
sql = """
SELECT user_id, is_public, role_id, user_order
FROM group_summary_users
WHERE group_id = ?
"""
if not include_private:
sql += " AND is_public = ?"
txn.execute(sql, (group_id, True))
else:
txn.execute(sql, (group_id,))
users = [
{
"user_id": row[0],
"is_public": row[1],
"role_id": row[2] if row[2] != _DEFAULT_ROLE_ID else None,
"order": row[3],
}
for row in txn
]
sql = """
SELECT role_id, is_public, profile, role_order
FROM group_summary_roles
INNER JOIN group_roles USING (group_id, role_id)
WHERE group_id = ?
"""
if not include_private:
sql += " AND is_public = ?"
txn.execute(sql, (group_id, True))
else:
txn.execute(sql, (group_id,))
roles = {
row[0]: {
"is_public": row[1],
"profile": json.loads(row[2]),
"order": row[3],
}
for row in txn
}
return users, roles
return self.runInteraction(
"get_users_for_summary_by_role", _get_users_for_summary_txn
)
def is_user_in_group(self, user_id, group_id): def is_user_in_group(self, user_id, group_id):
return self._simple_select_one_onecol( return self._simple_select_one_onecol(
table="group_users", table="group_users",

View File

@ -56,6 +56,69 @@ CREATE INDEX groups_rooms_g_idx ON group_rooms(group_id, room_id);
CREATE INDEX groups_rooms_r_idx ON group_rooms(room_id); CREATE INDEX groups_rooms_r_idx ON group_rooms(room_id);
-- Rooms to include in the summary
CREATE TABLE group_summary_rooms (
group_id TEXT NOT NULL,
room_id TEXT NOT NULL,
category_id TEXT NOT NULL,
room_order BIGINT NOT NULL,
is_public BOOLEAN NOT NULL, -- whether the room should be show to everyone
UNIQUE (group_id, category_id, room_id, room_order),
CHECK (room_order > 0)
);
CREATE UNIQUE INDEX group_summary_rooms_g_idx ON group_summary_rooms(group_id, room_id, category_id);
-- Categories to include in the summary
CREATE TABLE group_summary_room_categories (
group_id TEXT NOT NULL,
category_id TEXT NOT NULL,
cat_order BIGINT NOT NULL,
UNIQUE (group_id, category_id, cat_order),
CHECK (cat_order > 0)
);
-- The categories in the group
CREATE TABLE group_room_categories (
group_id TEXT NOT NULL,
category_id TEXT NOT NULL,
profile TEXT NOT NULL,
is_public BOOLEAN NOT NULL, -- whether the category should be show to everyone
UNIQUE (group_id, category_id)
);
-- The users to include in the group summary
CREATE TABLE group_summary_users (
group_id TEXT NOT NULL,
user_id TEXT NOT NULL,
role_id TEXT NOT NULL,
user_order BIGINT NOT NULL,
is_public BOOLEAN NOT NULL -- whether the user should be show to everyone
);
CREATE INDEX group_summary_users_g_idx ON group_summary_users(group_id);
-- The roles to include in the group summary
CREATE TABLE group_summary_roles (
group_id TEXT NOT NULL,
role_id TEXT NOT NULL,
role_order BIGINT NOT NULL,
UNIQUE (group_id, role_id, role_order),
CHECK (role_order > 0)
);
-- The roles in a groups
CREATE TABLE group_roles (
group_id TEXT NOT NULL,
role_id TEXT NOT NULL,
profile TEXT NOT NULL,
is_public BOOLEAN NOT NULL, -- whether the role should be show to everyone
UNIQUE (group_id, role_id)
);
-- List of attestations we've given out and need to renew -- List of attestations we've given out and need to renew
CREATE TABLE group_attestations_renewals ( CREATE TABLE group_attestations_renewals (
group_id TEXT NOT NULL, group_id TEXT NOT NULL,