From 0788fb51189bf2ca39fefef808fd32b90a133035 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 30 Mar 2023 15:46:40 -0400 Subject: [PATCH] feat: #11420, add new GET routes to retrieve pending and invited members of a group, plus accept/reject pending --- public/language/en-GB/error.json | 2 + .../write/groups/slug/membership/uid.yaml | 5 +- src/api/groups.js | 48 ++++++++++++++++++- src/controllers/write/groups.js | 20 ++++++++ src/groups/index.js | 4 +- src/groups/invite.js | 15 +++--- src/routes/write/groups.js | 11 +++++ src/socket.io/groups.js | 21 +------- test/groups.js | 3 +- 9 files changed, 98 insertions(+), 31 deletions(-) diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index 4daad12c70..a4ae4d1e38 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -154,6 +154,8 @@ "group-already-requested": "Your membership request has already been submitted", "group-join-disabled": "You are not able to join this group at this time", "group-leave-disabled": "You are not able to leave this group at this time", + "group-user-not-pending": "User does not have a pending request to join this group.", + "gorup-user-not-invited": "User has not been invited to join this group.", "post-already-deleted": "This post has already been deleted", "post-already-restored": "This post has already been restored", diff --git a/public/openapi/write/groups/slug/membership/uid.yaml b/public/openapi/write/groups/slug/membership/uid.yaml index 9a5fd7df47..d08039504d 100644 --- a/public/openapi/write/groups/slug/membership/uid.yaml +++ b/public/openapi/write/groups/slug/membership/uid.yaml @@ -2,7 +2,10 @@ put: tags: - groups summary: join a group - description: This operation joins an existing group, or causes another user to join a group. If the group is private and you are not an administrator, this method will cause that user to request membership, instead. For user _invitations_, you'll want to call `PUT /groups/{slug}/invites/{uid}`. + description: | + This operation joins an existing group, or causes another user to join a group. + If the group is private and you are not an administrator, this method will cause that user to request membership, instead. + For user _invitations_, you'll want to call `POST /groups/{slug}/invites/{uid}`. parameters: - in: path name: slug diff --git a/src/api/groups.js b/src/api/groups.js index 9e6aa30d0b..1abc90ada8 100644 --- a/src/api/groups.js +++ b/src/api/groups.js @@ -206,11 +206,57 @@ groupsAPI.rescind = async (caller, data) => { await groups.ownership.rescind(data.uid, groupName); logGroupEvent(caller, 'group-owner-rescind', { - groupName: groupName, + groupName, targetUid: data.uid, }); }; +groupsAPI.getPending = async (caller, { slug }) => { + const groupName = await groups.getGroupNameByGroupSlug(slug); + await isOwner(caller, groupName); + + return await groups.getPending(groupName); +}; + +groupsAPI.accept = async (caller, { slug, uid }) => { + const groupName = await groups.getGroupNameByGroupSlug(slug); + + await isOwner(caller, groupName); + const isPending = await groups.isPending(uid, groupName); + if (!isPending) { + throw new Error('[[error:group-user-not-pending]]'); + } + + await groups.acceptMembership(groupName, uid); + logGroupEvent(caller, 'group-accept-membership', { + groupName, + targetUid: uid, + }); +}; + +groupsAPI.reject = async (caller, { slug, uid }) => { + const groupName = await groups.getGroupNameByGroupSlug(slug); + + await isOwner(caller, groupName); + const isPending = await groups.isPending(uid, groupName); + if (!isPending) { + throw new Error('[[error:group-user-not-pending]]'); + } + + await groups.rejectMembership(groupName, uid); + logGroupEvent(caller, 'group-reject-membership', { + groupName, + targetUid: uid, + }); +}; + +groupsAPI.getInvites = async (caller, { slug }) => { + const groupName = await groups.getGroupNameByGroupSlug(slug); + await isOwner(caller, groupName); + + return await groups.getInvites(groupName); +}; + async function isOwner(caller, groupName) { if (typeof groupName !== 'string') { throw new Error('[[error:invalid-group-name]]'); diff --git a/src/controllers/write/groups.js b/src/controllers/write/groups.js index f4019cb075..ed66d6f075 100644 --- a/src/controllers/write/groups.js +++ b/src/controllers/write/groups.js @@ -47,3 +47,23 @@ Groups.rescind = async (req, res) => { await api.groups.rescind(req, req.params); helpers.formatApiResponse(200, res); }; + +Groups.getPending = async (req, res) => { + const pending = await api.groups.getPending(req, req.params); + helpers.formatApiResponse(200, res, { pending }); +}; + +Groups.accept = async (req, res) => { + await api.groups.accept(req, req.params); + helpers.formatApiResponse(200, res); +}; + +Groups.reject = async (req, res) => { + await api.groups.reject(req, req.params); + helpers.formatApiResponse(200, res); +}; + +Groups.getInvites = async (req, res) => { + const invites = await api.groups.getInvites(req, req.params); + helpers.formatApiResponse(200, res, { invites }); +}; diff --git a/src/groups/index.js b/src/groups/index.js index 5f1904f632..a66e34c6b3 100644 --- a/src/groups/index.js +++ b/src/groups/index.js @@ -123,8 +123,8 @@ Groups.get = async function (groupName, options) { const [groupData, members, pending, invited, isMember, isPending, isInvited, isOwner] = await Promise.all([ Groups.getGroupData(groupName), Groups.getOwnersAndMembers(groupName, options.uid, 0, stop), - Groups.getUsersFromSet(`group:${groupName}:pending`, ['username', 'userslug', 'picture']), - Groups.getUsersFromSet(`group:${groupName}:invited`, ['username', 'userslug', 'picture']), + Groups.getPending(groupName), + Groups.getInvites(groupName), Groups.isMember(options.uid, groupName), Groups.isPending(options.uid, groupName), Groups.isInvited(options.uid, groupName), diff --git a/src/groups/invite.js b/src/groups/invite.js index 5cabdbdcdf..ddb244cd0a 100644 --- a/src/groups/invite.js +++ b/src/groups/invite.js @@ -9,6 +9,14 @@ const plugins = require('../plugins'); const notifications = require('../notifications'); module.exports = function (Groups) { + Groups.getPending = async function (groupName) { + return await Groups.getUsersFromSet(`group:${groupName}:pending`, ['username', 'userslug', 'picture']); + }; + + Groups.getInvites = async function (groupName) { + return await Groups.getUsersFromSet(`group:${groupName}:invited`, ['username', 'userslug', 'picture']); + }; + Groups.requestMembership = async function (groupName, uid) { await inviteOrRequestMembership(groupName, uid, 'request'); const { displayname } = await user.getUserFields(uid, ['username']); @@ -107,11 +115,4 @@ module.exports = function (Groups) { const map = _.zipObject(checkUids, isMembers); return isArray ? uids.map(uid => !!map[uid]) : !!map[uids[0]]; } - - Groups.getPending = async function (groupName) { - if (!groupName) { - return []; - } - return await db.getSetMembers(`group:${groupName}:pending`); - }; }; diff --git a/src/routes/write/groups.js b/src/routes/write/groups.js index 0245690fcb..f247d69e9a 100644 --- a/src/routes/write/groups.js +++ b/src/routes/write/groups.js @@ -14,10 +14,21 @@ module.exports = function () { setupApiRoute(router, 'head', '/:slug', [middleware.assert.group], controllers.write.groups.exists); setupApiRoute(router, 'put', '/:slug', [...middlewares, middleware.assert.group], controllers.write.groups.update); setupApiRoute(router, 'delete', '/:slug', [...middlewares, middleware.assert.group], controllers.write.groups.delete); + setupApiRoute(router, 'put', '/:slug/membership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.join); setupApiRoute(router, 'delete', '/:slug/membership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.leave); + setupApiRoute(router, 'put', '/:slug/ownership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.grant); setupApiRoute(router, 'delete', '/:slug/ownership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.rescind); + setupApiRoute(router, 'get', '/:slug/pending', [...middlewares, middleware.assert.group], controllers.write.groups.getPending); + setupApiRoute(router, 'put', '/:slug/pending/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.accept); + setupApiRoute(router, 'delete', '/:slug/pending/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.reject); + + setupApiRoute(router, 'get', '/:slug/invites', [...middlewares, middleware.assert.group], controllers.write.groups.getInvites); + // setupApiRoute(router, 'post', '/:slug/invites', [...middlewares, middleware.assert.group], controllers.write.groups.issueInvite); + // setupApiRoute(router, 'put', '/:slug/invites/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.acceptInvite); + // setupApiRoute(router, 'delete', '/:slug/invites/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.rejectInvite); + return router; }; diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 3b6f30a38d..2fc889996a 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -66,24 +66,6 @@ async function isInvited(socket, data) { } } -SocketGroups.accept = async (socket, data) => { - await isOwner(socket, data); - await groups.acceptMembership(data.groupName, data.toUid); - logGroupEvent(socket, 'group-accept-membership', { - groupName: data.groupName, - targetUid: data.toUid, - }); -}; - -SocketGroups.reject = async (socket, data) => { - await isOwner(socket, data); - await groups.rejectMembership(data.groupName, data.toUid); - logGroupEvent(socket, 'group-reject-membership', { - groupName: data.groupName, - targetUid: data.toUid, - }); -}; - SocketGroups.acceptAll = async (socket, data) => { await isOwner(socket, data); await acceptRejectAll(SocketGroups.accept, socket, data); @@ -98,7 +80,8 @@ async function acceptRejectAll(method, socket, data) { if (typeof data.groupName !== 'string') { throw new Error('[[error:invalid-group-name]]'); } - const uids = await groups.getPending(data.groupName); + const users = await groups.getPending(data.groupName); + const uids = users.map(u => u.uid); await Promise.all(uids.map(async (uid) => { await method(socket, { groupName: data.groupName, toUid: uid }); })); diff --git a/test/groups.js b/test/groups.js index f79b94b0c8..3731eec89a 100644 --- a/test/groups.js +++ b/test/groups.js @@ -944,7 +944,8 @@ describe('Groups', () => { ]); await requestMembership(uid1, uid2); await socketGroups.rejectAll({ uid: adminUid }, { groupName: 'PrivateCanJoin' }); - const pending = await Groups.getPending('PrivateCanJoin'); + let pending = await Groups.getPending('PrivateCanJoin'); + pending = pending.map(u => u.uid); assert.equal(pending.length, 0); await requestMembership(uid1, uid2); await socketGroups.acceptAll({ uid: adminUid }, { groupName: 'PrivateCanJoin' });