From 6eea6451d2c5d4b329319c4d59082acaac3b6e1f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 20 Dec 2021 17:04:16 -0500 Subject: [PATCH] feat: `GET /chats/:roomId/users` [breaking] --- public/openapi/write.yaml | 2 + public/openapi/write/chats/roomId/users.yaml | 61 ++++++++++++++++++++ public/src/client/chats.js | 19 +++--- src/api/chats.js | 11 ++++ src/controllers/write/chats.js | 5 +- src/routes/write/chats.js | 2 +- src/socket.io/modules.js | 14 ++--- test/messaging.js | 19 +++--- 8 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 public/openapi/write/chats/roomId/users.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index d7a7a55d55..6b55e90783 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -140,6 +140,8 @@ paths: $ref: 'write/chats.yaml' /chats/{roomId}: $ref: 'write/chats/roomId.yaml' + /chats/{roomId}/users: + $ref: 'write/chats/roomId/users.yaml' /chats/{roomId}/{mid}: $ref: 'write/chats/roomId/mid.yaml' /flags/: diff --git a/public/openapi/write/chats/roomId/users.yaml b/public/openapi/write/chats/roomId/users.yaml new file mode 100644 index 0000000000..ecf8a7cf89 --- /dev/null +++ b/public/openapi/write/chats/roomId/users.yaml @@ -0,0 +1,61 @@ +get: + tags: + - chats + summary: get chat room users + description: This operation retrieves the users in a chat room message + parameters: + - in: path + name: roomId + schema: + type: number + required: true + description: a valid chat room id + example: 1 + responses: + '200': + description: Users successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + users: + type: array + items: + type: object + properties: + uid: + type: number + description: A user identifier + username: + type: string + description: A friendly name for a given user account + picture: + nullable: true + type: string + status: + type: string + displayname: + type: string + description: This is either username or fullname depending on forum and user settings + icon:text: + type: string + description: A single-letter representation of a username. This is used in the + auto-generated icon given to users + without an avatar + icon:bgColor: + type: string + description: A six-character hexadecimal colour code assigned to the user. This + value is used in conjunction with + `icon:text` for the user's + auto-generated icon + example: "#f44336" + isOwner: + type: boolean + canKick: + type: boolean \ No newline at end of file diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 1a6f1d638f..a98e368fa7 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -308,20 +308,15 @@ define('forum/chats', [ }; Chats.refreshParticipantsList = function (roomId, modal) { - socket.emit('modules.chats.getUsersInRoom', { roomId: roomId }, function (err, users) { - const listEl = modal.find('.list-group'); - - if (err) { - return translator.translate('[[error:invalid-data]]', function (translated) { - listEl.find('li').text(translated); - }); - } - - app.parseAndTranslate('partials/modals/manage_room_users', { - users: users, - }, function (html) { + const listEl = modal.find('.list-group'); + api.get(`/chats/${roomId}/users`, {}).then(({ users }) => { + app.parseAndTranslate('partials/modals/manage_room_users', { users }, function (html) { listEl.html(html); }); + }).catch(() => { + translator.translate('[[error:invalid-data]]', function (translated) { + listEl.find('li').text(translated); + }); }); }; diff --git a/src/api/chats.js b/src/api/chats.js index c71f272b1a..cabe54480e 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -72,3 +72,14 @@ chatsAPI.rename = async (caller, data) => { roomId: data.roomId, }); }; + +chatsAPI.users = async (caller, data) => { + const [isOwner, users] = await Promise.all([ + messaging.isRoomOwner(caller.uid, data.roomId), + messaging.getUsersInRoom(data.roomId, 0, -1), + ]); + users.forEach((user) => { + user.canKick = (parseInt(user.uid, 10) !== parseInt(caller.uid, 10)) && isOwner; + }); + return { users }; +}; diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index 076fd9be3a..752d950db4 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -54,7 +54,10 @@ Chats.rename = async (req, res) => { }; Chats.users = async (req, res) => { - // ... + const users = await api.chats.users(req, { + ...req.params, + }); + helpers.formatApiResponse(200, res, users); }; Chats.invite = async (req, res) => { diff --git a/src/routes/write/chats.js b/src/routes/write/chats.js index 597a7829bf..65b65813b3 100644 --- a/src/routes/write/chats.js +++ b/src/routes/write/chats.js @@ -19,7 +19,7 @@ module.exports = function () { setupApiRoute(router, 'put', '/:roomId', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['name'])], controllers.write.chats.rename); // no route for room deletion, noted here just in case... - // setupApiRoute(router, 'get', '/:roomId/users', [...middlewares, middleware.assert.room], controllers.write.chats.users); + setupApiRoute(router, 'get', '/:roomId/users', [...middlewares, middleware.assert.room], controllers.write.chats.users); // setupApiRoute(router, 'put', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.invite); // setupApiRoute(router, 'delete', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.kick); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 5d62ce566f..11fb2c7913 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -84,21 +84,17 @@ SocketModules.chats.loadRoom = async function (socket, data) { }; SocketModules.chats.getUsersInRoom = async function (socket, data) { + sockets.warnDeprecated(socket, 'GET /api/v3/chats/:roomId/user'); + if (!data || !data.roomId) { throw new Error('[[error:invalid-data]]'); } - const [isUserInRoom, isOwner, userData] = await Promise.all([ - Messaging.isUserInRoom(socket.uid, data.roomId), - Messaging.isRoomOwner(socket.uid, data.roomId), - Messaging.getUsersInRoom(data.roomId, 0, -1), - ]); + const isUserInRoom = await Messaging.isUserInRoom(socket.uid, data.roomId); if (!isUserInRoom) { throw new Error('[[error:no-privileges]]'); } - userData.forEach((user) => { - user.canKick = (parseInt(user.uid, 10) !== parseInt(socket.uid, 10)) && isOwner; - }); - return userData; + + return api.chats.users(socket, data); }; SocketModules.chats.addUserToRoom = async function (socket, data) { diff --git a/test/messaging.js b/test/messaging.js index ce03a27dfa..9cd1a55643 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -22,6 +22,7 @@ describe('Messaging Library', () => { const mocks = { users: { foo: {}, // the admin + bar: {}, baz: {}, // the user with chat restriction enabled herp: {}, }, @@ -53,10 +54,12 @@ describe('Messaging Library', () => { // Create 3 users: 1 admin, 2 regular ({ foo: mocks.users.foo.uid, + bar: mocks.users.bar.uid, baz: mocks.users.baz.uid, herp: mocks.users.herp.uid, } = await utils.promiseParallel({ foo: User.create({ username: 'foo', password: 'barbar' }), // admin + bar: User.create({ username: 'bar', password: 'bazbaz' }), // admin baz: User.create({ username: 'baz', password: 'quuxquux' }), // restricted user herp: User.create({ username: 'herp', password: 'derpderp' }), // a regular user })); @@ -65,6 +68,7 @@ describe('Messaging Library', () => { await User.setSetting(mocks.users.baz.uid, 'restrictChat', '1'); ({ jar: mocks.users.foo.jar, csrf_token: mocks.users.foo.csrf } = await util.promisify(helpers.loginUser)('foo', 'barbar')); + ({ jar: mocks.users.bar.jar, csrf_token: mocks.users.bar.csrf } = await util.promisify(helpers.loginUser)('bar', 'bazbaz')); ({ jar: mocks.users.baz.jar, csrf_token: mocks.users.baz.csrf } = await util.promisify(helpers.loginUser)('baz', 'quuxquux')); ({ jar: mocks.users.herp.jar, csrf_token: mocks.users.herp.csrf } = await util.promisify(helpers.loginUser)('herp', 'derpderp')); @@ -190,17 +194,16 @@ describe('Messaging Library', () => { }); it('should get users in room', async () => { - const data = await socketModules.chats.getUsersInRoom({ uid: mocks.users.foo.uid }, { roomId: roomId }); - assert(Array.isArray(data)); - assert.strictEqual(data.length, 3); + const { body } = await callv3API('get', `/chats/${roomId}/users`, {}, 'foo'); + assert(Array.isArray(body.response.users)); + assert.strictEqual(body.response.users.length, 3); + console.log(body.response.users); }); it('should throw error if user is not in room', async () => { - try { - const data = await socketModules.chats.getUsersInRoom({ uid: 123123123 }, { roomId: roomId }); - } catch (err) { - assert.equal(err.message, '[[error:no-privileges]]'); - } + const { statusCode, body } = await callv3API('get', `/chats/${roomId}/users`, {}, 'bar'); + assert.strictEqual(statusCode, 403); + assert.equal(body.status.message, await translator.translate('[[error:no-privileges]]')); }); it('should fail to add users to room if max is reached', (done) => {