node-red-contrib-chatbot
Version:
REDBot a Chat bot for a full featured chat bot for Telegram**, Facebook Messenger and Slack. Almost no coding skills required
623 lines (585 loc) • 18 kB
JavaScript
var _ = require('underscore');
var moment = require('moment');
var ChatExpress = require('../chat-platform/chat-platform');
var fs = require('fs');
var os = require('os');
var RtmClient = require('@slack/client').RtmClient;
var WebClient = require('@slack/client').WebClient;
var RTM_EVENTS = require('@slack/client').RTM_EVENTS;
var request = require('request').defaults({ encoding: null });
var ChatLog = require('../chat-log');
var utils = require('../../lib/helpers/utils');
var when = utils.when;
var Slack = new ChatExpress({
inboundMessageEvent: RTM_EVENTS.MESSAGE,
transport: 'slack',
chatIdKey: 'channel',
userIdKey: 'user',
tsKey: function(payload) {
return moment.unix(payload.ts);
},
type: function(payload) {
var type = payload.type;
// get mime if any file
var fileMimeType = payload.subtype === 'file_share' ? payload.file.mimetype : null;
// convert message type
if (fileMimeType != null && fileMimeType.indexOf('image') !== -1) {
type = 'photo';
} else if (fileMimeType != null && fileMimeType.indexOf('audio') !== -1) {
type = 'audio';
} else if (fileMimeType != null && fileMimeType.indexOf('video') !== -1) {
type = 'video';
} else if (fileMimeType != null) {
type = 'document';
} else if (!_.isEmpty(payload.subtype)) {
// slack uses a taxonomy with type and subtype, basically everything is a "message"
type = payload.subtype;
}
return type;
},
onStop: function() {
var options = this.getOptions();
options.connector.disconnect();
return true;
},
onStart: function() {
var options = this.getOptions();
options.connector = new RtmClient(options.token);
options.connector.start();
options.client = new WebClient(options.token);
if (_.isEmpty(options.botname)) {
this.warn('Param "botname" is empty, this is needed to detect echo messages from the bot itself and can, '
+ 'cause endless circular effect.');
}
return true;
},
routes: {
'/redbot/slack': function(req, res) {
var payload = this.parsePayload(req.body);
if (payload != null && payload.type === 'interactive_message' && payload.actions[0].value.indexOf('dialog_') !== -1) {
// if it's the callback of a dialog button, then relay a dialog message
this.receive({
type: 'dialog',
channel: payload.channel.id,
user: payload.user.id,
text: payload.actions[0].value.replace('dialog_', ''),
ts: payload.action_ts,
trigger_id: payload.trigger_id,
callback_id: payload.callback_id
});
// if there's feedback, send it back, otherwise do nothing
if (!_.isEmpty(this.getButtonFeedback(payload.actions[0].name))) {
res.send({
response_type: 'ephemeral',
replace_original: false,
text: this.getButtonFeedback(payload.actions[0].name)
});
} else {
res.sendStatus(200); // generic answer
}
} else if (payload != null && payload.type === 'interactive_message') {
// relay a message with the value of the button
this.receive({
type: 'message',
channel: payload.channel.id,
user: payload.user.id,
text: payload.actions[0].value,
ts: payload.action_ts,
trigger_id: payload.trigger_id,
callback_id: payload.callback_id
});
// if there's feedback, send it back, otherwise do nothing
if (!_.isEmpty(this.getButtonFeedback(payload.actions[0].name))) {
res.send({
response_type: 'ephemeral',
replace_original: false,
text: this.getButtonFeedback(payload.actions[0].name)
});
} else {
res.sendStatus(200); // generic answer
}
} else if (payload.type === 'dialog_submission') {
// intercept a dialog response and relay
this.receive({
type: 'response',
channel: payload.channel.id,
user: payload.user.id,
response: payload.submission,
ts: payload.action_ts,
trigger_id: payload.trigger_id,
callback_id: payload.callback_id
});
res.send(''); // 200 empty body
} else {
res.sendStatus(200);
}
},
'/redbot/slack/test': function(req, res) {
res.send('ok');
}
},
routesDescription: {
'/redbot/slack': 'Use this in the "Request URL" of the "Interactive Components" of your Slack App',
'/redbot/slack/test': 'Use this to test that your SSL (with certificate or ngrok) is working properly, should answer "ok"'
}
});
Slack.in(function(message) {
var options = this.getOptions();
return new Promise(function(resolve) {
// cleanup the payload
delete message.payload.source_team;
delete message.payload.team;
// if the message has subtype bot_message, then stop if, it's the echo
if (message.originalMessage.subtype === 'bot_message') {
return;
}
// echo after a button is clicked, discard
if (message.originalMessage.subtype === 'message_changed') {
return;
}
// skip message from the bot
if (message.originalMessage.username === options.botname) {
return;
}
resolve(message);
});
});
Slack.in(function(message) {
var options = this.getOptions();
var authorizedUsernames = options.authorizedUsernames;
// check if it's in the list of authorized users
if (!_.isEmpty(authorizedUsernames)) {
if (_(authorizedUsernames).contains(message.payload.userId)) {
return new Promise(function(resolve, reject) {
return when(message.chat().set('authorized', true))
.then(function() {
resolve(message);
}, reject);
});
}
}
return message;
});
Slack.in('message', function(message) {
return new Promise(function(resolve, reject) {
var chatContext = message.chat();
message.payload.content = message.originalMessage.text;
delete message.payload.text;
when(chatContext.set('message', message.payload.content))
.then(function() {
resolve(message);
}, function(error) {
reject(error);
});
});
});
Slack.in('dialog', function(message) {
message.payload.content = message.originalMessage.text;
return message;
});
Slack.in('response', function(message) {
message.payload.content = message.originalMessage.response;
return message;
});
Slack.out('dialog', function(message) {
var options = this.getOptions();
var client = options.client;
return new Promise(function(resolve, reject) {
// map some element in order to change var conventions
var elements = _(message.payload.elements)
.map(function(item) {
var element = _.clone(item);
element.max_length = element.maxLength;
element.min_length = element.minLength;
delete element.minLength;
delete element.maxLength;
return element;
});
var dialog = {
callback_id: message.originalMessage.callback_id,
title: message.payload.title,
submit_label: message.payload.submitLabel,
elements: elements
};
client.dialog.open(JSON.stringify(dialog), message.originalMessage.trigger_id, function(err) {
if (err != null) {
reject(err);
} else {
resolve(message);
}
});
});
});
Slack.out('message', function(message) {
var options = this.getOptions();
var chatContext = message.chat();
return new Promise(function(resolve, reject) {
var client = options.client;
client.chat.postMessage(
message.payload.chatId,
message.payload.content,
function(err, res) {
if (err) {
reject(err)
} else {
when(chatContext.set('messageId', res.ts))
.then(
function() {
resolve(message);
},
function(error) {
reject(error);
});
}
}
);
});
});
Slack.out('location', function(message) {
var options = this.getOptions();
return new Promise(function(resolve, reject) {
var client = options.client;
// build map link
var link = 'https://www.google.com/maps?f=q&q=' + message.payload.content.latitude + ','
+ message.payload.content.longitude + '&z=16';
// send simple attachment
var attachments = [
{
'author_name': 'Position',
'title': link,
'title_link': link,
'color': '#7CD197'
}
];
client.chat.postMessage(
message.payload.chatId,
'',
{
attachments: attachments
},
function(err) {
if (err) {
reject(err)
} else {
resolve(message);
}
}
);
});
});
Slack.out('action', function(message) {
var connector = this.getConnector();
return new Promise(function(resolve) {
connector.sendTyping(message.payload.chatId);
resolve(message);
});
});
Slack.in('photo', function(message) {
var chatServer = this;
return new Promise(function(resolve, reject) {
chatServer.downloadUrl(message.originalMessage.file.url_private_download)
.then(
function(body) {
message.payload.content = body;
resolve(message);
},
function() {
reject('Error loading: ' + message.payload.file.url_private_download)
});
});
});
Slack.out('photo', function(message) {
var chatServer = this;
return new Promise(function(resolve, reject) {
chatServer.sendBuffer(message.payload.chatId, message.payload.content, message.payload.filename, message.payload.caption)
.then(
function() {
resolve(message);
},
function(error) {
reject(error);
});
});
});
Slack.in('document', function(message) {
var chatServer = this;
return new Promise(function(resolve, reject) {
chatServer.downloadUrl(message.originalMessage.file.url_private_download)
.then(
function(body) {
message.payload.content = body;
resolve(message);
},
function() {
reject('Error loading: ' + message.payload.file.url_private_download)
});
});
});
Slack.out('document', function(message) {
var chatServer = this;
return new Promise(function(resolve, reject) {
chatServer.sendBuffer(message.payload.chatId, message.payload.content, message.payload.filename, message.payload.caption)
.then(
function() {
resolve(message);
},
function(error) {
reject(error);
});
});
});
Slack.in('audio', function(message) {
var chatServer = this;
return new Promise(function(resolve, reject) {
chatServer.downloadUrl(message.originalMessage.file.url_private_download)
.then(
function(body) {
message.payload.content = body;
resolve(message);
},
function() {
reject('Error loading: ' + message.payload.file.url_private_download)
});
});
});
Slack.out('audio', function(message) {
var chatServer = this;
return new Promise(function(resolve, reject) {
chatServer.sendBuffer(message.payload.chatId, message.payload.content, message.payload.filename, message.payload.caption)
.then(
function() {
resolve(message);
},
function(error) {
reject(error);
});
});
});
Slack.in('video', function(message) {
var chatServer = this;
return new Promise(function(resolve, reject) {
chatServer.downloadUrl(message.originalMessage.file.url_private_download)
.then(
function(body) {
message.payload.content = body;
resolve(message);
},
function() {
reject('Error loading: ' + message.payload.file.url_private_download)
});
});
});
Slack.out('video', function(message) {
var chatServer = this;
return new Promise(function(resolve, reject) {
chatServer.sendBuffer(message.payload.chatId, message.payload.content, message.payload.filename, message.payload.caption)
.then(
function() {
resolve(message);
},
function(error) {
reject(error);
});
});
});
Slack.out('inline-buttons', function(message) {
var options = this.getOptions();
var chatServer = this;
return new Promise(function(resolve, reject) {
var client = options.client;
client.chat.postMessage(
message.payload.chatId,
'',
{
attachments: [
{
'text': message.payload.content,
callback_id: !_.isEmpty(message.payload.name) ? message.payload.name : _.uniqueId('callback_'),
color: '#3AA3E3',
attachment_type: 'default',
actions: chatServer.parseButtons(message.payload.buttons)
}
]
},
function(err) {
if (err) {
reject(err)
} else {
resolve(message);
}
}
);
});
});
Slack.out('generic-template', function(message) {
var chatServer = this;
var options = this.getOptions();
var chatContext = message.chat();
return new Promise(function(resolve, reject) {
var client = options.client;
var attachments = _(message.payload.elements).map(function(item) {
var attachment = {
title: item.title,
callback_id: !_.isEmpty(item.title) ? item.title : _.uniqueId('callback_'),
actions: chatServer.parseButtons(item.buttons)
};
if (!_.isEmpty(item.subtitle)) {
attachment.text = item.subtitle;
}
if (!_.isEmpty(item.imageUrl)) {
attachment.image_url = item.imageUrl;
}
return attachment;
});
client.chat.postMessage(
message.payload.chatId,
'',
{
attachments: attachments
},
function(err, res) {
if (err) {
reject(err)
} else {
when(chatContext.set('messageId', res.ts))
.then(
function() {
resolve(message);
},
function(error) {
reject(error);
});
}
}
);
});
});
// log messages, these should be the last
Slack.out(function(message) {
var options = this.getOptions();
var logfile = options.logfile;
var chatContext = message.chat();
if (!_.isEmpty(logfile)) {
return when(chatContext.all())
.then(function(variables) {
var chatLog = new ChatLog(variables);
return chatLog.log(message, logfile);
});
}
return message;
});
Slack.in('*', function(message) {
var options = this.getOptions();
var logfile = options.logfile;
var chatContext = message.chat();
if (!_.isEmpty(logfile)) {
return when(chatContext.all())
.then(function(variables) {
var chatLog = new ChatLog(variables);
return chatLog.log(message, logfile);
});
}
return message;
});
Slack.mixin({
parseButtons: function(buttons) {
var chatServer = this;
return _(buttons)
.chain()
.filter(function(button) {
return button.type === 'postback' || button.type === 'dialog';
})
.map(function(button) {
var name = button.value || button.label;
if (!_.isEmpty(button.answer)) {
chatServer.setButtonFeedback(name, button.answer);
}
// if the button is dialog, then prefix the "dialog_" to trigger a dialog message
var value = button.value || button.label;
if (button.type === 'dialog') {
value = 'dialog_' + value;
}
return {
name: name,
text: button.label || button.value,
value: value,
type: 'button',
style: !_.isEmpty(button.style) ? button.style : 'default'
};
})
.value();
},
downloadUrl: function(url) {
return new Promise(function(resolve, reject) {
var options = {
url: url
};
request(options, function(error, response, body) {
if (error) {
reject('Unable to download file ' + url);
} else {
resolve(body);
}
});
});
},
// eslint-disable-next-line max-params
sendBuffer: function(chatId, buffer, filename, caption) {
var options = this.getOptions();
return new Promise(function (resolve, reject) {
//var client = server.options.client;
var client = options.client;
filename = !_.isEmpty(filename) ? filename : _.uniqueId('tmp_file_');
var tmpFile = os.tmpdir() + '/' + filename;
// create a temp file, then pass as stream
fs.writeFile(tmpFile, buffer, function(err) {
if (err) {
reject(err);
}
client.files.upload(
filename,
{
file: fs.createReadStream(tmpFile),
filetype: 'auto',
title: caption,
channels: chatId
},
function(err) {
// uploaded, if no error remove temp file
if (err) {
reject(err)
} else {
fs.unlink(tmpFile, function() {
resolve();
});
}
}
); // end upload file
}); // end write file
});
},
setButtonFeedback: function(name, message) {
if (this._buttonFeedbacks == null) {
this._buttonFeedbacks = {};
}
this._buttonFeedbacks[name] = message;
},
getButtonFeedback: function(name) {
return this._buttonFeedbacks != null ? this._buttonFeedbacks[name] : null;
},
/**
* @method parsePayload
* Parse an incoming message after an interactive message
* https://api.slack.com/interactive-messages#responding
*/
parsePayload: function(message) {
var obj = null;
try {
obj = JSON.parse(message.payload);
} catch(e) {
// do nothing
}
return obj;
}
});
module.exports = Slack;