Merge remote-tracking branch 'origin/develop' into release-candidate

pull/21842/head
Diego Sampaio 4 years ago
commit 3e7b441d9f
No known key found for this signature in database
GPG Key ID: E060152B30502562
  1. 4
      .github/workflows/build_and_test.yml
  2. 23
      app/lib/server/functions/setUserActiveStatus.js
  3. 12
      app/models/server/models/Rooms.js
  4. 9
      app/models/server/models/Users.js
  5. 4
      app/models/server/raw/Users.js
  6. 2
      ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js
  7. 12
      ee/app/livechat-enterprise/server/lib/Helper.js
  8. 42
      package-lock.json
  9. 2
      package.json
  10. 1
      packages/rocketchat-i18n/i18n/en.i18n.json
  11. 252
      tests/end-to-end/api/24-methods.js

@ -61,7 +61,9 @@ jobs:
id: cache-nodemodules
uses: actions/cache@v2
with:
path: node_modules
path: |
./node_modules
./ee/server/services/node_modules
key: ${{ runner.OS }}-node_modules-${{ hashFiles('**/package-lock.json', '.github/workflows/build_and_test.yml') }}
- name: Cache meteor local

@ -9,6 +9,25 @@ import { relinquishRoomOwnerships } from './relinquishRoomOwnerships';
import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner';
import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms';
function reactivateDirectConversations(userId) {
// since both users can be deactivated at the same time, we should just reactivate rooms if both users are active
// for that, we need to fetch the direct messages, fetch the users involved and then the ids of rooms we can reactivate
const directConversations = Rooms.getDirectConversationsByUserId(userId, { projection: { _id: 1, uids: 1 } }).fetch();
const userIds = directConversations.reduce((acc, r) => acc.push(...r.uids) && acc, []);
const uniqueUserIds = [...new Set(userIds)];
const activeUsers = Users.findActiveByUserIds(uniqueUserIds, { projection: { _id: 1 } }).fetch();
const activeUserIds = activeUsers.map((u) => u._id);
const roomsToReactivate = directConversations.reduce((acc, room) => {
const otherUserId = room.uids.find((u) => u !== userId);
if (activeUserIds.includes(otherUserId)) {
acc.push(room._id);
}
return acc;
}, []);
Rooms.setDmReadOnlyByUserId(userId, roomsToReactivate, false, false);
}
export function setUserActiveStatus(userId, active, confirmRelinquish = false) {
check(userId, String);
check(active, Boolean);
@ -39,10 +58,10 @@ export function setUserActiveStatus(userId, active, confirmRelinquish = false) {
if (active === false) {
Users.unsetLoginTokens(userId);
Rooms.setReadOnlyByUserId(userId, true, false);
Rooms.setDmReadOnlyByUserId(userId, undefined, true, false);
} else {
Users.unsetReason(userId);
Rooms.setReadOnlyByUserId(userId, false, false);
reactivateDirectConversations(userId);
}
if (active && !settings.get('Accounts_Send_Email_When_Activating')) {
return true;

@ -229,9 +229,13 @@ export class Rooms extends Base {
return this.update(query, update);
}
setReadOnlyByUserId(_id, readOnly, reactWhenReadOnly) {
setDmReadOnlyByUserId(_id, ids, readOnly, reactWhenReadOnly) {
const query = {
uids: _id,
uids: {
$size: 2,
$in: [_id],
},
...ids && Array.isArray(ids) ? { _id: { $in: ids } } : {},
t: 'd',
};
@ -245,6 +249,10 @@ export class Rooms extends Base {
return this.update(query, update, { multi: true });
}
getDirectConversationsByUserId(_id, options) {
return this.find({ t: 'd', uids: { $size: 2, $in: [_id] } }, options);
}
setAllowReactingWhenReadOnlyById = function(_id, allowReacting) {
const query = {
_id,

@ -735,6 +735,15 @@ export class Users extends Base {
}, options);
}
findActiveByUserIds(ids, options = {}) {
return this.find({
active: true,
type: { $nin: ['app'] },
roles: { $ne: ['guest'] },
_id: { $in: ids },
}, options);
}
findActiveLocalGuests(idExceptions = [], options = {}) {
const query = {
active: true,

@ -196,11 +196,11 @@ export class UsersRaw extends BaseRaw {
return result.value;
}
async getAgentAndAmountOngoingChats(userId) {
async getAgentAndAmountOngoingChats(userId, department) {
const aggregate = [
{ $match: { _id: userId, status: { $exists: true, $ne: 'offline' }, statusLivechat: 'available', roles: 'livechat-agent' } },
{ $lookup: { from: 'rocketchat_subscription', localField: '_id', foreignField: 'u._id', as: 'subs' } },
{ $project: { agentId: '$_id', username: 1, lastAssignTime: 1, lastRoutingTime: 1, 'queueInfo.chats': { $size: { $filter: { input: '$subs', as: 'sub', cond: { $and: [{ $eq: ['$$sub.t', 'l'] }, { $eq: ['$$sub.open', true] }, { $ne: ['$$sub.onHold', true] }] } } } } } },
{ $project: { agentId: '$_id', username: 1, lastAssignTime: 1, lastRoutingTime: 1, 'queueInfo.chats': { $size: { $filter: { input: '$subs', as: 'sub', cond: { $and: [{ $eq: ['$$sub.t', 'l'] }, { $eq: ['$$sub.open', true] }, { $ne: ['$$sub.onHold', true] }, { ...department && { $eq: ['$$sub.department', department] } }] } } } } } },
{ $sort: { 'queueInfo.chats': 1, lastAssignTime: 1, lastRoutingTime: 1, username: 1 } },
];

@ -27,7 +27,7 @@ callbacks.add('livechat.checkAgentBeforeTakeInquiry', async ({ agent, inquiry, o
return agent;
}
const user = await Users.getAgentAndAmountOngoingChats(agentId);
const user = await Users.getAgentAndAmountOngoingChats(agentId, departmentId);
if (!user) {
return null;
}

@ -16,17 +16,17 @@ import { dispatchAgentDelegated } from '../../../../../app/livechat/server/lib/H
import notifications from '../../../../../app/notifications/server/lib/Notifications';
export const getMaxNumberSimultaneousChat = ({ agentId, departmentId }) => {
if (agentId) {
const user = Users.getAgentInfo(agentId);
const { livechat: { maxNumberSimultaneousChat } = {} } = user || {};
if (departmentId) {
const department = LivechatDepartment.findOneById(departmentId);
const { maxNumberSimultaneousChat } = department || {};
if (maxNumberSimultaneousChat > 0) {
return maxNumberSimultaneousChat;
}
}
if (departmentId) {
const department = LivechatDepartment.findOneById(departmentId);
const { maxNumberSimultaneousChat } = department || {};
if (agentId) {
const user = Users.getAgentInfo(agentId);
const { livechat: { maxNumberSimultaneousChat } = {} } = user || {};
if (maxNumberSimultaneousChat > 0) {
return maxNumberSimultaneousChat;
}

42
package-lock.json generated

@ -6272,9 +6272,9 @@
}
},
"@rocket.chat/apps-engine": {
"version": "1.25.0-alpha.4943",
"resolved": "https://registry.npmjs.org/@rocket.chat/apps-engine/-/apps-engine-1.25.0-alpha.4943.tgz",
"integrity": "sha512-a1dUxhlQ5Ab86S7lcN+ieU+XdduFA9KIW5fzOVifoEiZ75CzCGPweK076e2Qz/KW/uo11gyaM6wOQoQxy8h63Q==",
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/@rocket.chat/apps-engine/-/apps-engine-1.25.0.tgz",
"integrity": "sha512-Dd66C0JbaXwzVL8JxeK3q1v0DcGyWiuSJNwmSkqkwba5arh7XLSP9WdFbwlGI8ckBbXzFCpmow5LSc9tDYAGBg==",
"requires": {
"adm-zip": "^0.4.9",
"cryptiles": "^4.1.3",
@ -22943,7 +22943,7 @@
},
"chownr": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
"dev": true,
"optional": true
@ -22978,7 +22978,7 @@
},
"debug": {
"version": "4.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"optional": true,
@ -23009,7 +23009,7 @@
},
"fs-minipass": {
"version": "1.2.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
"dev": true,
"optional": true,
@ -23043,7 +23043,7 @@
},
"glob": {
"version": "7.1.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"dev": true,
"optional": true,
@ -23075,7 +23075,7 @@
},
"ignore-walk": {
"version": "3.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
"dev": true,
"optional": true,
@ -23096,7 +23096,7 @@
},
"inherits": {
"version": "2.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true,
"optional": true
@ -23144,7 +23144,7 @@
},
"minipass": {
"version": "2.3.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"dev": true,
"optional": true,
@ -23155,7 +23155,7 @@
},
"minizlib": {
"version": "1.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
"dev": true,
"optional": true,
@ -23175,7 +23175,7 @@
},
"ms": {
"version": "2.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true,
"optional": true
@ -23189,7 +23189,7 @@
},
"needle": {
"version": "2.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==",
"dev": true,
"optional": true,
@ -23201,7 +23201,7 @@
},
"node-pre-gyp": {
"version": "0.12.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
"dev": true,
"optional": true,
@ -23231,14 +23231,14 @@
},
"npm-bundled": {
"version": "1.0.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==",
"dev": true,
"optional": true
},
"npm-packlist": {
"version": "1.4.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
"dev": true,
"optional": true,
@ -23318,7 +23318,7 @@
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true,
"optional": true
@ -23363,7 +23363,7 @@
},
"rimraf": {
"version": "2.6.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"dev": true,
"optional": true,
@ -23394,7 +23394,7 @@
},
"semver": {
"version": "5.7.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"dev": true,
"optional": true
@ -23454,7 +23454,7 @@
},
"tar": {
"version": "4.4.8",
"resolved": false,
"resolved": "",
"integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
"dev": true,
"optional": true,
@ -23494,7 +23494,7 @@
},
"yallist": {
"version": "3.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"dev": true,
"optional": true

@ -140,7 +140,7 @@
"@nivo/heatmap": "^0.61.0",
"@nivo/line": "^0.61.1",
"@nivo/pie": "^0.61.1",
"@rocket.chat/apps-engine": "1.25.0-alpha.4943",
"@rocket.chat/apps-engine": "1.25.0",
"@rocket.chat/css-in-js": "^0.6.3-dev.234",
"@rocket.chat/emitter": "^0.6.3-dev.234",
"@rocket.chat/fuselage": "^0.6.3-dev.236",

@ -489,6 +489,7 @@
"Apps_Permissions_livechat-room_read": "Access Livechat room information",
"Apps_Permissions_livechat-room_write": "Modify Livechat room information",
"Apps_Permissions_livechat-department_read": "Access Livechat department information",
"Apps_Permissions_livechat-department_multiple": "Access to multiple Livechat departments information",
"Apps_Permissions_livechat-department_write": "Modify Livechat department information",
"Apps_Permissions_slashcommand": "Register new slash commands",
"Apps_Permissions_api": "Register new HTTP endpoints",

@ -1479,4 +1479,256 @@ describe('Meteor.methods', function() {
});
});
});
describe('[@setUserActiveStatus]', () => {
let testUser;
let testUser2;
const testUserCredentials = {};
let dmId;
let dmTestId;
before('create test user', (done) => {
const username = `user.test.${ Date.now() }`;
const email = `${ username }@rocket.chat`;
request.post(api('users.create'))
.set(credentials)
.send({ email, name: username, username, password: username, roles: ['user'] })
.end((err, res) => {
testUser = res.body.user;
done();
});
});
before('create test user 2', (done) => {
const username = `user.test.${ Date.now() }`;
const email = `${ username }@rocket.chat`;
request.post(api('users.create'))
.set(credentials)
.send({ email, name: username, username, password: username, roles: ['user'] })
.end((err, res) => {
testUser2 = res.body.user;
done();
});
});
before('login testUser', (done) => {
request.post(api('login'))
.send({
user: testUser.username,
password: testUser.username,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
testUserCredentials['X-Auth-Token'] = res.body.data.authToken;
testUserCredentials['X-User-Id'] = res.body.data.userId;
})
.end(done);
});
before('create direct conversation with user', (done) => {
request.post(methodCall('createDirectMessage'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'createDirectMessage',
params: [testUser.username],
}),
})
.end((err, res) => {
const result = JSON.parse(res.body.message);
expect(result.result).to.be.an('object');
expect(result.result).to.have.property('rid').that.is.an('string');
dmId = result.result.rid;
done();
});
});
before('create direct conversation between both users', (done) => {
request.post(methodCall('createDirectMessage'))
.set(testUserCredentials)
.send({
message: JSON.stringify({
method: 'createDirectMessage',
params: [testUser2.username],
}),
})
.end((err, res) => {
const result = JSON.parse(res.body.message);
expect(result.result).to.be.an('object');
expect(result.result).to.have.property('rid').that.is.an('string');
dmTestId = result.result.rid;
done();
});
});
it('should deactivate a user', (done) => {
request.post(methodCall('setUserActiveStatus'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'setUserActiveStatus',
params: [testUser._id, false, false],
}),
})
.end((err, res) => {
expect(res.body).to.have.property('success').that.is.an('boolean');
const result = JSON.parse(res.body.message);
expect(result.result).to.be.equal(true);
done();
});
});
it('should deactivate another user', (done) => {
request.post(methodCall('setUserActiveStatus'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'setUserActiveStatus',
params: [testUser2._id, false, false],
}),
})
.end((err, res) => {
expect(res.body).to.have.property('success').that.is.an('boolean');
const result = JSON.parse(res.body.message);
expect(result.result).to.be.equal(true);
done();
});
});
it('should mark the direct conversation between admin=>testUser as readonly when user is deactivated', (done) => {
request.post(methodCall('getRoomByTypeAndName'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'getRoomByTypeAndName',
params: ['d', dmId],
}),
})
.end((err, res) => {
expect(res.body.success).to.equal(true);
const result = JSON.parse(res.body.message);
expect(result.result.ro).to.equal(true);
done();
});
});
it('should activate a user', (done) => {
request.post(methodCall('setUserActiveStatus'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'setUserActiveStatus',
params: [testUser._id, true, false],
}),
})
.end((err, res) => {
expect(res.body).to.have.property('success').that.is.an('boolean');
const result = JSON.parse(res.body.message);
expect(result.result).to.be.equal(true);
done();
});
});
it('should set readonly=false when user is activated (and the other side is also active)', (done) => {
request.post(methodCall('getRoomByTypeAndName'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'getRoomByTypeAndName',
params: ['d', dmId],
}),
})
.end((err, res) => {
expect(res.body.success).to.equal(true);
const result = JSON.parse(res.body.message);
expect(result.result.ro).to.equal(false);
done();
});
});
it('should keep the direct conversation between testUser=>testUser2 as readonly when one of them is deactivated', (done) => {
request.post(api('login'))
.send({
user: testUser.username,
password: testUser.username,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
testUserCredentials['X-Auth-Token'] = res.body.data.authToken;
testUserCredentials['X-User-Id'] = res.body.data.userId;
})
.then(() => {
request.post(methodCall('getRoomByTypeAndName'))
.set(testUserCredentials)
.send({
message: JSON.stringify({
method: 'getRoomByTypeAndName',
params: ['d', dmTestId],
}),
})
.end((err, res) => {
expect(res.body.success).to.equal(true);
const result = JSON.parse(res.body.message);
expect(result.result.ro).to.equal(true);
done();
});
})
.catch(done);
});
it('should activate another user', (done) => {
request.post(methodCall('setUserActiveStatus'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'setUserActiveStatus',
params: [testUser2._id, true, false],
}),
})
.end((err, res) => {
expect(res.body).to.have.property('success').that.is.an('boolean');
const result = JSON.parse(res.body.message);
expect(result.result).to.be.equal(true);
done();
});
});
it('should set readonly=false when both users are activated', (done) => {
request.post(methodCall('getRoomByTypeAndName'))
.set(testUserCredentials)
.send({
message: JSON.stringify({
method: 'getRoomByTypeAndName',
params: ['d', dmTestId],
}),
})
.end((err, res) => {
expect(res.body.success).to.equal(true);
const result = JSON.parse(res.body.message);
expect(result.result.ro).to.equal(false);
done();
});
});
it('should keep readonly=true when user is activated (and the other side is deactivated)', (done) => {
request.post(methodCall('getRoomByTypeAndName'))
.set(testUserCredentials)
.send({
message: JSON.stringify({
method: 'getRoomByTypeAndName',
params: ['d', dmTestId],
}),
})
.end((err, res) => {
expect(res.body.success).to.equal(true);
const result = JSON.parse(res.body.message);
expect(result.result.ro).to.equal(false);
done();
});
});
});
});

Loading…
Cancel
Save