From eda817ac9586ccde9268beb456b6e0890b0d4c6f Mon Sep 17 00:00:00 2001 From: Andrew Bromwich Date: Sat, 21 Aug 2021 03:35:33 +1000 Subject: [PATCH] [NEW] REST endpoint to delete a DM and allow DM for two other users (#18022) --- app/api/server/v1/im.js | 24 +++- app/authorization/server/startup.js | 2 +- ee/server/services/.eslintrc.js | 2 +- server/methods/createDirectMessage.js | 120 ++++++++++-------- tests/data/rooms.helper.js | 20 +-- tests/end-to-end/api/04-direct-message.js | 144 +++++++++++++++++++++- 6 files changed, 245 insertions(+), 67 deletions(-) diff --git a/app/api/server/v1/im.js b/app/api/server/v1/im.js index 233f0320d8a..a6780c2ce05 100644 --- a/app/api/server/v1/im.js +++ b/app/api/server/v1/im.js @@ -7,8 +7,9 @@ import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMes import { settings } from '../../../settings/server'; import { API } from '../api'; import { getDirectMessageByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getDirectMessageByNameOrIdWithOptionToJoin'; +import { createDirectMessage } from '../../../../server/methods/createDirectMessage'; -function findDirectMessageRoom(params, user) { +function findDirectMessageRoom(params, user, allowAdminOverride) { if ((!params.roomId || !params.roomId.trim()) && (!params.username || !params.username.trim())) { throw new Meteor.Error('error-room-param-not-provided', 'Body param "roomId" or "username" is required'); } @@ -18,7 +19,8 @@ function findDirectMessageRoom(params, user) { nameOrId: params.username || params.roomId, }); - const canAccess = Meteor.call('canAccessRoom', room._id, user._id); + const canAccess = Meteor.call('canAccessRoom', room._id, user._id) + || (allowAdminOverride && hasPermission(user._id, 'view-room-administration')); if (!canAccess || !room || room.t !== 'd') { throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "username" param provided does not match any direct message'); } @@ -33,7 +35,7 @@ function findDirectMessageRoom(params, user) { API.v1.addRoute(['dm.create', 'im.create'], { authRequired: true }, { post() { - const { username, usernames } = this.requestParams(); + const { username, usernames, excludeSelf } = this.requestParams(); const users = username ? [username] : usernames && usernames.split(',').map((username) => username.trim()); @@ -41,7 +43,7 @@ API.v1.addRoute(['dm.create', 'im.create'], { authRequired: true }, { throw new Meteor.Error('error-room-not-found', 'The required "username" or "usernames" param provided does not match any direct message'); } - const room = Meteor.call('createDirectMessage', ...users); + const room = createDirectMessage(users, excludeSelf); return API.v1.success({ room: { ...room, _id: room.rid }, @@ -49,6 +51,20 @@ API.v1.addRoute(['dm.create', 'im.create'], { authRequired: true }, { }, }); +API.v1.addRoute(['dm.delete', 'im.delete'], { authRequired: true }, { + post() { + if (!hasPermission(this.userId, 'view-room-administration')) { + return API.v1.unauthorized(); + } + + const findResult = findDirectMessageRoom(this.requestParams(), this.user, true); + + Meteor.call('eraseRoom', findResult.room._id); + + return API.v1.success(); + }, +}); + API.v1.addRoute(['dm.close', 'im.close'], { authRequired: true }, { post() { const findResult = findDirectMessageRoom(this.requestParams(), this.user); diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js index bcfa53e52f4..b2d1c41cc96 100644 --- a/app/authorization/server/startup.js +++ b/app/authorization/server/startup.js @@ -9,7 +9,7 @@ Meteor.startup(function() { // Note: // 1.if we need to create a role that can only edit channel message, but not edit group message // then we can define edit--message instead of edit-message - // 2. admin, moderator, and user roles should not be deleted as they are referened in the code. + // 2. admin, moderator, and user roles should not be deleted as they are referenced in the code. const permissions = [ { _id: 'access-permissions', roles: ['admin'] }, { _id: 'access-setting-permissions', roles: ['admin'] }, diff --git a/ee/server/services/.eslintrc.js b/ee/server/services/.eslintrc.js index ab065ac6f44..6e2518fbdbd 100644 --- a/ee/server/services/.eslintrc.js +++ b/ee/server/services/.eslintrc.js @@ -3,5 +3,5 @@ module.exports = { extends: '../../../.eslintrc', rules: { 'import/no-unresolved': 'off', - } + }, }; diff --git a/server/methods/createDirectMessage.js b/server/methods/createDirectMessage.js index f5a0a080eb5..160643a1088 100644 --- a/server/methods/createDirectMessage.js +++ b/server/methods/createDirectMessage.js @@ -1,5 +1,5 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; +import { check, Match } from 'meteor/check'; import { settings } from '../../app/settings'; import { hasPermission } from '../../app/authorization'; @@ -8,74 +8,90 @@ import { RateLimiter } from '../../app/lib'; import { addUser } from '../../app/federation/server/functions/addUser'; import { createRoom } from '../../app/lib/server'; -Meteor.methods({ - createDirectMessage(...usernames) { - check(usernames, [String]); +export function createDirectMessage(usernames, excludeSelf) { + check(usernames, [String]); + check(excludeSelf, Match.Optional(Boolean)); - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'createDirectMessage', - }); - } + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'createDirectMessage', + }); + } - const me = Meteor.user(); + const me = Meteor.user(); - if (!me.username) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'createDirectMessage', - }); + if (!me.username) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'createDirectMessage', + }); + } + + if (settings.get('Message_AllowDirectMessagesToYourself') === false && usernames.length === 1 && me.username === usernames[0]) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'createDirectMessage', + }); + } + + const users = usernames.filter((username) => username !== me.username).map((username) => { + let to = Users.findOneByUsernameIgnoringCase(username); + + // If the username does have an `@`, but does not exist locally, we create it first + if (!to && username.indexOf('@') !== -1) { + to = addUser(username); } - if (settings.get('Message_AllowDirectMessagesToYourself') === false && usernames.length === 1 && me.username === usernames[0]) { + if (!to) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createDirectMessage', }); } + return to; + }); - const users = usernames.filter((username) => username !== me.username).map((username) => { - let to = Users.findOneByUsernameIgnoringCase(username); - - // If the username does have an `@`, but does not exist locally, we create it first - if (!to && username.indexOf('@') !== -1) { - to = addUser(username); - } + const roomUsers = excludeSelf ? users : [me, ...users]; - if (!to) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'createDirectMessage', - }); - } - return to; + if (roomUsers.length === 1) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'createDirectMessage', }); + } - if (!hasPermission(Meteor.userId(), 'create-d')) { - // If the user can't create DMs but can access already existing ones - if (hasPermission(Meteor.userId(), 'view-d-room')) { - // Check if the direct room already exists, then return it - - const uids = [me, ...users].map(({ _id }) => _id).sort(); - const room = Rooms.findOneDirectRoomContainingAllUserIDs(uids, { fields: { _id: 1 } }); - if (room) { - return { - t: 'd', - rid: room._id, - ...room, - }; - } + if (!hasPermission(Meteor.userId(), 'create-d')) { + // If the user can't create DMs but can access already existing ones + if (hasPermission(Meteor.userId(), 'view-d-room')) { + // Check if the direct room already exists, then return it + const uids = roomUsers.map(({ _id }) => _id).sort(); + const room = Rooms.findOneDirectRoomContainingAllUserIDs(uids, { fields: { _id: 1 } }); + if (room) { + return { + t: 'd', + rid: room._id, + ...room, + }; } - - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'createDirectMessage', - }); } - const { _id: rid, inserted, ...room } = createRoom('d', null, null, [me, ...users], null, { }, { creator: me._id }); + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'createDirectMessage', + }); + } + + const options = { creator: me._id }; + if (excludeSelf && hasPermission(this.userId, 'view-room-administration')) { + options.subscriptionExtra = { open: true }; + } + const { _id: rid, inserted, ...room } = createRoom('d', null, null, roomUsers, null, { }, options); - return { - t: 'd', - rid, - ...room, - }; + return { + t: 'd', + rid, + ...room, + }; +} + +Meteor.methods({ + createDirectMessage(...usernames) { + return createDirectMessage(usernames, false); }, }); diff --git a/tests/data/rooms.helper.js b/tests/data/rooms.helper.js index 868ddee0527..f2a5439170e 100644 --- a/tests/data/rooms.helper.js +++ b/tests/data/rooms.helper.js @@ -29,24 +29,28 @@ export const asyncCreateRoom = ({ name, type, username, members = [] }) => new P .end(resolve); }); -export const closeRoom = ({ type, roomId }) => { +function actionRoom({ action, type, roomId }) { if (!type) { - throw new Error('"type" is required in "closeRoom" test helper'); + throw new Error(`"type" is required in "${ action }Room" test helper`); } if (!roomId) { - throw new Error('"roomId" is required in "closeRoom" test helper'); + throw new Error(`"roomId" is required in "${ action }Room" test helper`); } const endpoints = { - c: 'channels.close', - p: 'groups.close', - d: 'im.close', + c: 'channels', + p: 'groups', + d: 'im', }; return new Promise((resolve) => { - request.post(api(endpoints[type])) + request.post(api(`${ endpoints[type] }.${ action }`)) .set(credentials) .send({ roomId, }) .end(resolve); }); -}; +} + +export const deleteRoom = ({ type, roomId }) => actionRoom({ action: 'delete', type, roomId }); + +export const closeRoom = ({ type, roomId }) => actionRoom({ action: 'close', type, roomId }); diff --git a/tests/end-to-end/api/04-direct-message.js b/tests/end-to-end/api/04-direct-message.js index 1e58ff6b428..416b412c4f2 100644 --- a/tests/end-to-end/api/04-direct-message.js +++ b/tests/end-to-end/api/04-direct-message.js @@ -10,6 +10,8 @@ import { apiEmail, } from '../../data/api-data.js'; import { password, adminUsername } from '../../data/user.js'; +import { deleteRoom } from '../../data/rooms.helper'; +import { createUser, deleteUser, login } from '../../data/users.helper'; import { updateSetting, updatePermission } from '../../data/permissions.helper'; @@ -209,7 +211,6 @@ describe('[Direct Messages]', function() { .set(credentials) .send({ roomId: directMessage._id, - userId: 'rocket.cat', }) .expect('Content-Type', 'application/json') .expect(200) @@ -423,6 +424,7 @@ describe('[Direct Messages]', function() { .end(done); }); }); + describe('/im.members', () => { it('should return and array with two members', (done) => { request.get(api('im.members')) @@ -477,4 +479,144 @@ describe('[Direct Messages]', function() { .end(done); }); }); + + describe('/im.create', () => { + let otherUser; + let roomId; + + before(async () => { + otherUser = await createUser(); + }); + + after(async () => { + await deleteRoom({ type: 'd', roomId }); + await deleteUser(otherUser); + otherUser = undefined; + }); + + it('creates a DM between two other parties (including self)', (done) => { + request.post(api('im.create')) + .set(credentials) + .send({ + usernames: ['rocket.cat', otherUser.username].join(','), + }) + .expect(200) + .expect('Content-Type', 'application/json') + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('room').and.to.be.an('object'); + expect(res.body.room).to.have.property('usernames').and.to.have.members([adminUsername, 'rocket.cat', otherUser.username]); + roomId = res.body.room._id; + }) + .end(done); + }); + + it('creates a DM between two other parties (excluding self)', (done) => { + request.post(api('im.create')) + .set(credentials) + .send({ + usernames: ['rocket.cat', otherUser.username].join(','), + excludeSelf: true, + }) + .expect(200) + .expect('Content-Type', 'application/json') + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('room').and.to.be.an('object'); + expect(res.body.room).to.have.property('usernames').and.to.have.members(['rocket.cat', otherUser.username]); + roomId = res.body.room._id; + }) + .end(done); + }); + }); + + describe('/im.delete', () => { + let testDM; + + it('/im.create', (done) => { + request.post(api('im.create')) + .set(credentials) + .send({ + username: 'rocket.cat', + }) + .expect(200) + .expect('Content-Type', 'application/json') + .expect((res) => { + testDM = res.body.room; + }) + .end(done); + }); + + it('/im.delete', (done) => { + request.post(api('im.delete')) + .set(credentials) + .send({ + username: 'rocket.cat', + }) + .expect(200) + .expect('Content-Type', 'application/json') + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + + it('/im.open', (done) => { + request.post(api('im.open')) + .set(credentials) + .send({ + roomId: testDM._id, + }) + .expect(400) + .expect('Content-Type', 'application/json') + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'invalid-channel'); + }) + .end(done); + }); + + context('when authenticated as a non-admin user', () => { + let otherUser; + let otherCredentials; + + before(async () => { + otherUser = await createUser(); + otherCredentials = await login(otherUser.username, password); + }); + + after(async () => { + await deleteUser(otherUser); + otherUser = undefined; + }); + + it('/im.create', (done) => { + request.post(api('im.create')) + .set(credentials) + .send({ + username: otherUser.username, + }) + .expect(200) + .expect('Content-Type', 'application/json') + .expect((res) => { + testDM = res.body.room; + }) + .end(done); + }); + + it('/im.delete', (done) => { + request.post(api('im.delete')) + .set(otherCredentials) + .send({ + roomId: testDM._id, + }) + .expect(403) + .expect('Content-Type', 'application/json') + .expect((res) => { + expect(res.body).to.have.property('success', false); + }) + .end(done); + }); + }); + }); });