import { BINARY_ENCODING, IExecuteFunctions, } from 'n8n-core'; import { IDataObject, INodeTypeDescription, INodeExecutionData, INodeType, ILoadOptionsFunctions, INodePropertyOptions, } from 'n8n-workflow'; import { channelFields, channelOperations, } from './ChannelDescription'; import { messageFields, messageOperations, } from './MessageDescription'; import { starFields, starOperations, } from './StarDescription'; import { fileFields, fileOperations, } from './FileDescription'; import { slackApiRequest, slackApiRequestAllItems, validateJSON, } from './GenericFunctions'; import { IAttachment, } from './MessageInterface'; interface Attachment { fields: { item?: object[]; }; } interface Text { type?: string; text?: string; emoji?: boolean; verbatim?: boolean; } interface Confirm { title?: Text; text?: Text; confirm?: Text; deny?: Text; style?: string; } interface Element { type?: string; text?: Text; action_id?: string; url?: string; value?: string; style?: string; confirm?: Confirm; } interface Block { type?: string; elements?: Element[]; block_id?: string; text?: Text; fields?: Text[]; accessory?: Element; } export class Slack implements INodeType { description: INodeTypeDescription = { displayName: 'Slack', name: 'slack', icon: 'file:slack.png', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Consume Slack API', defaults: { name: 'Slack', color: '#BB2244', }, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'slackApi', required: true, displayOptions: { show: { authentication: [ 'accessToken', ], }, }, }, { name: 'slackOAuth2Api', required: true, displayOptions: { show: { authentication: [ 'oAuth2', ], }, }, }, ], properties: [ { displayName: 'Authentication', name: 'authentication', type: 'options', options: [ { name: 'Access Token', value: 'accessToken', }, { name: 'OAuth2', value: 'oAuth2', }, ], default: 'accessToken', description: 'The resource to operate on.', }, { displayName: 'Resource', name: 'resource', type: 'options', options: [ { name: 'Channel', value: 'channel', }, { name: 'File', value: 'file', }, { name: 'Message', value: 'message', }, { name: 'Star', value: 'star', }, ], default: 'message', description: 'The resource to operate on.', }, ...channelOperations, ...channelFields, ...messageOperations, ...messageFields, ...starOperations, ...starFields, ...fileOperations, ...fileFields, ], }; methods = { loadOptions: { // Get all the users to display them to user so that he can // select them easily async getUsers(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; const users = await slackApiRequestAllItems.call(this, 'members', 'GET', '/users.list'); for (const user of users) { const userName = user.name; const userId = user.id; returnData.push({ name: userName, value: userId, }); } returnData.sort((a, b) => { if (a.name < b.name) { return -1; } if (a.name > b.name) { return 1; } return 0; }); return returnData; }, // Get all the users to display them to user so that he can // select them easily async getChannels(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; const channels = await slackApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list'); for (const channel of channels) { const channelName = channel.name; const channelId = channel.id; returnData.push({ name: channelName, value: channelId, }); } returnData.sort((a, b) => { if (a.name < b.name) { return -1; } if (a.name > b.name) { return 1; } return 0; }); return returnData; }, } }; async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; const length = items.length as unknown as number; let qs: IDataObject; let responseData; const authentication = this.getNodeParameter('authentication', 0) as string; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; for (let i = 0; i < length; i++) { qs = {}; if (resource === 'channel') { //https://api.slack.com/methods/conversations.archive if (operation === 'archive') { const channel = this.getNodeParameter('channelId', i) as string; const body: IDataObject = { channel, }; responseData = await slackApiRequest.call(this, 'POST', '/conversations.archive', body, qs); } //https://api.slack.com/methods/conversations.close if (operation === 'close') { const channel = this.getNodeParameter('channelId', i) as string; const body: IDataObject = { channel, }; responseData = await slackApiRequest.call(this, 'POST', '/conversations.close', body, qs); } //https://api.slack.com/methods/conversations.create if (operation === 'create') { const channel = this.getNodeParameter('channelId', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const body: IDataObject = { name: channel, }; if (additionalFields.isPrivate) { body.is_private = additionalFields.isPrivate as boolean; } if (additionalFields.users) { body.user_ids = (additionalFields.users as string[]).join(','); } responseData = await slackApiRequest.call(this, 'POST', '/conversations.create', body, qs); responseData = responseData.channel; } //https://api.slack.com/methods/conversations.kick if (operation === 'kick') { const channel = this.getNodeParameter('channelId', i) as string; const userId = this.getNodeParameter('userId', i) as string; const body: IDataObject = { name: channel, user: userId, }; responseData = await slackApiRequest.call(this, 'POST', '/conversations.kick', body, qs); } //https://api.slack.com/methods/conversations.join if (operation === 'join') { const channel = this.getNodeParameter('channelId', i) as string; const body: IDataObject = { channel, }; responseData = await slackApiRequest.call(this, 'POST', '/conversations.join', body, qs); responseData = responseData.channel; } //https://api.slack.com/methods/conversations.info if (operation === 'get') { const channel = this.getNodeParameter('channelId', i) as string; qs.channel = channel, responseData = await slackApiRequest.call(this, 'POST', '/conversations.info', {}, qs); responseData = responseData.channel; } //https://api.slack.com/methods/conversations.list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const filters = this.getNodeParameter('filters', i) as IDataObject; if (filters.types) { qs.types = (filters.types as string[]).join(','); } if (filters.excludeArchived) { qs.exclude_archived = filters.excludeArchived as boolean; } if (returnAll === true) { responseData = await slackApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/conversations.list', {}, qs); responseData = responseData.channels; } } //https://api.slack.com/methods/conversations.history if (operation === 'history') { const channel = this.getNodeParameter('channelId', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const filters = this.getNodeParameter('filters', i) as IDataObject; qs.channel = channel; if (filters.inclusive) { qs.inclusive = filters.inclusive as boolean; } if (filters.latest) { qs.latest = filters.latest as string; } if (filters.oldest) { qs.oldest = filters.oldest as string; } if (returnAll === true) { responseData = await slackApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.history', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/conversations.history', {}, qs); responseData = responseData.messages; } } //https://api.slack.com/methods/conversations.invite if (operation === 'invite') { const channel = this.getNodeParameter('channelId', i) as string; const userId = this.getNodeParameter('userId', i) as string; const body: IDataObject = { channel, user: userId, }; responseData = await slackApiRequest.call(this, 'POST', '/conversations.invite', body, qs); responseData = responseData.channel; } //https://api.slack.com/methods/conversations.leave if (operation === 'leave') { const channel = this.getNodeParameter('channelId', i) as string; const body: IDataObject = { channel, }; responseData = await slackApiRequest.call(this, 'POST', '/conversations.leave', body, qs); } //https://api.slack.com/methods/conversations.open if (operation === 'open') { const options = this.getNodeParameter('options', i) as IDataObject; const body: IDataObject = {}; if (options.channelId) { body.channel = options.channelId as string; } if (options.returnIm) { body.return_im = options.returnIm as boolean; } if (options.users) { body.users = (options.users as string[]).join(','); } responseData = await slackApiRequest.call(this, 'POST', '/conversations.open', body, qs); responseData = responseData.channel; } //https://api.slack.com/methods/conversations.rename if (operation === 'rename') { const channel = this.getNodeParameter('channelId', i) as IDataObject; const name = this.getNodeParameter('name', i) as IDataObject; const body: IDataObject = { channel, name, }; responseData = await slackApiRequest.call(this, 'POST', '/conversations.rename', body, qs); responseData = responseData.channel; } //https://api.slack.com/methods/conversations.replies if (operation === 'replies') { const channel = this.getNodeParameter('channelId', i) as string; const ts = this.getNodeParameter('ts', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const filters = this.getNodeParameter('filters', i) as IDataObject; qs.channel = channel; qs.ts = ts; if (filters.inclusive) { qs.inclusive = filters.inclusive as boolean; } if (filters.latest) { qs.latest = filters.latest as string; } if (filters.oldest) { qs.oldest = filters.oldest as string; } if (returnAll === true) { responseData = await slackApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.replies', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/conversations.replies', {}, qs); responseData = responseData.messages; } } //https://api.slack.com/methods/conversations.setPurpose if (operation === 'setPurpose') { const channel = this.getNodeParameter('channelId', i) as IDataObject; const purpose = this.getNodeParameter('purpose', i) as IDataObject; const body: IDataObject = { channel, purpose, }; responseData = await slackApiRequest.call(this, 'POST', '/conversations.setPurpose', body, qs); responseData = responseData.channel; } //https://api.slack.com/methods/conversations.setTopic if (operation === 'setTopic') { const channel = this.getNodeParameter('channelId', i) as IDataObject; const topic = this.getNodeParameter('topic', i) as IDataObject; const body: IDataObject = { channel, topic, }; responseData = await slackApiRequest.call(this, 'POST', '/conversations.setTopic', body, qs); responseData = responseData.channel; } //https://api.slack.com/methods/conversations.unarchive if (operation === 'unarchive') { const channel = this.getNodeParameter('channelId', i) as string; const body: IDataObject = { channel, }; responseData = await slackApiRequest.call(this, 'POST', '/conversations.unarchive', body, qs); } } if (resource === 'message') { //https://api.slack.com/methods/chat.postMessage if (operation === 'post') { const channel = this.getNodeParameter('channel', i) as string; const text = this.getNodeParameter('text', i) as string; const body: IDataObject = { channel, text, }; const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; if (authentication === 'accessToken') { body.as_user = this.getNodeParameter('as_user', i) as boolean; } if (body.as_user === false) { body.username = this.getNodeParameter('username', i) as string; } if (!jsonParameters) { const attachments = this.getNodeParameter('attachments', i, []) as unknown as Attachment[]; const blocksUi = (this.getNodeParameter('blocksUi', i, []) as IDataObject).blocksValues as IDataObject[]; // The node does save the fields data differently than the API // expects so fix the data befre we send the request for (const attachment of attachments) { if (attachment.fields !== undefined) { if (attachment.fields.item !== undefined) { // Move the field-content up // @ts-ignore attachment.fields = attachment.fields.item; } else { // If it does not have any items set remove it delete attachment.fields; } } } body['attachments'] = attachments; if (blocksUi) { const blocks: Block[] = []; for (const blockUi of blocksUi) { const block: Block = {}; const elements: Element[] = []; block.block_id = blockUi.blockId as string; block.type = blockUi.type as string; if (block.type === 'actions') { const elementsUi = (blockUi.elementsUi as IDataObject).elementsValues as IDataObject[]; if (elementsUi) { for (const elementUi of elementsUi) { const element: Element = {}; if (elementUi.actionId === '') { throw new Error('Action ID must be set'); } if (elementUi.text === '') { throw new Error('Text must be set'); } element.action_id = elementUi.actionId as string; element.type = elementUi.type as string; element.text = { text: elementUi.text as string, type: 'plain_text', emoji: elementUi.emoji as boolean, }; if (elementUi.url) { element.url = elementUi.url as string; } if (elementUi.value) { element.value = elementUi.value as string; } if (elementUi.style !== 'default') { element.style = elementUi.style as string; } const confirmUi = (elementUi.confirmUi as IDataObject).confirmValue as IDataObject; if (confirmUi) { const confirm: Confirm = {}; const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject; const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject; const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject; const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject; const style = confirmUi.style as string; if (titleUi) { confirm.title = { type: 'plain_text', text: titleUi.text as string, emoji: titleUi.emoji as boolean, }; } if (textUi) { confirm.text = { type: 'plain_text', text: textUi.text as string, emoji: textUi.emoji as boolean, }; } if (confirmTextUi) { confirm.confirm = { type: 'plain_text', text: confirmTextUi.text as string, emoji: confirmTextUi.emoji as boolean, }; } if (denyUi) { confirm.deny = { type: 'plain_text', text: denyUi.text as string, emoji: denyUi.emoji as boolean, }; } if (style !== 'default') { confirm.style = style as string; } element.confirm = confirm; } elements.push(element); } block.elements = elements; } } else if (block.type === 'section') { const textUi = (blockUi.textUi as IDataObject).textValue as IDataObject; if (textUi) { const text: Text = {}; if (textUi.type === 'plainText') { text.type = 'plain_text'; text.emoji = textUi.emoji as boolean; } else { text.type = 'mrkdwn'; text.verbatim = textUi.verbatim as boolean; } text.text = textUi.text as string; block.text = text; } else { throw new Error('Property text must be defined'); } const fieldsUi = (blockUi.fieldsUi as IDataObject).fieldsValues as IDataObject[]; if (fieldsUi) { const fields: Text[] = []; for (const fieldUi of fieldsUi) { const field: Text = {}; if (fieldUi.type === 'plainText') { field.type = 'plain_text'; field.emoji = fieldUi.emoji as boolean; } else { field.type = 'mrkdwn'; field.verbatim = fieldUi.verbatim as boolean; } field.text = fieldUi.text as string; fields.push(field); } // If not fields were added then it's not needed to send the property if (fields.length > 0) { block.fields = fields; } } const accessoryUi = (blockUi.accessoryUi as IDataObject).accessoriesValues as IDataObject; if (accessoryUi) { const accessory: Element = {}; if (accessoryUi.type === 'button') { accessory.type = 'button'; accessory.text = { text: accessoryUi.text as string, type: 'plain_text', emoji: accessoryUi.emoji as boolean, }; if (accessoryUi.url) { accessory.url = accessoryUi.url as string; } if (accessoryUi.value) { accessory.value = accessoryUi.value as string; } if (accessoryUi.style !== 'default') { accessory.style = accessoryUi.style as string; } const confirmUi = (accessoryUi.confirmUi as IDataObject).confirmValue as IDataObject; if (confirmUi) { const confirm: Confirm = {}; const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject; const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject; const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject; const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject; const style = confirmUi.style as string; if (titleUi) { confirm.title = { type: 'plain_text', text: titleUi.text as string, emoji: titleUi.emoji as boolean, }; } if (textUi) { confirm.text = { type: 'plain_text', text: textUi.text as string, emoji: textUi.emoji as boolean, }; } if (confirmTextUi) { confirm.confirm = { type: 'plain_text', text: confirmTextUi.text as string, emoji: confirmTextUi.emoji as boolean, }; } if (denyUi) { confirm.deny = { type: 'plain_text', text: denyUi.text as string, emoji: denyUi.emoji as boolean, }; } if (style !== 'default') { confirm.style = style as string; } accessory.confirm = confirm; } } block.accessory = accessory; } } blocks.push(block); } body.blocks = blocks; } } else { const attachmentsJson = this.getNodeParameter('attachmentsJson', i, []) as string; const blocksJson = this.getNodeParameter('blocksJson', i, []) as string; if (attachmentsJson !== '' && validateJSON(attachmentsJson) === undefined) { throw new Error('Attachments it is not a valid json'); } if (blocksJson !== '' && validateJSON(blocksJson) === undefined) { throw new Error('Blocks it is not a valid json'); } if (attachmentsJson !== '') { body.attachments = attachmentsJson; } if (blocksJson !== '') { body.blocks = blocksJson; } } // Add all the other options to the request const otherOptions = this.getNodeParameter('otherOptions', i) as IDataObject; Object.assign(body, otherOptions); responseData = await slackApiRequest.call(this, 'POST', '/chat.postMessage', body, qs); } //https://api.slack.com/methods/chat.update if (operation === 'update') { const channel = this.getNodeParameter('channelId', i) as string; const text = this.getNodeParameter('text', i) as string; const ts = this.getNodeParameter('ts', i) as string; const attachments = this.getNodeParameter('attachments', i, []) as unknown as IAttachment[]; const body: IDataObject = { channel, text, ts, }; if (authentication === 'accessToken') { body.as_user = this.getNodeParameter('as_user', i) as boolean; } // The node does save the fields data differently than the API // expects so fix the data befre we send the request for (const attachment of attachments) { if (attachment.fields !== undefined) { if (attachment.fields.item !== undefined) { // Move the field-content up // @ts-ignore attachment.fields = attachment.fields.item; } else { // If it does not have any items set remove it delete attachment.fields; } } } body['attachments'] = attachments; // Add all the other options to the request const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; Object.assign(body, updateFields); responseData = await slackApiRequest.call(this, 'POST', '/chat.update', body, qs); } } if (resource === 'star') { //https://api.slack.com/methods/stars.add if (operation === 'add') { const options = this.getNodeParameter('options', i) as IDataObject; const body: IDataObject = {}; if (options.channelId) { body.channel = options.channelId as string; } if (options.fileId) { body.file = options.fileId as string; } if (options.fileComment) { body.file_comment = options.fileComment as string; } if (options.timestamp) { body.timestamp = options.timestamp as string; } responseData = await slackApiRequest.call(this, 'POST', '/stars.add', body, qs); } //https://api.slack.com/methods/stars.remove if (operation === 'delete') { const options = this.getNodeParameter('options', i) as IDataObject; const body: IDataObject = {}; if (options.channelId) { body.channel = options.channelId as string; } if (options.fileId) { body.file = options.fileId as string; } if (options.fileComment) { body.file_comment = options.fileComment as string; } if (options.timestamp) { body.timestamp = options.timestamp as string; } responseData = await slackApiRequest.call(this, 'POST', '/stars.remove', body, qs); } //https://api.slack.com/methods/stars.list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; if (returnAll === true) { responseData = await slackApiRequestAllItems.call(this, 'items', 'GET', '/stars.list', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/stars.list', {}, qs); responseData = responseData.items; } } } if (resource === 'file') { //https://api.slack.com/methods/files.upload if (operation === 'upload') { const options = this.getNodeParameter('options', i) as IDataObject; const binaryData = this.getNodeParameter('binaryData', i) as boolean; const body: IDataObject = {}; if (options.channelIds) { body.channels = (options.channelIds as string[]).join(','); } if (options.fileName) { body.filename = options.fileName as string; } if (options.initialComment) { body.initial_comment = options.initialComment as string; } if (options.threadTs) { body.thread_ts = options.threadTs as string; } if (options.title) { body.title = options.title as string; } if (binaryData) { const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; if (items[i].binary === undefined //@ts-ignore || items[i].binary[binaryPropertyName] === undefined) { throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); } body.file = { //@ts-ignore value: Buffer.from(items[i].binary[binaryPropertyName].data, BINARY_ENCODING), options: { //@ts-ignore filename: items[i].binary[binaryPropertyName].fileName, //@ts-ignore contentType: items[i].binary[binaryPropertyName].mimeType, } }; responseData = await slackApiRequest.call(this, 'POST', '/files.upload', {}, qs, { 'Content-Type': 'multipart/form-data' }, { formData: body }); responseData = responseData.file; } else { const fileContent = this.getNodeParameter('fileContent', i) as string; body.content = fileContent; responseData = await slackApiRequest.call(this, 'POST', '/files.upload', body, qs, { 'Content-Type': 'application/x-www-form-urlencoded' }, { form: body }); responseData = responseData.file; } } //https://api.slack.com/methods/files.list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const filters = this.getNodeParameter('filters', i) as IDataObject; if (filters.channelId) { qs.channel = filters.channelId as string; } if (filters.showFilesHidden) { qs.show_files_hidden_by_limit = filters.showFilesHidden as boolean; } if (filters.tsFrom) { qs.ts_from = filters.tsFrom as string; } if (filters.tsTo) { qs.ts_to = filters.tsTo as string; } if (filters.types) { qs.types = (filters.types as string[]).join(',') as string; } if (filters.userId) { qs.user = filters.userId as string; } if (returnAll === true) { responseData = await slackApiRequestAllItems.call(this, 'files', 'GET', '/files.list', {}, qs); } else { qs.count = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/files.list', {}, qs); responseData = responseData.files; } } //https://api.slack.com/methods/files.info if (operation === 'get') { const fileId = this.getNodeParameter('fileId', i) as string; qs.file = fileId; responseData = await slackApiRequest.call(this, 'GET', '/files.info', {}, qs); responseData = responseData.file; } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else { returnData.push(responseData as IDataObject); } } return [this.helpers.returnJsonArray(returnData)]; } }