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
664 lines (616 loc) • 20.2 kB
JavaScript
var _ = require('underscore');
var moment = require('moment');
var ChatExpress = require('../chat-platform/chat-platform');
var RtmClient = require('@slack/client').RTMClient;
var WebClient = require('@slack/client').WebClient;
var lcd = require('../helpers/lcd');
var request = require('request').defaults({ encoding: null });
var ChatLog = require('../chat-log');
var utils = require('../../lib/helpers/utils');
var when = utils.when;
/*
- Send a message, upload file: https://slackapi.github.io/node-slack-sdk/web_api#posting-a-message
*/
var Slack = new ChatExpress({
transport: 'slack',
transportDescription: '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 = _.isArray(payload.files) && !_.isEmpty(payload.files) ? payload.files[0].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 chatServer = this;
var options = this.getOptions();
options.connector = new RtmClient(options.token);
options.connector.start();
// needed to send messages with attachments
options.client = new WebClient(options.token);
options.connector.on('message', function(message) {
// For structure of `event`, see https://api.slack.com/events/message
// skipe messages from the bot, do not re-introduce in the flow
if ( (message.subtype && message.subtype === 'bot_message') ||
(!message.subtype && message.user === options.connector.activeUserId) ) {
return;
}
chatServer.receive(message);
});
return true;
},
routes: {
'/redbot/slack/command': function(req, res) {
this.receive({
type: 'message',
channel: req.body.channel_id,
user: req.body.user_id,
text: req.body.command + ' ' + req.body.text,
trigger_id: req.body.trigger_id,
ts: moment().unix()
});
res.send(''); // 200 empty body
},
'/redbot/slack/test': function(req, res) {
res.send('ok');
},
'/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.send(''); // 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.send(''); // 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);
}
}
},
routesDescription: {
'/redbot/slack': 'Use this in the "Request URL" of the "Interactive Components" of your Slack App',
'/redbot/slack/command': 'Endpoint for Slack command',
'/redbot/slack/test': 'Use this to test that your SSL (with certificate or ngrok) is working properly, should answer "ok"'
}
});
Slack.in(function(message) {
return new Promise(function(resolve) {
// cleanup the payload
delete message.payload.source_team;
delete message.payload.team;
// todo check if necessary
// echo after a button is clicked, discard
if (message.originalMessage.subtype === 'message_changed') {
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({ dialog: JSON.stringify(dialog), trigger_id: message.originalMessage.trigger_id })
.then(
function() {
resolve(message);
},
reject
);
});
});
Slack.out('message', function(message) {
var options = this.getOptions();
var slackExtensions = this.getSlackExtensions();
var chatContext = message.chat();
return new Promise(function(resolve, reject) {
var client = options.client;
var payload = Object.assign({ channel: message.payload.chatId, text: message.payload.content }, slackExtensions);
client.chat.postMessage(payload)
.then(function(res) {
return when(chatContext.set('messageId', res.ts));
})
.then(
function() {
resolve(message);
},
function(error) {
reject(error);
}
);
});
});
Slack.out('location', function(message) {
var options = this.getOptions();
var slackExtensions = this.getSlackExtensions();
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': !_.isEmpty(message.payload.place) ? message.payload.place : 'Position',
'title': link,
'title_link': link,
'color': '#7CD197'
}
];
var payload = Object.assign({
channel: message.payload.chatId,
text: '',
attachments: attachments
}, slackExtensions);
client.chat
.postMessage(payload)
.then(
function() {
resolve(message);
},
reject
);
});
});
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;
var url = message.originalMessage.files[0].url_private_download;
return new Promise(function(resolve, reject) {
chatServer.downloadUrl(url)
.then(
function(body) {
message.payload.content = body;
resolve(message);
},
function(e) {
reject('Photo Error: ' + e);
});
});
});
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;
var url = message.originalMessage.files[0].url_private_download;
return new Promise(function(resolve, reject) {
chatServer.downloadUrl(url)
.then(
function(body) {
message.payload.content = body;
resolve(message);
},
function(e) {
reject('Document Error: ' + e)
});
});
});
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;
var url = message.originalMessage.files[0].url_private_download;
return new Promise(function(resolve, reject) {
chatServer.downloadUrl(url)
.then(
function(body) {
message.payload.content = body;
resolve(message);
},
function(e) {
reject('Audio Error: ' + e)
});
});
});
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;
var url = message.originalMessage.files[0].url_private_download;
return new Promise(function(resolve, reject) {
chatServer.downloadUrl(url)
.then(
function(body) {
message.payload.content = body;
resolve(message);
},
function(e) {
reject('Video Error: ' + e)
});
});
});
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 slackExtensions = this.getSlackExtensions();
var chatServer = this;
return new Promise(function(resolve, reject) {
var client = options.client;
var payload = {
channel: message.payload.chatId,
text: message.payload.content,
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)
}
]
};
payload = Object.assign(payload, slackExtensions);
client.chat
.postMessage(payload)
.then(
function() {
resolve(message);
},
reject
);
});
});
// todo classes only when selected
Slack.out('generic-template', function(message) {
var chatServer = this;
var options = this.getOptions();
var chatContext = message.chat();
var slackExtensions = this.getSlackExtensions();
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;
});
var payload = {
channel: message.payload.chatId,
text: message.payload.content,
attachments: attachments
};
payload = Object.assign(payload, slackExtensions);
client.chat
.postMessage(payload)
.then(function(res) {
return when(chatContext.set('messageId', res.ts));
})
.then(
function() {
resolve(message);
},
reject
);
});
});
// 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({
getSlackExtensions: function() {
var options = this.getOptions();
var slackExtensions = {};
if (!_.isEmpty(options.username)) {
slackExtensions.username = options.username;
slackExtensions.as_user = false;
}
if (!_.isEmpty(options.iconEmoji)) {
slackExtensions.icon_emoji = options.iconEmoji;
slackExtensions.as_user = false;
}
return slackExtensions;
},
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) {
var node_options = this.getOptions();
return new Promise(function(resolve, reject) {
// In order to retried private files a valid OAuth token must be
// provided on the Bearer Authorization. The current slack code parses out
// url_private_download into the url variable.
if (!_.isEmpty(node_options.oauthToken)) {
var options = {
url: url,
headers : {
'Authorization': 'Bearer ' + node_options.oauthToken,
}
};
request(options, function(error, response, body) {
if (error) {
reject('Unable to download file ' + url);
} else {
resolve(body);
}
});
} else {
// eslint-disable-next-line no-console
console.log(lcd.error('The Slack bot configuration has no OAuth token.'));
// eslint-disable-next-line no-console
console.log(lcd.grey(
'In order to upload binaries from the Slack client, a OAuth token must be provided, get the token in the'
+ '"OAuth & Permission" section in https://api.slack.com'
));
reject('No OAuth Token configured. Check');
}
});
},
// eslint-disable-next-line max-params
sendBuffer: function(chatId, buffer, filename, caption) {
var options = this.getOptions();
var client = options.client;
filename = !_.isEmpty(filename) ? filename : _.uniqueId('tmp_file_');
return client.files
.upload({
filename: filename,
file: buffer,
filetype: 'auto',
title: caption,
channels: chatId
});
},
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;
}
});
Slack.registerMessageType('action', 'Action', 'Send an action message (like typing, ...)');
Slack.registerMessageType('audio', 'Audio', 'Send an audio message');
Slack.registerMessageType('dialog', 'Dialog', 'Open a dialog form');
Slack.registerMessageType('document', 'Document', 'Send a document or generic file');
Slack.registerMessageType('inline-buttons', 'Inline buttons', 'Send a message with inline buttons');
Slack.registerMessageType('location', 'Location', 'Send a map location message');
Slack.registerMessageType('message', 'Message', 'Send a plain text message');
Slack.registerMessageType('photo', 'Photo', 'Send a photo message');
Slack.registerMessageType('video', 'Video', 'Send video message');
Slack.registerMessageType('response', 'Dialog Response', 'Dialog response received');
module.exports = Slack;