diff --git a/back/methods/chat/send.js b/back/methods/chat/send.js index fcb49f4b8..c5c8feead 100644 --- a/back/methods/chat/send.js +++ b/back/methods/chat/send.js @@ -1,7 +1,6 @@ -const axios = require('axios'); module.exports = Self => { Self.remoteMethodCtx('send', { - description: 'Send a RocketChat message', + description: 'Creates a direct message in the chat model for a user or a channel', accessType: 'WRITE', accepts: [{ arg: 'to', @@ -31,39 +30,19 @@ module.exports = Self => { const recipient = to.replace('@', ''); if (sender.name != recipient) { - await sendMessage(sender, to, message); + await models.Chat.create({ + senderFk: sender.id, + recipient: to, + dated: new Date(), + checkUserStatus: 0, + message: message, + status: 0, + attempts: 0 + }); return true; } return false; }; - - async function sendMessage(sender, channel, message) { - if (process.env.NODE_ENV !== 'production') { - return new Promise(resolve => { - return resolve({ - statusCode: 200, - message: 'Fake notification sent' - }); - }); - } - - const login = await Self.getServiceAuth(); - const avatar = `${login.host}/avatar/${sender.name}`; - - const options = { - headers: { - 'X-Auth-Token': login.auth.token, - 'X-User-Id': login.auth.userId - }, - }; - - return axios.post(`${login.api}/chat.postMessage`, { - 'channel': channel, - 'avatar': avatar, - 'alias': sender.nickname, - 'text': message - }, options); - } }; diff --git a/back/methods/chat/sendCheckingPresence.js b/back/methods/chat/sendCheckingPresence.js index 2217aaee2..3bc022429 100644 --- a/back/methods/chat/sendCheckingPresence.js +++ b/back/methods/chat/sendCheckingPresence.js @@ -1,8 +1,6 @@ -const axios = require('axios'); - module.exports = Self => { Self.remoteMethodCtx('sendCheckingPresence', { - description: 'Sends a RocketChat message to a connected user or department channel', + description: 'Creates a message in the chat model checking the user status', accessType: 'WRITE', accepts: [{ arg: 'workerId', @@ -36,6 +34,7 @@ module.exports = Self => { const models = Self.app.models; const userId = ctx.req.accessToken.userId; + const sender = await models.Account.findById(userId); const recipient = await models.Account.findById(recipientId, null, myOptions); // Prevent sending messages to yourself @@ -44,54 +43,16 @@ module.exports = Self => { if (!recipient) throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`); - const {data} = await Self.getUserStatus(recipient.name); - if (data) { - if (data.status === 'offline' || data.status === 'busy') { - // Send message to department room - const workerDepartment = await models.WorkerDepartment.findById(recipientId, { - include: { - relation: 'department' - } - }, myOptions); - const department = workerDepartment && workerDepartment.department(); - const channelName = department && department.chatName; + await models.Chat.create({ + senderFk: sender.id, + recipient: `@${recipient.name}`, + dated: new Date(), + checkUserStatus: 1, + message: message, + status: 0, + attempts: 0 + }); - if (channelName) - return Self.send(ctx, `#${channelName}`, `@${recipient.name} ➔ ${message}`); - else - return Self.send(ctx, `@${recipient.name}`, message); - } else - return Self.send(ctx, `@${recipient.name}`, message); - } - }; - - /** - * Returns the current user status on Rocketchat - * - * @param {string} username - The recipient user name - * @return {Promise} - The request promise - */ - Self.getUserStatus = async function getUserStatus(username) { - if (process.env.NODE_ENV !== 'production') { - return new Promise(resolve => { - return resolve({ - data: { - status: 'online' - } - }); - }); - } - - const login = await Self.getServiceAuth(); - - const options = { - params: {username}, - headers: { - 'X-Auth-Token': login.auth.token, - 'X-User-Id': login.auth.userId - }, - }; - - return axios.get(`${login.api}/users.getStatus`, options); + return true; }; }; diff --git a/back/methods/chat/sendQueued.js b/back/methods/chat/sendQueued.js new file mode 100644 index 000000000..c34642c7e --- /dev/null +++ b/back/methods/chat/sendQueued.js @@ -0,0 +1,168 @@ +const axios = require('axios'); +module.exports = Self => { + Self.remoteMethodCtx('sendQueued', { + description: 'Send a RocketChat message', + accessType: 'WRITE', + accepts: [], + returns: { + type: 'object', + root: true + }, + http: { + path: `/sendQueued`, + verb: 'POST' + } + }); + + Self.sendQueued = async() => { + const models = Self.app.models; + const maxAttempts = 3; + const sentStatus = 1; + const errorStatus = 2; + + const chats = await models.Chat.find({ + where: { + status: {neq: sentStatus}, + attempts: {lt: maxAttempts} + } + }); + + for (let chat of chats) { + if (chat.checkUserStatus) { + try { + await Self.sendCheckingUserStatus(chat); + await updateChat(chat, sentStatus); + } catch (error) { + await updateChat(chat, errorStatus); + } + } else { + try { + await Self.sendMessage(chat.senderFk, chat.recipient, chat.message); + await updateChat(chat, sentStatus); + } catch (error) { + await updateChat(chat, errorStatus); + } + } + } + }; + + /** + * Check user status in Rocket + * + * @param {object} chat - The sender id + * @return {Promise} - The request promise + */ + Self.sendCheckingUserStatus = async function sendCheckingUserStatus(chat) { + const models = Self.app.models; + + const recipientName = chat.recipient.slice(1); + const recipient = await models.Account.findOne({ + where: { + name: recipientName + } + }); + + const {data} = await Self.getUserStatus(recipient.name); + if (data) { + if (data.status === 'offline' || data.status === 'busy') { + // Send message to department room + const workerDepartment = await models.WorkerDepartment.findById(recipient.id, { + include: { + relation: 'department' + } + }); + const department = workerDepartment && workerDepartment.department(); + const channelName = department && department.chatName; + + if (channelName) + return Self.sendMessage(chat.senderFk, `#${channelName}`, `@${recipient.name} ➔ ${message}`); + else + return Self.sendMessage(chat.senderFk, `@${recipient.name}`, chat.message); + } else + return Self.sendMessage(chat.senderFk, `@${recipient.name}`, chat.message); + } + }; + + /** + * Send a rocket message + * + * @param {object} senderFk - The sender id + * @param {string} recipient - The user (@) or channel (#) to send the message + * @param {string} message - The message to send + * @return {Promise} - The request promise + */ + Self.sendMessage = async function sendMessage(senderFk, recipient, message) { + if (process.env.NODE_ENV !== 'production') { + return new Promise(resolve => { + return resolve({ + statusCode: 200, + message: 'Fake notification sent' + }); + }); + } + + const models = Self.app.models; + const sender = await models.Account.findById(senderFk); + + const login = await Self.getServiceAuth(); + const avatar = `${login.host}/avatar/${sender.name}`; + + const options = { + headers: { + 'X-Auth-Token': login.auth.token, + 'X-User-Id': login.auth.userId + }, + }; + + return axios.post(`${login.api}/chat.postMessage`, { + 'channel': recipient, + 'avatar': avatar, + 'alias': sender.nickname, + 'text': message + }, options); + }; + + /** + * Update status and attempts of a chat + * + * @param {object} chat - The chat + * @param {string} status - The new status + * @return {Promise} - The request promise + */ + async function updateChat(chat, status) { + return chat.updateAttributes({ + status: status, + attempts: ++chat.attempts + }); + } + + /** + * Returns the current user status on Rocketchat + * + * @param {string} username - The recipient user name + * @return {Promise} - The request promise + */ + Self.getUserStatus = async function getUserStatus(username) { + if (process.env.NODE_ENV !== 'production') { + return new Promise(resolve => { + return resolve({ + data: { + status: 'online' + } + }); + }); + } + + const login = await Self.getServiceAuth(); + + const options = { + params: {username}, + headers: { + 'X-Auth-Token': login.auth.token, + 'X-User-Id': login.auth.userId + }, + }; + + return axios.get(`${login.api}/users.getStatus`, options); + }; +}; diff --git a/back/methods/chat/spec/send.spec.js b/back/methods/chat/spec/send.spec.js index 634e1d420..dd07a1342 100644 --- a/back/methods/chat/spec/send.spec.js +++ b/back/methods/chat/spec/send.spec.js @@ -1,14 +1,14 @@ const app = require('vn-loopback/server/server'); describe('Chat send()', () => { - it('should return a "Fake notification sent" as response', async() => { + it('should return true as response', async() => { let ctx = {req: {accessToken: {userId: 1}}}; let response = await app.models.Chat.send(ctx, '@salesPerson', 'I changed something'); expect(response).toEqual(true); }); - it('should retrun false as response', async() => { + it('should return false as response', async() => { let ctx = {req: {accessToken: {userId: 18}}}; let response = await app.models.Chat.send(ctx, '@salesPerson', 'I changed something'); diff --git a/back/methods/chat/spec/sendCheckingPresence.spec.js b/back/methods/chat/spec/sendCheckingPresence.spec.js index 712e7f947..5d1a1b3dd 100644 --- a/back/methods/chat/spec/sendCheckingPresence.spec.js +++ b/back/methods/chat/spec/sendCheckingPresence.spec.js @@ -1,58 +1,21 @@ const models = require('vn-loopback/server/server').models; describe('Chat sendCheckingPresence()', () => { - const today = new Date(); - today.setHours(6, 0); - const ctx = {req: {accessToken: {userId: 1}}}; - const chatModel = models.Chat; - const departmentId = 23; - const workerId = 1107; + it('should return true as response', async() => { + const workerId = 1107; - it(`should call to send() method with "@HankPym" as recipient argument`, async() => { - spyOn(chatModel, 'send').and.callThrough(); - spyOn(chatModel, 'getUserStatus').and.returnValue( - new Promise(resolve => { - return resolve({ - data: { - status: 'online' - } - }); - }) - ); + let ctx = {req: {accessToken: {userId: 1}}}; + let response = await models.Chat.sendCheckingPresence(ctx, workerId, 'I changed something'); - await chatModel.sendCheckingPresence(ctx, workerId, 'I changed something'); - - expect(chatModel.send).toHaveBeenCalledWith(ctx, '@HankPym', 'I changed something'); + expect(response).toEqual(true); }); - it(`should call to send() method with "#cooler" as recipient argument`, async() => { - spyOn(chatModel, 'send').and.callThrough(); - spyOn(chatModel, 'getUserStatus').and.returnValue( - new Promise(resolve => { - return resolve({ - data: { - status: 'offline' - } - }); - }) - ); + it('should return false as response', async() => { + const salesPersonId = 18; - const tx = await models.Claim.beginTransaction({}); + let ctx = {req: {accessToken: {userId: 18}}}; + let response = await models.Chat.sendCheckingPresence(ctx, salesPersonId, 'I changed something'); - try { - const options = {transaction: tx}; - - const department = await models.Department.findById(departmentId, null, options); - await department.updateAttribute('chatName', 'cooler'); - - await chatModel.sendCheckingPresence(ctx, workerId, 'I changed something'); - - expect(chatModel.send).toHaveBeenCalledWith(ctx, '#cooler', '@HankPym ➔ I changed something'); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } + expect(response).toEqual(false); }); }); diff --git a/back/methods/chat/spec/sendQueued.spec.js b/back/methods/chat/spec/sendQueued.spec.js new file mode 100644 index 000000000..bbf5a73c7 --- /dev/null +++ b/back/methods/chat/spec/sendQueued.spec.js @@ -0,0 +1,41 @@ +const models = require('vn-loopback/server/server').models; + +describe('Chat sendCheckingPresence()', () => { + const today = new Date(); + today.setHours(6, 0); + const chatModel = models.Chat; + + it(`should call to sendCheckingUserStatus()`, async() => { + spyOn(chatModel, 'sendCheckingUserStatus').and.callThrough(); + + const chat = { + checkUserStatus: 1, + status: 0, + attempts: 0 + }; + + await chatModel.destroyAll(); + await chatModel.create(chat); + + await chatModel.sendQueued(); + + expect(chatModel.sendCheckingUserStatus).toHaveBeenCalled(); + }); + + it(`should call to sendMessage() method`, async() => { + spyOn(chatModel, 'sendMessage').and.callThrough(); + + const chat = { + checkUserStatus: 0, + status: 0, + attempts: 0 + }; + + await chatModel.destroyAll(); + await chatModel.create(chat); + + await chatModel.sendQueued(); + + expect(chatModel.sendMessage).toHaveBeenCalled(); + }); +}); diff --git a/back/models/chat.js b/back/models/chat.js index 7d8468aae..95a1e2c29 100644 --- a/back/models/chat.js +++ b/back/models/chat.js @@ -3,4 +3,5 @@ module.exports = Self => { require('../methods/chat/send')(Self); require('../methods/chat/sendCheckingPresence')(Self); require('../methods/chat/notifyIssues')(Self); + require('../methods/chat/sendQueued')(Self); }; diff --git a/back/models/chat.json b/back/models/chat.json index 697d8c181..8fc3a6304 100644 --- a/back/models/chat.json +++ b/back/models/chat.json @@ -1,6 +1,39 @@ { "name": "Chat", "base": "VnModel", + "options": { + "mysql": { + "table": "chat" + } + }, + "properties": { + "id": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "senderFk": { + "type": "number" + }, + "recipient": { + "type": "string" + }, + "dated": { + "type": "date" + }, + "checkUserStatus": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + }, + "attempts": { + "type": "number" + } + }, "acls": [{ "property": "validations", "accessType": "EXECUTE", diff --git a/db/changes/10470-family/00-chat.sql b/db/changes/10470-family/00-chat.sql new file mode 100644 index 000000000..d4a8f068a --- /dev/null +++ b/db/changes/10470-family/00-chat.sql @@ -0,0 +1,13 @@ +CREATE TABLE `vn`.`chat` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `senderFk` int(10) unsigned DEFAULT NULL, + `recipient` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL, + `dated` date DEFAULT NULL, + `checkUserStatus` tinyint(1) DEFAULT NULL, + `message` varchar(200) COLLATE utf8_unicode_ci DEFAULT NULL, + `status` tinyint(1) DEFAULT NULL, + `attempts` int(1) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `chat_FK` (`senderFk`), + CONSTRAINT `chat_FK` FOREIGN KEY (`senderFk`) REFERENCES `account`.`user` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; \ No newline at end of file diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index c91ba71c2..a245f6f90 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2558,7 +2558,13 @@ INSERT INTO `vn`.`supplierAgencyTerm` (`agencyFk`, `supplierFk`, `minimumPackage (2, 1, 60, 0.00, 0.00, NULL, 0, 5.00, 33), (3, 2, 0, 15.00, 0.00, NULL, 0, 0.00, 0), (4, 2, 0, 20.00, 0.00, NULL, 0, 0.00, 0), - (5, 442, 0, 0.00, 3.05, NULL, 0, 0.00, 0); + (5, 442, 0, 0.00, 3.05, NULL, 0, 0.00, 0); + +INSERT INTO `vn`.`chat` (`senderFk`, `recipient`, `dated`, `checkUserStatus`, `message`, `status`, `attempts`) + VALUES + (1101, '@PetterParker', CURDATE(), 1, 'First test message', 0, 0), + (1101, '@PetterParker', CURDATE(), 0, 'Second test message', 0, 0); + INSERT INTO `vn`.`mobileAppVersionControl` (`appName`, `version`, `isVersionCritical`) VALUES