UNPKG

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

1,045 lines (972 loc) 33.9 kB
const _ = require('underscore'); const moment = require('moment'); const { ChatExpress, ChatLog } = require('chat-platform'); const request = require('request').defaults({ encoding: null }); const { App } = require('@slack/bolt'); const lcd = require('../../helpers/lcd'); const { when, params } = require('../../helpers/utils'); /* BOLT DOCUMENTATION: https://slack.dev/bolt-js/concepts */ /* YAML file to start a bot with WS support display_information: name: Guidone features: app_home: home_tab_enabled: false messages_tab_enabled: true messages_tab_read_only_enabled: false bot_user: display_name: Guidone always_online: true oauth_config: scopes: bot: - channels:history - channels:join - commands - chat:write - im:history - users:write - files:read - files:write settings: event_subscriptions: bot_events: - message.channels - message.im interactivity: is_enabled: true org_deploy_enabled: false socket_mode_enabled: true is_hosted: false token_rotation_enabled: false */ /* YAML display_information: name: MySlack WebHook features: app_home: home_tab_enabled: false messages_tab_enabled: true messages_tab_read_only_enabled: false bot_user: display_name: MySlack WebHook always_online: true oauth_config: scopes: bot: - channels:history - channels:join - chat:write - commands - files:read - files:write - im:history - users:write - chat:write.customize settings: event_subscriptions: request_url: https://123456.ngrok.io/slack/events bot_events: - message.channels - message.im interactivity: is_enabled: true request_url: https://123456.ngrok.io/slack/events org_deploy_enabled: false socket_mode_enabled: false token_rotation_enabled: false */ // set the messageId in a returning payload const setMessageId = (message, messageId) => ({ ...message, payload: { ...message.payload, messageId } }); const escapeSlackCommands = text => { if (typeof text === 'string' && text.startsWith('//')) { return text.replace('//', '/'); } return text; }; const parseActions = actions => { const result = {}; actions.forEach(action => { switch(action.type) { case 'button': result[action.block_id] = action.value; break; case 'overflow': case 'static_select': result[action.block_id] = action.selected_option.value; break; case 'multi_static_select': result[action.block_id] = action.selected_options.map(option => option.value); break; } }) return result; } const parseChannel = event => { // not sure if channel_ids is still used in slack event payloads if (_.isArray(event.channel_ids) && !_.isEmpty(event.channel_ids)) { return event.channel_ids[0]; } else if (event.channel ) { return event.channel; } else { return null } }; const boltMiddleware = (chatServer) => { return async function({ next, body: payload, client, ack }) { // skip if empty if (payload == null) { next(); return; } if (!_.isEmpty(payload.command)) { await ack(); chatServer.receive({ ...payload, type: 'command', channel: payload.channel_id, user: payload.user_id, channel_id: undefined, user_id: undefined }); next(); } else if (payload.type === 'event_callback' && payload.event.type === 'message') { chatServer.receive({ ...payload.event, text: escapeSlackCommands(payload.event.text), ts: moment.unix(payload.event.ts), }); } else if (payload != null && payload.type === 'interactive_message' && payload.actions[0].value.indexOf('dialog_') !== -1) { ack(); // if it's the callback of a dialog button, then relay a dialog message chatServer.receive({ type: 'dialog', channel: payload.channel.id, user: payload.user.id, text: payload.actions[0].value.replace('dialog_', ''), ts: moment.unix(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(chatServer.getButtonFeedback(payload.actions[0].name))) { client.chat.postEphemeral({ channel: payload.channel.id, user: payload.user.id, text: chatServer.getButtonFeedback(payload.actions[0].name) }); } else { next(); } } else if (payload != null && payload.type === 'interactive_message') { ack(); // relay a message with the value of the button chatServer.receive({ type: 'message', channel: payload.channel.id, user: payload.user.id, text: payload.actions[0].value, ts: moment.unix(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(chatServer.getButtonFeedback(payload.actions[0].name))) { client.chat.postEphemeral({ channel: payload.channel.id, user: payload.user.id, text: chatServer.getButtonFeedback(payload.actions[0].name) }); } else { next(); } } else if (payload.type === 'dialog_submission') { ack(); // intercept a dialog response and relay chatServer.receive({ type: 'response', channel: payload.channel.id, user: payload.user.id, response: payload.submission, ts: moment.unix(payload.action_ts), trigger_id: payload.trigger_id, callback_id: payload.callback_id }); next(); } else if (payload.type === 'block_actions') { ack(); chatServer.receive({ type: 'response', channel: payload.channel.id, user: payload.user.id, response: parseActions(payload.actions), actions: payload.actions, ts: moment.unix(payload.action_ts), trigger_id: payload.trigger_id, callback_id: payload.callback_id }); next(); } else if (payload.type === 'event_callback') { chatServer.receive({ type: 'event', channel: parseChannel(payload.event), eventPayload: _.omit(payload.event, 'type'), eventType: payload.event.type }); next(); } else { next(); } }; } const Slack = new ChatExpress({ transport: 'slack', transportDescription: 'Slack', color: '#39143e', chatIdKey: 'channel', userIdKey: 'user', messageIdKey: function(payload) { return payload.event_ts; }, tsKey: function(payload) { return moment.unix(payload.ts).toISOString(); }, type: function(payload) { let type = payload.type; // get mime if any file const 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: async function() { if (this.server != null) { await this.server.stop(); } return true; }, onStart: async function() { const chatServer = this; const options = this.getOptions(); const serverPort = !_.isEmpty(options.serverPort) && !isNaN(parseInt(options.serverPort, 10)) ? parseInt(options.serverPort, 10) : 3001; // create listening server chatServer.server = new App({ token: options.token, signingSecret: options.signingSecret, socketMode: options.useWebSocket, appToken: options.appToken, port: serverPort }); options.client = chatServer.server.client; chatServer.server.use(boltMiddleware(chatServer)); await chatServer.server.start(serverPort) // show endpoints if not using web sockets if (!options.useWebSocket) { // eslint-disable-next-line no-console console.log(lcd.grey('------ WebHooks for SLACK ----------------')); // eslint-disable-next-line no-console console.log(lcd.green(`http://localhost:${serverPort}/events`) + lcd.grey(' - ') + lcd.white('Callback for events') ); // eslint-disable-next-line no-console console.log(lcd.green(`http://localhost:${serverPort}/interactions`) + lcd.grey(' - ') + lcd.white('Callback for interactions (postback, dialogs, etc)') ); // eslint-disable-next-line no-console console.log(lcd.green(`http://localhost:${serverPort}/commands`) + lcd.grey(' - ') + lcd.white('Callback for commands') ); // eslint-disable-next-line no-console console.log(lcd.green(`http://localhost:${serverPort}/test`) + lcd.grey(' - ') + lcd.white('Use this to test that your SSL (with certificate or ngrok) is working properly, should answer "ok"') ); // eslint-disable-next-line no-console console.log(''); } } }); Slack.in(async function(message) { // 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; } return message; }); Slack.in(function(message) { var options = this.getOptions(); var authorizedUsernames = (options.authorizedUsernames || '').split(','); // check if it's in the list of authorized users if (!_.isEmpty(authorizedUsernames)) { if (_(authorizedUsernames).contains(String(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(function(message) { const originalMessage = message.originalMessage; if (originalMessage.type === 'event') { message.payload = { type: 'event', eventType: originalMessage.eventType, ...originalMessage.eventPayload }; } return when(message); }); Slack.in('command', async function(message) { if (_.isString(message.originalMessage.text) && !_.isEmpty(message.originalMessage.text)) { message.payload.content = message.originalMessage.command; message.payload.type = 'command'; message.payload.arguments = !_.isEmpty(message.originalMessage.text) ? message.originalMessage.text.split(' ') : []; } return message; }); Slack.in('message', function(message) { return new Promise(function(resolve) { message.payload.content = message.originalMessage.text; delete message.payload.text; resolve(message); }); }); 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', async function(message) { const options = this.getOptions(); const client = options.client; // map some element in order to change var conventions const 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; }); const dialog = { callback_id: message.originalMessage.callback_id, title: message.payload.title, submit_label: message.payload.submitLabel, elements: elements }; await client.dialog.open({ dialog: JSON.stringify(dialog), trigger_id: message.originalMessage.trigger_id }) return message; }); Slack.out(async function(message) { const options = this.getOptions(); const client = options.client; const param = params(message); const deleteMessageId = param('deleteMessageId'); if (deleteMessageId) { await client.chat.delete({ channel: message.payload.chatId, ts: deleteMessageId }); message.payload = {}; } return message; }); Slack.out('message', async function(message) { const options = this.getOptions(); const slackExtensions = this.getSlackExtensions(message); const chatContext = message.chat(); const client = options.client; const param = params(message); const payload = Object.assign({ channel: message.payload.chatId, text: message.payload.content, mrkdwn: param('markdown', undefined), // send to the right thread if present thread_ts: !_.isEmpty(message.originalMessage.thread_ts) ? message.originalMessage.thread_ts : undefined }, slackExtensions); const res = await client.chat.postMessage(payload); await when(chatContext.set({ messageId: res.ts, outboundMessageId: res.ts })); return setMessageId(message, res.ts); }); Slack.out('blocks', async function(message) { const options = this.getOptions(); const slackExtensions = this.getSlackExtensions(message); const chatContext = message.chat(); const client = options.client; const payload = Object.assign({ channel: message.payload.chatId, text: message.payload.text, blocks: message.payload.content }, slackExtensions); const res = await client.chat.postMessage(payload) await when(chatContext.set({ messageId: res.ts, outboundMessageId: res.ts })); return setMessageId(message, res.ts); }); Slack.out('location', async function(message) { const options = this.getOptions(); const slackExtensions = this.getSlackExtensions(message); const chatContext = message.chat(); const client = options.client; // build map link const link = 'https://www.google.com/maps?f=q&q=' + message.payload.content.latitude + ',' + message.payload.content.longitude + '&z=16'; // send simple attachment const attachments = [ { 'author_name': !_.isEmpty(message.payload.place) ? message.payload.place : 'Position', 'title': link, 'title_link': link, 'color': '#7CD197' } ]; const payload = Object.assign({ channel: message.payload.chatId, text: '', attachments: attachments }, slackExtensions); const res = await client.chat.postMessage(payload) await when(chatContext.set({ messageId: res.ts, outboundMessageId: res.ts })); return setMessageId(message, res.ts); }); 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', async function(message) { const chatServer = this; const chatContext = message.chat(); const res = await chatServer.sendBuffer( message.payload.chatId, message.payload.content, message.payload.filename, message.payload.caption ); await when(chatContext.set({ messageId: res.ts, outboundMessageId: res.ts })); return setMessageId(message, res.ts); }); 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', async function(message) { const chatServer = this; const chatContext = message.chat(); const res = await chatServer.sendBuffer( message.payload.chatId, message.payload.content, message.payload.filename, message.payload.caption ); await when(chatContext.set({ messageId: res.ts, outboundMessageId: res.ts })); return setMessageId(message, res.ts); }); 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', async function(message) { const chatServer = this; const chatContext = message.chat(); const res = await chatServer.sendBuffer( message.payload.chatId, message.payload.content, message.payload.filename, message.payload.caption ); await when(chatContext.set({ messageId: res.ts, outboundMessageId: res.ts })); return setMessageId(message, res.ts); }); 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', async function(message) { const chatServer = this; const chatContext = message.chat(); const res = await chatServer.sendBuffer( message.payload.chatId, message.payload.content, message.payload.filename, message.payload.caption ); await when(chatContext.set({ messageId: res.ts, outboundMessageId: res.ts })); return setMessageId(message, res.ts); }); Slack.out('inline-buttons', async function(message) { const options = this.getOptions(); const slackExtensions = this.getSlackExtensions(message); const chatServer = this; const chatContext = message.chat(); const client = options.client; let 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); const res = await client.chat.postMessage(payload) await chatContext.set('messageId', res.ts); return setMessageId(message, res.ts); }); // todo classes only when selected Slack.out('generic-template', async function(message) { const chatServer = this; const options = this.getOptions(); const chatContext = message.chat(); const slackExtensions = this.getSlackExtensions(message); const client = options.client; const 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; }); let payload = { channel: message.payload.chatId, text: message.payload.content, attachments: attachments }; payload = Object.assign(payload, slackExtensions); const res = await client.chat.postMessage(payload) await chatContext.set('messageId', res.ts); return setMessageId(message, res.ts); }); // 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(message) { const param = params(message); const slackExtensions = {}; if (!_.isEmpty(param('username'))) { slackExtensions.username = param('username'); // it breaks, it should be added as per documentation //slackExtensions.as_user = false; } if (!_.isEmpty(param('icon_emoji'))) { slackExtensions.icon_emoji = param('icon_emoji'); } 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.token)) { var options = { url: url, headers : { 'Authorization': 'Bearer ' + node_options.token, } }; 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; } }); const videoExtensions = ['.mp4']; const audioExtensions = ['.mp3']; const documentExtensions = ['.pdf', '.zip']; const photoExtensions = ['.jpg', '.jpeg', '.png', '.gif']; Slack.registerMessageType('generic-template', 'Generic Template', 'Facebook like generic template'); Slack.registerMessageType('action', 'Action', 'Send an action message (like typing, ...)'); Slack.registerMessageType('dialog', 'Dialog', 'Open a dialog form'); 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('blocks', 'Blocks', 'Send message as blocks (Slack Block Kit)'); Slack.registerMessageType('response', 'Response', 'Dialog or Block Kit response'); Slack.registerMessageType( 'video', 'Video', 'Send video message', file => { if (!_.isEmpty(file.extension) && !videoExtensions.includes(file.extension)) { return `Unsupported file format for video node "${file.filename}", allowed formats: ${videoExtensions.join(', ')}`; } return null; } ); Slack.registerMessageType( 'document', 'Document', 'Send a document or generic file', file => { if (!_.isEmpty(file.extension) && !documentExtensions.includes(file.extension)) { return `Unsupported file format for document node "${file.filename}", allowed formats: ${documentExtensions.join(', ')}`; } return null; } ); Slack.registerMessageType( 'audio', 'Audio', 'Send an audio message', file => { if (!_.isEmpty(file.extension) && !audioExtensions.includes(file.extension)) { return `Unsupported file format for audio node "${file.filename}", allowed formats: ${audioExtensions.join(', ')}`; } return null; } ); Slack.registerMessageType( 'photo', 'Photo', 'Send a photo message', file => { if (!_.isEmpty(file.extension) && !photoExtensions.includes(file.extension)) { return `Unsupported file format for image node "${file.filename}", allowed formats: ${photoExtensions.join(', ')}`; } } ); Slack.registerEvent('file_deleted', 'A file was deleted'); Slack.registerEvent('app_home_opened', 'User clicked into your App Home'); Slack.registerEvent('app_mention','Subscribe to only the message events that mention your app or bot'); Slack.registerEvent('app_rate_limited','Indicates your app\'s event subscriptions are being rate limited'); Slack.registerEvent('app_requested','User requested an app'); Slack.registerEvent('app_uninstalled', 'Your Slack app was uninstalled'); Slack.registerEvent('call_rejected', 'A call was rejected'); Slack.registerEvent('channel_archive', 'A channel was archived'); Slack.registerEvent('channel_created', 'A channel was created'); Slack.registerEvent('channel_deleted', 'A channel was deleted'); Slack.registerEvent('channel_history_changed', 'Bulk updates were made to a channel\'s history'); Slack.registerEvent('channel_left', 'You left a channel'); Slack.registerEvent('channel_rename', 'A channel was renamed'); Slack.registerEvent('channel_shared', 'A channel has been shared with an external workspace'); Slack.registerEvent('channel_unarchive', 'A channel was unarchived'); Slack.registerEvent('channel_unshared', 'A channel has been unshared with an external workspace'); Slack.registerEvent('dnd_updated', 'Do not Disturb settings changed for the current user'); Slack.registerEvent('dnd_updated_user', 'Do not Disturb settings changed for a member'); Slack.registerEvent('email_domain_changed', 'The workspace email domain has changed'); Slack.registerEvent('emoji_changed', 'A custom emoji has been added or changed'); Slack.registerEvent('file_change', 'A file was changed'); Slack.registerEvent('file_comment_added', 'A file comment was added'); Slack.registerEvent('file_comment_deleted', 'A file comment was deleted'); Slack.registerEvent('file_comment_edited', 'A file comment was edited'); Slack.registerEvent('file_created', 'A file was created'); Slack.registerEvent('file_deleted', 'A file was deleted'); Slack.registerEvent('file_public', 'A file was made public'); Slack.registerEvent('file_shared', 'A file was shared'); Slack.registerEvent('file_unshared', 'A file was unshared'); Slack.registerEvent('grid_migration_finished', 'An enterprise grid migration has finished on this workspace'); Slack.registerEvent('grid_migration_started', 'An enterprise grid migration has started on this workspace'); Slack.registerEvent('group_archive', 'A private channel was archived'); Slack.registerEvent('group_close', 'You closed a private channel'); Slack.registerEvent('group_deleted', 'A private channel was deleted'); Slack.registerEvent('group_history_changed', 'Bulk updates were made to a private channel\'s history'); Slack.registerEvent('group_left', 'You left a private channel'); Slack.registerEvent('group_open', 'You created a group DM'); Slack.registerEvent('group_rename', 'A private channel was renamed'); Slack.registerEvent('group_unarchive', 'A private channel was unarchived'); Slack.registerEvent('im_close', 'You closed a DM'); Slack.registerEvent('im_created', 'A DM was created'); Slack.registerEvent('im_history_changed', 'Bulk updates were made to a DM\'s history'); Slack.registerEvent('im_open', 'You opened a DM'); Slack.registerEvent('invite_requested', 'User requested an invite'); Slack.registerEvent('link_shared', 'A message was posted containing one or more links relevant to your application'); Slack.registerEvent('member_joined_channel', 'A user joined a public or private channel'); Slack.registerEvent('member_left_channel', 'A user left a public or private channel'); Slack.registerEvent('message', 'A message was sent to a channel'); Slack.registerEvent('message.app_home', 'A user sent a message to your Slack app'); Slack.registerEvent('message.channels', 'A message was posted to a channel'); Slack.registerEvent('message.groups', 'A message was posted to a private channel'); Slack.registerEvent('message.im', 'A message was posted in a direct message channel'); Slack.registerEvent('message.mpim', 'A message was posted in a multiparty direct message channel'); Slack.registerEvent('pin_added', 'A pin was added to a channel'); Slack.registerEvent('pin_removed', 'A pin was removed from a channel'); Slack.registerEvent('reaction_added', 'A member has added an emoji reaction to an item'); Slack.registerEvent('reaction_removed', 'A member removed an emoji reaction'); Slack.registerEvent('resources_added', 'Access to a set of resources was granted for your app'); Slack.registerEvent('resources_removed', 'Access to a set of resources was removed for your app'); Slack.registerEvent('scope_denied', 'OAuth scopes were denied to your app'); Slack.registerEvent('scope_granted', 'OAuth scopes were granted to your app'); Slack.registerEvent('star_added', 'A member has starred an item'); Slack.registerEvent('star_removed', 'A member removed a star'); Slack.registerEvent('subteam_created', 'A User Group has been added to the workspace'); Slack.registerEvent('subteam_members_changed', 'The membership of an existing User Group has changed'); Slack.registerEvent('subteam_self_added', 'You have been added to a User Group'); Slack.registerEvent('subteam_self_removed', 'You have been removed from a User Group'); Slack.registerEvent('subteam_updated', 'An existing User Group has been updated or its members changed'); Slack.registerEvent('team_domain_change', 'The workspace domain has changed'); Slack.registerEvent('team_join', 'A new member has joined'); Slack.registerEvent('team_rename', 'The workspace name has changed'); Slack.registerEvent('tokens_revoked', 'API tokens for your app were revoked'); Slack.registerEvent('url_verification', 'Verifies ownership of an Events API Request URL'); Slack.registerEvent('user_change', 'A member\'s data has changed'); Slack.registerEvent('user_resource_denied', 'User resource was denied to your app'); Slack.registerEvent('user_resource_granted', 'User resource was granted to your app'); Slack.registerEvent('user_resource_removed', 'User resource was removed from your app'); Slack.registerParam( 'username', 'string', { label: 'Username', description: 'Override default chatbot username (requires chat:write.customize permission)', placeholder: 'username' } ); Slack.registerParam( 'icon_emoji', 'string', { label: 'Icon Emoji', description: 'Override default chatbot icon (requires chat:write.customize permission)', placeholder: ':+1:' } ); Slack.registerParam( 'markdown', 'boolean', { label: 'Markdown', default: true, description: 'Render message with Markdown' } ); Slack.registerParam( 'deleteMessageId', 'string', { label: 'Delete Message', description: 'Delete a message given the message id', placeholder: 'Message Id', suggestions: [ '{{outboundMessageId}}', '{{inboundMessageId}}' ] } ); module.exports = Slack;