Merge pull request '4040-chat_sendQueued' (#993) from 4040-chat_sendQueued into dev
gitea/salix/pipeline/head There was a failure building this commit Details

Reviewed-on: #993
Reviewed-by: Carlos Jimenez Ruiz <carlosjr@verdnatura.es>
Reviewed-by: Joan Sanchez <joan@verdnatura.es>
This commit is contained in:
Joan Sanchez 2022-06-09 09:57:56 +00:00
commit be50663faa
10 changed files with 297 additions and 132 deletions

View File

@ -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);
}
};

View File

@ -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;
};
};

View File

@ -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);
};
};

View File

@ -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');

View File

@ -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);
});
});

View File

@ -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();
});
});

View File

@ -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);
};

View File

@ -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",

View File

@ -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;

View File

@ -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