diff --git a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts index 1ad0ab1278..8aef0a103e 100644 --- a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts @@ -68,5 +68,12 @@ export class SlackOAuth2Api implements ICredentialType { type: 'hidden', default: 'body', }, + { + displayName: + 'If you get an Invalid Scopes error, make sure you add the correct one here to your Slack integration', + name: 'notice', + type: 'notice', + default: '', + }, ]; } diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index 12a4524dd9..febdacc7f3 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -1,1391 +1,27 @@ -import type { IExecuteFunctions } from 'n8n-core'; - -import type { - IDataObject, - ILoadOptionsFunctions, - INodeExecutionData, - INodePropertyOptions, - INodeType, - INodeTypeDescription, - JsonObject, -} from 'n8n-workflow'; -import { NodeOperationError } from 'n8n-workflow'; - -import { channelFields, channelOperations } from './ChannelDescription'; -import { messageFields, messageOperations } from './MessageDescription'; -import { starFields, starOperations } from './StarDescription'; -import { fileFields, fileOperations } from './FileDescription'; -import { reactionFields, reactionOperations } from './ReactionDescription'; -import { userGroupFields, userGroupOperations } from './UserGroupDescription'; -import { userFields, userOperations } from './UserDescription'; -import { userProfileFields, userProfileOperations } from './UserProfileDescription'; -import { slackApiRequest, slackApiRequestAllItems, validateJSON } from './GenericFunctions'; -import type { IAttachment } from './MessageInterface'; - -import moment from 'moment'; - -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.svg', - group: ['output'], - version: 1, - subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Slack API', - defaults: { - name: 'Slack', - }, - 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', - }, - - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Channel', - value: 'channel', - }, - { - name: 'File', - value: 'file', - }, - { - name: 'Message', - value: 'message', - }, - { - name: 'Reaction', - value: 'reaction', - }, - { - name: 'Star', - value: 'star', - }, - { - name: 'User', - value: 'user', - }, - { - name: 'User Group', - value: 'userGroup', - }, - { - name: 'User Profile', - value: 'userProfile', - }, - ], - default: 'message', - }, - - ...channelOperations, - ...channelFields, - ...messageOperations, - ...messageFields, - ...starOperations, - ...starFields, - ...fileOperations, - ...fileFields, - ...reactionOperations, - ...reactionFields, - ...userOperations, - ...userFields, - ...userGroupOperations, - ...userGroupFields, - ...userProfileOperations, - ...userProfileFields, - ], - }; - - 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 qs = { types: 'public_channel,private_channel', limit: 1000 }; - const channels = await slackApiRequestAllItems.call( - this, - 'channels', - 'GET', - '/conversations.list', - {}, - qs, - ); - 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; - }, - // Get all the team fields to display them to user so that he can - // select them easily - async getTeamFields(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const { - profile: { fields }, - } = await slackApiRequest.call(this, 'GET', '/team.profile.get'); - for (const field of fields) { - const fieldName = field.label; - const fieldId = field.id; - returnData.push({ - name: fieldName, - value: fieldId, - }); - } - return returnData; - }, - }, - }; - - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; - const length = items.length; - let qs: IDataObject; - let responseData; - const authentication = this.getNodeParameter('authentication', 0) as string; - const resource = this.getNodeParameter('resource', 0); - const operation = this.getNodeParameter('operation', 0); - - for (let i = 0; i < length; i++) { - try { - responseData = { - error: 'Resource ' + resource + ' / operation ' + operation + ' not found!', - }; - 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); - const body: IDataObject = { - name: channel, - }; - if (additionalFields.isPrivate) { - body.is_private = additionalFields.isPrivate as boolean; - } - 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 = { - 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); - const filters = this.getNodeParameter('filters', i); - if (filters.types) { - qs.types = (filters.types as string[]).join(','); - } - if (filters.excludeArchived) { - qs.exclude_archived = filters.excludeArchived as boolean; - } - if (returnAll) { - responseData = await slackApiRequestAllItems.call( - this, - 'channels', - 'GET', - '/conversations.list', - {}, - qs, - ); - } else { - qs.limit = this.getNodeParameter('limit', i); - 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); - const filters = this.getNodeParameter('filters', i); - qs.channel = channel; - if (filters.inclusive) { - qs.inclusive = filters.inclusive as boolean; - } - if (filters.latest) { - qs.latest = new Date(filters.latest as string).getTime() / 1000; - } - if (filters.oldest) { - qs.oldest = new Date(filters.oldest as string).getTime() / 1000; - } - if (returnAll) { - responseData = await slackApiRequestAllItems.call( - this, - 'messages', - 'GET', - '/conversations.history', - {}, - qs, - ); - } else { - qs.limit = this.getNodeParameter('limit', i); - 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 userIds = (this.getNodeParameter('userIds', i) as string[]).join(','); - const body: IDataObject = { - channel, - users: userIds, - }; - 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.members - if (operation === 'member') { - const returnAll = this.getNodeParameter('returnAll', 0); - const resolveData = this.getNodeParameter('resolveData', 0); - qs.channel = this.getNodeParameter('channelId', i) as string; - if (returnAll) { - responseData = await slackApiRequestAllItems.call( - this, - 'members', - 'GET', - '/conversations.members', - {}, - qs, - ); - responseData = responseData.map((member: string) => ({ member })); - } else { - qs.limit = this.getNodeParameter('limit', i); - responseData = await slackApiRequest.call( - this, - 'GET', - '/conversations.members', - {}, - qs, - ); - responseData = responseData.members.map((member: string) => ({ member })); - } - - if (resolveData) { - const data: IDataObject[] = []; - for (const { member } of responseData) { - const { user } = await slackApiRequest.call( - this, - 'GET', - '/users.info', - {}, - { user: member }, - ); - data.push(user); - } - responseData = data; - } - } - //https://api.slack.com/methods/conversations.open - if (operation === 'open') { - const options = this.getNodeParameter('options', i); - 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); - const filters = this.getNodeParameter('filters', i); - qs.channel = channel; - qs.ts = ts; - if (filters.inclusive) { - qs.inclusive = filters.inclusive as boolean; - } - if (filters.latest) { - qs.latest = new Date(filters.latest as string).getTime() / 1000; - } - if (filters.oldest) { - qs.oldest = new Date(filters.oldest as string).getTime() / 1000; - } - if (returnAll) { - responseData = await slackApiRequestAllItems.call( - this, - 'messages', - 'GET', - '/conversations.replies', - {}, - qs, - ); - } else { - qs.limit = this.getNodeParameter('limit', i); - 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 (['post', 'postEphemeral'].includes(operation)) { - const channel = this.getNodeParameter('channel', i) as string; - const { sendAsUser } = this.getNodeParameter('otherOptions', i) as IDataObject; - const text = this.getNodeParameter('text', i) as string; - const body: IDataObject = { - channel, - text, - }; - - let action = 'postMessage'; - - if (operation === 'postEphemeral') { - body.user = this.getNodeParameter('user', i) as string; - action = 'postEphemeral'; - } - - const jsonParameters = this.getNodeParameter('jsonParameters', i); - - if (authentication === 'accessToken' && sendAsUser !== '') { - body.username = sendAsUser; - } - - 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 - // @ts-ignore - 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 NodeOperationError(this.getNode(), 'Action ID must be set', { - itemIndex: i, - }); - } - if (elementUi.text === '') { - throw new NodeOperationError(this.getNode(), 'Text must be set', { - itemIndex: i, - }); - } - 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; - } - 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 textData: Text = {}; - if (textUi.type === 'plainText') { - textData.type = 'plain_text'; - textData.emoji = textUi.emoji as boolean; - } else { - textData.type = 'mrkdwn'; - textData.verbatim = textUi.verbatim as boolean; - } - textData.text = textUi.text as string; - block.text = textData; - } else { - throw new NodeOperationError( - this.getNode(), - 'Property text must be defined', - { itemIndex: i }, - ); - } - 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 textUiFromConfirm = (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 (textUiFromConfirm) { - confirm.text = { - type: 'plain_text', - text: textUiFromConfirm.text as string, - emoji: textUiFromConfirm.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; - } - 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 NodeOperationError(this.getNode(), 'Attachments it is not a valid json', { - itemIndex: i, - }); - } - if (blocksJson !== '' && validateJSON(blocksJson) === undefined) { - throw new NodeOperationError(this.getNode(), 'Blocks it is not a valid json', { - itemIndex: i, - }); - } - 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.${action}`, 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, - }; - - // 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 - // @ts-ignore - delete attachment.fields; - } - } - } - body.attachments = attachments; - - const jsonParameters = this.getNodeParameter('jsonParameters', i, false); - if (jsonParameters) { - const blocksJson = this.getNodeParameter('blocksJson', i, []) as string; - - if (blocksJson !== '' && validateJSON(blocksJson) === undefined) { - throw new NodeOperationError(this.getNode(), 'Blocks it is not a valid json', { - itemIndex: i, - }); - } - if (blocksJson !== '') { - body.blocks = blocksJson; - } - - const attachmentsJson = this.getNodeParameter('attachmentsJson', i, '') as string; - - if (attachmentsJson !== '' && validateJSON(attachmentsJson) === undefined) { - throw new NodeOperationError(this.getNode(), 'Attachments it is not a valid json', { - itemIndex: i, - }); - } - - if (attachmentsJson !== '') { - body.attachments = attachmentsJson; - } - } - - // Add all the other options to the request - const updateFields = this.getNodeParameter('updateFields', i); - Object.assign(body, updateFields); - responseData = await slackApiRequest.call(this, 'POST', '/chat.update', body, qs); - } - //https://api.slack.com/methods/chat.delete - if (operation === 'delete') { - const channel = this.getNodeParameter('channelId', i) as string; - const timestamp = this.getNodeParameter('timestamp', i) as string; - const body: IDataObject = { - channel, - ts: timestamp, - }; - // Add all the other options to the request - responseData = await slackApiRequest.call(this, 'POST', '/chat.delete', body, qs); - } - //https://api.slack.com/methods/chat.getPermalink - if (operation === 'getPermalink') { - const channel = this.getNodeParameter('channelId', i) as string; - const timestamp = this.getNodeParameter('timestamp', i) as string; - const query = { - channel, - message_ts: timestamp, - }; - responseData = await slackApiRequest.call(this, 'GET', '/chat.getPermalink', {}, query); - } - } - if (resource === 'reaction') { - const channel = this.getNodeParameter('channelId', i) as string; - const timestamp = this.getNodeParameter('timestamp', i) as string; - //https://api.slack.com/methods/reactions.add - if (operation === 'add') { - const name = this.getNodeParameter('name', i) as string; - const body: IDataObject = { - channel, - name, - timestamp, - }; - responseData = await slackApiRequest.call(this, 'POST', '/reactions.add', body, qs); - } - //https://api.slack.com/methods/reactions.remove - if (operation === 'remove') { - const name = this.getNodeParameter('name', i) as string; - const body: IDataObject = { - channel, - name, - timestamp, - }; - responseData = await slackApiRequest.call(this, 'POST', '/reactions.remove', body, qs); - } - //https://api.slack.com/methods/reactions.get - if (operation === 'get') { - qs.channel = channel; - qs.timestamp = timestamp; - responseData = await slackApiRequest.call(this, 'GET', '/reactions.get', {}, qs); - } - } - if (resource === 'star') { - //https://api.slack.com/methods/stars.add - if (operation === 'add') { - const options = this.getNodeParameter('options', i); - 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); - 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); - if (returnAll) { - responseData = await slackApiRequestAllItems.call( - this, - 'items', - 'GET', - '/stars.list', - {}, - qs, - ); - } else { - qs.limit = this.getNodeParameter('limit', i); - 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); - const binaryData = this.getNodeParameter('binaryData', i); - 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); - if ( - items[i].binary === undefined || - //@ts-ignore - items[i].binary[binaryPropertyName] === undefined - ) { - throw new NodeOperationError( - this.getNode(), - `No binary data property "${binaryPropertyName}" does not exists on item!`, - { itemIndex: i }, - ); - } - const binaryDataBuffer = await this.helpers.getBinaryDataBuffer( - i, - binaryPropertyName, - ); - body.file = { - //@ts-ignore - value: binaryDataBuffer, - 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); - const filters = this.getNodeParameter('filters', i); - 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(','); - } - if (filters.userId) { - qs.user = filters.userId as string; - } - if (returnAll) { - responseData = await slackApiRequestAllItems.call( - this, - 'files', - 'GET', - '/files.list', - {}, - qs, - ); - } else { - qs.count = this.getNodeParameter('limit', i); - 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 (resource === 'user') { - //https://api.slack.com/methods/users.info - if (operation === 'info') { - qs.user = this.getNodeParameter('user', i) as string; - responseData = await slackApiRequest.call(this, 'GET', '/users.info', {}, qs); - responseData = responseData.user; - } - //https://api.slack.com/methods/users.list - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i); - if (returnAll) { - responseData = await slackApiRequestAllItems.call( - this, - 'members', - 'GET', - '/users.list', - {}, - qs, - ); - } else { - qs.limit = this.getNodeParameter('limit', i); - responseData = await slackApiRequest.call(this, 'GET', '/users.list', {}, qs); - responseData = responseData.members; - } - } - //https://api.slack.com/methods/users.getPresence - if (operation === 'getPresence') { - qs.user = this.getNodeParameter('user', i) as string; - responseData = await slackApiRequest.call(this, 'GET', '/users.getPresence', {}, qs); - } - } - if (resource === 'userGroup') { - //https://api.slack.com/methods/usergroups.create - if (operation === 'create') { - const name = this.getNodeParameter('name', i) as string; - - const additionalFields = this.getNodeParameter('additionalFields', i); - - const body: IDataObject = { - name, - }; - - Object.assign(body, additionalFields); - - responseData = await slackApiRequest.call(this, 'POST', '/usergroups.create', body, qs); - - responseData = responseData.usergroup; - } - //https://api.slack.com/methods/usergroups.enable - if (operation === 'enable') { - const userGroupId = this.getNodeParameter('userGroupId', i) as string; - - const additionalFields = this.getNodeParameter('additionalFields', i); - - const body: IDataObject = { - usergroup: userGroupId, - }; - - Object.assign(body, additionalFields); - - responseData = await slackApiRequest.call(this, 'POST', '/usergroups.enable', body, qs); - - responseData = responseData.usergroup; - } - //https://api.slack.com/methods/usergroups.disable - if (operation === 'disable') { - const userGroupId = this.getNodeParameter('userGroupId', i) as string; - - const additionalFields = this.getNodeParameter('additionalFields', i); - - const body: IDataObject = { - usergroup: userGroupId, - }; - - Object.assign(body, additionalFields); - - responseData = await slackApiRequest.call( - this, - 'POST', - '/usergroups.disable', - body, - qs, - ); - - responseData = responseData.usergroup; - } - - //https://api.slack.com/methods/usergroups.list - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i); - - const additionalFields = this.getNodeParameter('additionalFields', i); - - responseData = await slackApiRequest.call( - this, - 'GET', - '/usergroups.list', - {}, - additionalFields, - ); - - responseData = responseData.usergroups; - - if (!returnAll) { - const limit = this.getNodeParameter('limit', i); - - responseData = responseData.slice(0, limit); - } - } - - //https://api.slack.com/methods/usergroups.update - if (operation === 'update') { - const userGroupId = this.getNodeParameter('userGroupId', i) as string; - - const updateFields = this.getNodeParameter('updateFields', i); - - const body: IDataObject = { - usergroup: userGroupId, - }; - - Object.assign(body, updateFields); - - responseData = await slackApiRequest.call(this, 'POST', '/usergroups.update', body, qs); - - responseData = responseData.usergroup; - } - } - if (resource === 'userProfile') { - //https://api.slack.com/methods/users.profile.set - if (operation === 'update') { - const additionalFields = this.getNodeParameter('additionalFields', i); - - const timezone = this.getTimezone(); - - const body: IDataObject = {}; - - Object.assign(body, additionalFields); - - if (body.status_expiration === undefined) { - body.status_expiration = 0; - } else { - body.status_expiration = moment.tz(body.status_expiration as string, timezone).unix(); - } - - if (body.customFieldUi) { - const customFields = (body.customFieldUi as IDataObject) - .customFieldValues as IDataObject[]; - - body.fields = {}; - - for (const customField of customFields) { - //@ts-ignore - body.fields[customField.id] = { - value: customField.value, - alt: customField.alt, - }; - } - } - - responseData = await slackApiRequest.call( - this, - 'POST', - '/users.profile.set', - { profile: body }, - qs, - ); - - responseData = responseData.profile; - } - //https://api.slack.com/methods/users.profile.get - if (operation === 'get') { - const additionalFields = this.getNodeParameter('additionalFields', i); - - responseData = await slackApiRequest.call( - this, - 'POST', - '/users.profile.get', - undefined, - additionalFields, - ); - - responseData = responseData.profile; - } - } - - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - returnData.push({ json: { error: (error as JsonObject).message } }); - continue; - } - throw error; - } - } - return this.prepareOutputData(returnData); +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; + +import { SlackV1 } from './V1/SlackV1.node'; + +import { SlackV2 } from './V2/SlackV2.node'; + +export class Slack extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Slack', + name: 'slack', + icon: 'file:slack.svg', + group: ['output'], + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Slack API', + defaultVersion: 2, + }; + + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new SlackV1(baseDescription), + 2: new SlackV2(baseDescription), + }; + + super(nodeVersions, baseDescription); } } diff --git a/packages/nodes-base/nodes/Slack/ChannelDescription.ts b/packages/nodes-base/nodes/Slack/V1/ChannelDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Slack/ChannelDescription.ts rename to packages/nodes-base/nodes/Slack/V1/ChannelDescription.ts diff --git a/packages/nodes-base/nodes/Slack/FileDescription.ts b/packages/nodes-base/nodes/Slack/V1/FileDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Slack/FileDescription.ts rename to packages/nodes-base/nodes/Slack/V1/FileDescription.ts diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/V1/GenericFunctions.ts similarity index 100% rename from packages/nodes-base/nodes/Slack/GenericFunctions.ts rename to packages/nodes-base/nodes/Slack/V1/GenericFunctions.ts diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/V1/MessageDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Slack/MessageDescription.ts rename to packages/nodes-base/nodes/Slack/V1/MessageDescription.ts diff --git a/packages/nodes-base/nodes/Slack/MessageInterface.ts b/packages/nodes-base/nodes/Slack/V1/MessageInterface.ts similarity index 100% rename from packages/nodes-base/nodes/Slack/MessageInterface.ts rename to packages/nodes-base/nodes/Slack/V1/MessageInterface.ts diff --git a/packages/nodes-base/nodes/Slack/ReactionDescription.ts b/packages/nodes-base/nodes/Slack/V1/ReactionDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Slack/ReactionDescription.ts rename to packages/nodes-base/nodes/Slack/V1/ReactionDescription.ts diff --git a/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts b/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts new file mode 100644 index 0000000000..261129648e --- /dev/null +++ b/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts @@ -0,0 +1,1397 @@ +import type { IExecuteFunctions } from 'n8n-core'; + +import type { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeBaseDescription, + INodeTypeDescription, + JsonObject, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; + +import { channelFields, channelOperations } from './ChannelDescription'; +import { messageFields, messageOperations } from './MessageDescription'; +import { starFields, starOperations } from './StarDescription'; +import { fileFields, fileOperations } from './FileDescription'; +import { reactionFields, reactionOperations } from './ReactionDescription'; +import { userGroupFields, userGroupOperations } from './UserGroupDescription'; +import { userFields, userOperations } from './UserDescription'; +import { userProfileFields, userProfileOperations } from './UserProfileDescription'; +import { slackApiRequest, slackApiRequestAllItems, validateJSON } from './GenericFunctions'; +import type { IAttachment } from './MessageInterface'; + +import moment from 'moment'; + +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 SlackV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + version: 1, + defaults: { + name: 'Slack', + }, + 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', + }, + + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Channel', + value: 'channel', + }, + { + name: 'File', + value: 'file', + }, + { + name: 'Message', + value: 'message', + }, + { + name: 'Reaction', + value: 'reaction', + }, + { + name: 'Star', + value: 'star', + }, + { + name: 'User', + value: 'user', + }, + { + name: 'User Group', + value: 'userGroup', + }, + { + name: 'User Profile', + value: 'userProfile', + }, + ], + default: 'message', + }, + + ...channelOperations, + ...channelFields, + ...messageOperations, + ...messageFields, + ...starOperations, + ...starFields, + ...fileOperations, + ...fileFields, + ...reactionOperations, + ...reactionFields, + ...userOperations, + ...userFields, + ...userGroupOperations, + ...userGroupFields, + ...userProfileOperations, + ...userProfileFields, + ], + }; + } + + 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 qs = { types: 'public_channel,private_channel', limit: 1000 }; + const channels = await slackApiRequestAllItems.call( + this, + 'channels', + 'GET', + '/conversations.list', + {}, + qs, + ); + 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; + }, + // Get all the team fields to display them to user so that he can + // select them easily + async getTeamFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { + profile: { fields }, + } = await slackApiRequest.call(this, 'GET', '/team.profile.get'); + for (const field of fields) { + const fieldName = field.label; + const fieldId = field.id; + returnData.push({ + name: fieldName, + value: fieldId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + const length = items.length; + let qs: IDataObject; + let responseData; + const authentication = this.getNodeParameter('authentication', 0) as string; + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + for (let i = 0; i < length; i++) { + try { + responseData = { + error: 'Resource ' + resource + ' / operation ' + operation + ' not found!', + }; + 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); + const body: IDataObject = { + name: channel, + }; + if (additionalFields.isPrivate) { + body.is_private = additionalFields.isPrivate as boolean; + } + 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 = { + 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); + const filters = this.getNodeParameter('filters', i); + if (filters.types) { + qs.types = (filters.types as string[]).join(','); + } + if (filters.excludeArchived) { + qs.exclude_archived = filters.excludeArchived as boolean; + } + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'channels', + 'GET', + '/conversations.list', + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + 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); + const filters = this.getNodeParameter('filters', i); + qs.channel = channel; + if (filters.inclusive) { + qs.inclusive = filters.inclusive as boolean; + } + if (filters.latest) { + qs.latest = new Date(filters.latest as string).getTime() / 1000; + } + if (filters.oldest) { + qs.oldest = new Date(filters.oldest as string).getTime() / 1000; + } + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'messages', + 'GET', + '/conversations.history', + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + 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 userIds = (this.getNodeParameter('userIds', i) as string[]).join(','); + const body: IDataObject = { + channel, + users: userIds, + }; + 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.members + if (operation === 'member') { + const returnAll = this.getNodeParameter('returnAll', 0); + const resolveData = this.getNodeParameter('resolveData', 0); + qs.channel = this.getNodeParameter('channelId', i) as string; + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'members', + 'GET', + '/conversations.members', + {}, + qs, + ); + responseData = responseData.map((member: string) => ({ member })); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await slackApiRequest.call( + this, + 'GET', + '/conversations.members', + {}, + qs, + ); + responseData = responseData.members.map((member: string) => ({ member })); + } + + if (resolveData) { + const data: IDataObject[] = []; + for (const { member } of responseData) { + const { user } = await slackApiRequest.call( + this, + 'GET', + '/users.info', + {}, + { user: member }, + ); + data.push(user); + } + responseData = data; + } + } + //https://api.slack.com/methods/conversations.open + if (operation === 'open') { + const options = this.getNodeParameter('options', i); + 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); + const filters = this.getNodeParameter('filters', i); + qs.channel = channel; + qs.ts = ts; + if (filters.inclusive) { + qs.inclusive = filters.inclusive as boolean; + } + if (filters.latest) { + qs.latest = new Date(filters.latest as string).getTime() / 1000; + } + if (filters.oldest) { + qs.oldest = new Date(filters.oldest as string).getTime() / 1000; + } + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'messages', + 'GET', + '/conversations.replies', + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + 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 (['post', 'postEphemeral'].includes(operation)) { + const channel = this.getNodeParameter('channel', i) as string; + const { sendAsUser } = this.getNodeParameter('otherOptions', i) as IDataObject; + const text = this.getNodeParameter('text', i) as string; + const body: IDataObject = { + channel, + text, + }; + + let action = 'postMessage'; + + if (operation === 'postEphemeral') { + body.user = this.getNodeParameter('user', i) as string; + action = 'postEphemeral'; + } + + const jsonParameters = this.getNodeParameter('jsonParameters', i); + + if (authentication === 'accessToken' && sendAsUser !== '') { + body.username = sendAsUser; + } + + 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 + // @ts-ignore + 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 NodeOperationError(this.getNode(), 'Action ID must be set', { + itemIndex: i, + }); + } + if (elementUi.text === '') { + throw new NodeOperationError(this.getNode(), 'Text must be set', { + itemIndex: i, + }); + } + 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; + } + 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) { + // eslint-disable-next-line @typescript-eslint/no-shadow + 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 NodeOperationError( + this.getNode(), + 'Property text must be defined', + { itemIndex: i }, + ); + } + 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; + // eslint-disable-next-line @typescript-eslint/no-shadow + 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; + } + 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 NodeOperationError(this.getNode(), 'Attachments it is not a valid json', { + itemIndex: i, + }); + } + if (blocksJson !== '' && validateJSON(blocksJson) === undefined) { + throw new NodeOperationError(this.getNode(), 'Blocks it is not a valid json', { + itemIndex: i, + }); + } + 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.${action}`, 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, + }; + + // 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 + // @ts-ignore + delete attachment.fields; + } + } + } + body.attachments = attachments; + + const jsonParameters = this.getNodeParameter('jsonParameters', i, false); + if (jsonParameters) { + const blocksJson = this.getNodeParameter('blocksJson', i, []) as string; + + if (blocksJson !== '' && validateJSON(blocksJson) === undefined) { + throw new NodeOperationError(this.getNode(), 'Blocks it is not a valid json', { + itemIndex: i, + }); + } + if (blocksJson !== '') { + body.blocks = blocksJson; + } + + const attachmentsJson = this.getNodeParameter('attachmentsJson', i, '') as string; + + if (attachmentsJson !== '' && validateJSON(attachmentsJson) === undefined) { + throw new NodeOperationError(this.getNode(), 'Attachments it is not a valid json', { + itemIndex: i, + }); + } + + if (attachmentsJson !== '') { + body.attachments = attachmentsJson; + } + } + + // Add all the other options to the request + const updateFields = this.getNodeParameter('updateFields', i); + Object.assign(body, updateFields); + responseData = await slackApiRequest.call(this, 'POST', '/chat.update', body, qs); + } + //https://api.slack.com/methods/chat.delete + if (operation === 'delete') { + const channel = this.getNodeParameter('channelId', i) as string; + const timestamp = this.getNodeParameter('timestamp', i) as string; + const body: IDataObject = { + channel, + ts: timestamp, + }; + // Add all the other options to the request + responseData = await slackApiRequest.call(this, 'POST', '/chat.delete', body, qs); + } + //https://api.slack.com/methods/chat.getPermalink + if (operation === 'getPermalink') { + const channel = this.getNodeParameter('channelId', i) as string; + const timestamp = this.getNodeParameter('timestamp', i) as string; + // eslint-disable-next-line @typescript-eslint/no-shadow + const qs = { + channel, + message_ts: timestamp, + }; + responseData = await slackApiRequest.call(this, 'GET', '/chat.getPermalink', {}, qs); + } + } + if (resource === 'reaction') { + const channel = this.getNodeParameter('channelId', i) as string; + const timestamp = this.getNodeParameter('timestamp', i) as string; + //https://api.slack.com/methods/reactions.add + if (operation === 'add') { + const name = this.getNodeParameter('name', i) as string; + const body: IDataObject = { + channel, + name, + timestamp, + }; + responseData = await slackApiRequest.call(this, 'POST', '/reactions.add', body, qs); + } + //https://api.slack.com/methods/reactions.remove + if (operation === 'remove') { + const name = this.getNodeParameter('name', i) as string; + const body: IDataObject = { + channel, + name, + timestamp, + }; + responseData = await slackApiRequest.call(this, 'POST', '/reactions.remove', body, qs); + } + //https://api.slack.com/methods/reactions.get + if (operation === 'get') { + qs.channel = channel; + qs.timestamp = timestamp; + responseData = await slackApiRequest.call(this, 'GET', '/reactions.get', {}, qs); + } + } + if (resource === 'star') { + //https://api.slack.com/methods/stars.add + if (operation === 'add') { + const options = this.getNodeParameter('options', i); + 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); + 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); + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'items', + 'GET', + '/stars.list', + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + 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); + const binaryData = this.getNodeParameter('binaryData', i); + 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); + if ( + items[i].binary === undefined || + //@ts-ignore + items[i].binary[binaryPropertyName] === undefined + ) { + throw new NodeOperationError( + this.getNode(), + `No binary data property "${binaryPropertyName}" does not exists on item!`, + { itemIndex: i }, + ); + } + const binaryDataBuffer = await this.helpers.getBinaryDataBuffer( + i, + binaryPropertyName, + ); + body.file = { + //@ts-ignore + value: binaryDataBuffer, + 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); + const filters = this.getNodeParameter('filters', i); + 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(','); + } + if (filters.userId) { + qs.user = filters.userId as string; + } + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'files', + 'GET', + '/files.list', + {}, + qs, + ); + } else { + qs.count = this.getNodeParameter('limit', i); + 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 (resource === 'user') { + //https://api.slack.com/methods/users.info + if (operation === 'info') { + qs.user = this.getNodeParameter('user', i) as string; + responseData = await slackApiRequest.call(this, 'GET', '/users.info', {}, qs); + responseData = responseData.user; + } + //https://api.slack.com/methods/users.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i); + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'members', + 'GET', + '/users.list', + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await slackApiRequest.call(this, 'GET', '/users.list', {}, qs); + responseData = responseData.members; + } + } + //https://api.slack.com/methods/users.getPresence + if (operation === 'getPresence') { + qs.user = this.getNodeParameter('user', i) as string; + responseData = await slackApiRequest.call(this, 'GET', '/users.getPresence', {}, qs); + } + } + if (resource === 'userGroup') { + //https://api.slack.com/methods/usergroups.create + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i); + + const body: IDataObject = { + name, + }; + + Object.assign(body, additionalFields); + + responseData = await slackApiRequest.call(this, 'POST', '/usergroups.create', body, qs); + + responseData = responseData.usergroup; + } + //https://api.slack.com/methods/usergroups.enable + if (operation === 'enable') { + const userGroupId = this.getNodeParameter('userGroupId', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i); + + const body: IDataObject = { + usergroup: userGroupId, + }; + + Object.assign(body, additionalFields); + + responseData = await slackApiRequest.call(this, 'POST', '/usergroups.enable', body, qs); + + responseData = responseData.usergroup; + } + //https://api.slack.com/methods/usergroups.disable + if (operation === 'disable') { + const userGroupId = this.getNodeParameter('userGroupId', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i); + + const body: IDataObject = { + usergroup: userGroupId, + }; + + Object.assign(body, additionalFields); + + responseData = await slackApiRequest.call( + this, + 'POST', + '/usergroups.disable', + body, + qs, + ); + + responseData = responseData.usergroup; + } + + //https://api.slack.com/methods/usergroups.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i); + + const additionalFields = this.getNodeParameter('additionalFields', i); + + // eslint-disable-next-line @typescript-eslint/no-shadow + const qs: IDataObject = {}; + + Object.assign(qs, additionalFields); + + responseData = await slackApiRequest.call(this, 'GET', '/usergroups.list', {}, qs); + + responseData = responseData.usergroups; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i); + + responseData = responseData.slice(0, limit); + } + } + + //https://api.slack.com/methods/usergroups.update + if (operation === 'update') { + const userGroupId = this.getNodeParameter('userGroupId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i); + + const body: IDataObject = { + usergroup: userGroupId, + }; + + Object.assign(body, updateFields); + + responseData = await slackApiRequest.call(this, 'POST', '/usergroups.update', body, qs); + + responseData = responseData.usergroup; + } + } + if (resource === 'userProfile') { + //https://api.slack.com/methods/users.profile.set + if (operation === 'update') { + const additionalFields = this.getNodeParameter('additionalFields', i); + + const timezone = this.getTimezone(); + + const body: IDataObject = {}; + + Object.assign(body, additionalFields); + + if (body.status_expiration === undefined) { + body.status_expiration = 0; + } else { + body.status_expiration = moment.tz(body.status_expiration as string, timezone).unix(); + } + + if (body.customFieldUi) { + const customFields = (body.customFieldUi as IDataObject) + .customFieldValues as IDataObject[]; + + body.fields = {}; + + for (const customField of customFields) { + //@ts-ignore + body.fields[customField.id] = { + value: customField.value, + alt: customField.alt, + }; + } + } + + responseData = await slackApiRequest.call( + this, + 'POST', + '/users.profile.set', + { profile: body }, + qs, + ); + + responseData = responseData.profile; + } + //https://api.slack.com/methods/users.profile.get + if (operation === 'get') { + const additionalFields = this.getNodeParameter('additionalFields', i); + + // eslint-disable-next-line @typescript-eslint/no-shadow + const qs: IDataObject = {}; + + Object.assign(qs, additionalFields); + + responseData = await slackApiRequest.call( + this, + 'POST', + '/users.profile.get', + undefined, + qs, + ); + + responseData = responseData.profile; + } + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { error: (error as JsonObject).message } }); + continue; + } + throw error; + } + } + return this.prepareOutputData(returnData); + } +} diff --git a/packages/nodes-base/nodes/Slack/StarDescription.ts b/packages/nodes-base/nodes/Slack/V1/StarDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Slack/StarDescription.ts rename to packages/nodes-base/nodes/Slack/V1/StarDescription.ts diff --git a/packages/nodes-base/nodes/Slack/UserDescription.ts b/packages/nodes-base/nodes/Slack/V1/UserDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Slack/UserDescription.ts rename to packages/nodes-base/nodes/Slack/V1/UserDescription.ts diff --git a/packages/nodes-base/nodes/Slack/UserGroupDescription.ts b/packages/nodes-base/nodes/Slack/V1/UserGroupDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Slack/UserGroupDescription.ts rename to packages/nodes-base/nodes/Slack/V1/UserGroupDescription.ts diff --git a/packages/nodes-base/nodes/Slack/UserProfileDescription.ts b/packages/nodes-base/nodes/Slack/V1/UserProfileDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Slack/UserProfileDescription.ts rename to packages/nodes-base/nodes/Slack/V1/UserProfileDescription.ts diff --git a/packages/nodes-base/nodes/Slack/V2/ChannelDescription.ts b/packages/nodes-base/nodes/Slack/V2/ChannelDescription.ts new file mode 100644 index 0000000000..2f74d8e379 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/V2/ChannelDescription.ts @@ -0,0 +1,1495 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const channelOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['channel'], + }, + }, + options: [ + { + name: 'Archive', + value: 'archive', + description: 'Archives a conversation', + action: 'Archive a channel', + }, + { + name: 'Close', + value: 'close', + description: 'Closes a direct message or multi-person direct message', + action: 'Close a channel', + }, + { + name: 'Create', + value: 'create', + description: 'Initiates a public or private channel-based conversation', + action: 'Create a channel', + }, + { + name: 'Get', + value: 'get', + description: 'Get information about a channel', + action: 'Get a channel', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many channels in a Slack team', + action: 'Get many channels', + }, + { + name: 'History', + value: 'history', + description: "Get a conversation's history of messages and events", + action: 'Get the history of a channel', + }, + { + name: 'Invite', + value: 'invite', + description: 'Invite a user to a channel', + action: 'Invite a user to a channel', + }, + { + name: 'Join', + value: 'join', + description: 'Joins an existing conversation', + action: 'Join a channel', + }, + { + name: 'Kick', + value: 'kick', + description: 'Removes a user from a channel', + action: 'Kick a user from a channel', + }, + { + name: 'Leave', + value: 'leave', + description: 'Leaves a conversation', + action: 'Leave a channel', + }, + { + name: 'Member', + value: 'member', + description: 'List members of a conversation', + action: 'Get members of a channel', + }, + { + name: 'Open', + value: 'open', + description: 'Opens or resumes a direct message or multi-person direct message', + action: 'Open a channel', + }, + { + name: 'Rename', + value: 'rename', + description: 'Renames a conversation', + action: 'Rename a channel', + }, + { + name: 'Replies', + value: 'replies', + description: 'Get a thread of messages posted to a channel', + action: 'Get a thread of messages posted to a channel', + }, + { + name: 'Set Purpose', + value: 'setPurpose', + description: 'Sets the purpose for a conversation', + action: 'Set the purpose of a channel', + }, + { + name: 'Set Topic', + value: 'setTopic', + description: 'Sets the topic for a conversation', + action: 'Set the topic of a channel', + }, + { + name: 'Unarchive', + value: 'unarchive', + description: 'Unarchives a conversation', + action: 'Unarchive a channel', + }, + ], + default: 'create', + }, +]; + +export const channelFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* channel:archive */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + description: 'The Slack channel to archive', + displayOptions: { + show: { + operation: ['archive'], + resource: ['channel'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* channel:close */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['close'], + resource: ['channel'], + }, + }, + required: true, + description: 'The Slack channel to close', + }, + + /* -------------------------------------------------------------------------- */ + /* channel:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'string', + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: ['create'], + resource: ['channel'], + }, + }, + required: true, + }, + { + displayName: 'Channel Visibility', + name: 'channelVisibility', + type: 'options', + default: 'public', + required: true, + displayOptions: { + show: { + operation: ['create'], + resource: ['channel'], + }, + }, + options: [ + { + name: 'Public Channel', + value: 'public', + }, + { + name: 'Private Channel', + value: 'private', + }, + ], + description: + 'Whether to create a Public or a Private Slack channel. More info.', + }, + + /* -------------------------------------------------------------------------- */ + /* channel:invite */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['invite'], + resource: ['channel'], + }, + }, + required: true, + description: 'The Slack channel to invite to', + }, + { + displayName: 'User Names or IDs', + name: 'userIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + displayOptions: { + show: { + operation: ['invite'], + resource: ['channel'], + }, + }, + required: true, + description: + 'The ID of the user to invite into channel. Choose from the list, or specify IDs using an expression.', + }, + + /* -------------------------------------------------------------------------- */ + /* channel:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + required: true, + displayOptions: { + show: { + operation: ['get'], + resource: ['channel'], + }, + }, + description: 'The Slack channel to get', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['channel'], + operation: ['get'], + }, + }, + options: [ + { + displayName: 'Include Num of Members', + name: 'includeNumMembers', + type: 'boolean', + default: false, + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* channel:kick */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['kick'], + resource: ['channel'], + }, + }, + required: true, + description: 'The Slack channel to kick the user from', + }, + { + displayName: 'User Name or ID', + name: 'userId', + type: 'options', + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + operation: ['kick'], + resource: ['channel'], + }, + }, + default: '', + }, + + /* -------------------------------------------------------------------------- */ + /* channel:join */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + description: 'The Slack channel to join', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['join'], + resource: ['channel'], + }, + }, + required: true, + }, + + /* -------------------------------------------------------------------------- */ + /* channel:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['channel'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['channel'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'Max number of results to return', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['channel'], + operation: ['getAll'], + }, + }, + options: [ + { + displayName: 'Exclude Archived', + name: 'excludeArchived', + type: 'boolean', + default: false, + description: 'Whether to exclude archived channels from the list', + }, + { + displayName: 'Types', + name: 'types', + type: 'multiOptions', + options: [ + { + name: 'Public Channel', + value: 'public_channel', + }, + { + name: 'Private Channel', + value: 'private_channel', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'mpim', + value: 'mpim', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'im', + value: 'im', + }, + ], + default: ['public_channel'], + description: 'Mix and match channel types', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* channel:history */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['history'], + resource: ['channel'], + }, + }, + required: true, + description: 'The Slack channel to get the history from', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['channel'], + operation: ['history'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['channel'], + operation: ['history'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'Max number of results to return', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['channel'], + operation: ['history'], + }, + }, + options: [ + { + displayName: 'Inclusive', + name: 'inclusive', + type: 'boolean', + default: false, + description: + 'Whether to include messages with latest or oldest timestamp in results only when either timestamp is specified', + }, + { + displayName: 'Latest', + name: 'latest', + type: 'dateTime', + default: '', + description: 'End of time range of messages to include in results', + }, + { + displayName: 'Oldest', + name: 'oldest', + type: 'dateTime', + default: '', + description: 'Start of time range of messages to include in results', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* channel:leave */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['leave'], + resource: ['channel'], + }, + }, + required: true, + description: 'The Slack channel to leave from', + }, + + /* -------------------------------------------------------------------------- */ + /* channel:member */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + description: 'The Slack channel to get the members from', + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['member'], + resource: ['channel'], + }, + }, + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['channel'], + operation: ['member'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + typeOptions: { + minValue: 1, + }, + description: 'Max number of results to return', + default: 100, + placeholder: 'Limit', + displayOptions: { + show: { + operation: ['member'], + resource: ['channel'], + returnAll: [false], + }, + }, + }, + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + default: false, + displayOptions: { + show: { + resource: ['channel'], + operation: ['member'], + }, + }, + description: + 'Whether to resolve the data automatically. By default the response only contain the ID to resource.', + }, + + /* -------------------------------------------------------------------------- */ + /* channel:open */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['channel'], + operation: ['open'], + }, + }, + options: [ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + description: + "Resume a conversation by supplying an im or mpim's ID. Or provide the users field instead.", + }, + { + displayName: 'Return IM', + name: 'returnIm', + type: 'boolean', + default: false, + description: 'Whether you want the full IM channel definition in the response', + }, + { + displayName: 'User Names or IDs', + name: 'users', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + description: + 'If only one user is included, this creates a 1:1 DM. The ordering of the users is preserved whenever a multi-person direct message is returned. Supply a channel when not supplying users. Choose from the list, or specify IDs using an expression.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* channel:rename */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['rename'], + resource: ['channel'], + }, + }, + required: true, + description: 'The Slack channel to rename', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + operation: ['rename'], + resource: ['channel'], + }, + }, + default: '', + required: true, + description: 'New name for conversation', + }, + + /* -------------------------------------------------------------------------- */ + /* channel:replies */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['replies'], + resource: ['channel'], + }, + }, + required: true, + description: 'The Slack channel to replies to', + }, + { + displayName: 'Message Timestamp', + name: 'ts', + type: 'number', + default: undefined, + displayOptions: { + show: { + operation: ['replies'], + resource: ['channel'], + }, + }, + required: true, + description: 'Timestamp of the message to reply', + placeholder: '1663233118.856619', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['channel'], + operation: ['replies'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['channel'], + operation: ['replies'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'Max number of results to return', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['channel'], + operation: ['replies'], + }, + }, + options: [ + { + displayName: 'Inclusive', + name: 'inclusive', + type: 'boolean', + default: false, + description: + 'Whether to include messages with latest or oldest timestamp in results only when either timestamp is specified', + }, + { + displayName: 'Latest', + name: 'latest', + type: 'string', + default: '', + description: 'End of time range of messages to include in results', + }, + { + displayName: 'Oldest', + name: 'oldest', + type: 'string', + default: '', + description: 'Start of time range of messages to include in results', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* channel:setPurpose */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['setPurpose'], + resource: ['channel'], + }, + }, + required: true, + description: 'The Slack channel to set the purpose of', + }, + { + displayName: 'Purpose', + name: 'purpose', + type: 'string', + displayOptions: { + show: { + operation: ['setPurpose'], + resource: ['channel'], + }, + }, + default: '', + required: true, + description: 'A new, specialer purpose', + }, + + /* -------------------------------------------------------------------------- */ + /* channel:setTopic */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['setTopic'], + resource: ['channel'], + }, + }, + required: true, + description: 'The Slack channel to set the topic of', + }, + { + displayName: 'Topic', + name: 'topic', + type: 'string', + displayOptions: { + show: { + operation: ['setTopic'], + resource: ['channel'], + }, + }, + default: '', + required: true, + }, + + /* -------------------------------------------------------------------------- */ + /* channel:unarchive */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['unarchive'], + resource: ['channel'], + }, + }, + required: true, + description: 'The Slack channel to unarchive', + }, +]; diff --git a/packages/nodes-base/nodes/Slack/V2/FileDescription.ts b/packages/nodes-base/nodes/Slack/V2/FileDescription.ts new file mode 100644 index 0000000000..fdd1aaf9be --- /dev/null +++ b/packages/nodes-base/nodes/Slack/V2/FileDescription.ts @@ -0,0 +1,286 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const fileOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['file'], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + action: 'Get a file', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get & filters team files', + action: 'Get many files', + }, + { + name: 'Upload', + value: 'upload', + description: 'Create or upload an existing file', + action: 'Upload a file', + }, + ], + default: 'upload', + }, +]; + +export const fileFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* file:upload */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: ['upload'], + resource: ['file'], + }, + }, + description: 'Whether the data to upload should be taken from binary field', + }, + { + displayName: 'File Content', + name: 'fileContent', + type: 'string', + default: '', + displayOptions: { + show: { + operation: ['upload'], + resource: ['file'], + binaryData: [false], + }, + }, + placeholder: '', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + operation: ['upload'], + resource: ['file'], + binaryData: [true], + }, + }, + placeholder: '', + description: 'Name of the binary property which contains the data for the file to be uploaded', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: ['upload'], + resource: ['file'], + }, + }, + default: {}, + description: 'Other options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Channel Names or IDs', + name: 'channelIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: [], + description: + 'The channels to send the file to. Choose from the list, or specify IDs using an expression.', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + }, + { + displayName: 'Initial Comment', + name: 'initialComment', + type: 'string', + default: '', + description: 'The message text introducing the file in specified channels', + }, + { + displayName: 'Thread Timestamp', + name: 'threadTs', + type: 'string', + default: '', + description: + "Provide another message's Timestamp value to upload this file as a reply. Never use a reply's Timestamp value; use its parent instead.", + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + ], + }, + + /* ----------------------------------------------------------------------- */ + /* file:getAll */ + /* ----------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['file'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['file'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'Max number of results to return', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['file'], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Channel Name or ID', + name: 'channelId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + description: + 'Channel containing the file to be listed. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Show Files Hidden By Limit', + name: 'showFilesHidden', + type: 'boolean', + default: false, + description: + 'Whether to show truncated file info for files hidden due to being too old, and the team who owns the file being over the file limit', + }, + { + displayName: 'Message Timestamp From', + name: 'tsFrom', + type: 'string', + default: '', + description: 'Filter files created after this timestamp (inclusive)', + }, + { + displayName: 'Message Timestamp To', + name: 'tsTo', + type: 'string', + default: '', + description: 'Filter files created before this timestamp (inclusive)', + }, + { + displayName: 'Types', + name: 'types', + type: 'multiOptions', + options: [ + { + name: 'All', + value: 'all', + }, + { + name: 'Google Docs', + value: 'gdocs', + }, + { + name: 'Images', + value: 'images', + }, + { + name: 'PDFs', + value: 'pdfs', + }, + { + name: 'Snippets', + value: 'snippets', + }, + { + name: 'Spaces', + value: 'spaces', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'zips', + value: 'zips', + }, + ], + default: ['all'], + description: 'Filter files by type', + }, + { + displayName: 'User Name or ID', + name: 'userId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: + 'Filter files created by a single user. Choose from the list, or specify an ID using an expression.', + }, + ], + }, + + /* ----------------------------------------------------------------------- */ + /* file:get */ + /* ----------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + displayOptions: { + show: { + resource: ['file'], + operation: ['get'], + }, + }, + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts new file mode 100644 index 0000000000..02cb92d7ea --- /dev/null +++ b/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts @@ -0,0 +1,137 @@ +import type { OptionsWithUri } from 'request'; +import type { IExecuteFunctions, IExecuteSingleFunctions, ILoadOptionsFunctions } from 'n8n-core'; +import type { IDataObject, IOAuth2Options } from 'n8n-workflow'; + +import { NodeOperationError } from 'n8n-workflow'; + +import _ from 'lodash'; + +export async function slackApiRequest( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: object = {}, + query: object = {}, + headers: {} | undefined = undefined, + option: {} = {}, + // tslint:disable-next-line:no-any +): Promise { + const authenticationMethod = this.getNodeParameter('authentication', 0, 'accessToken') as string; + let options: OptionsWithUri = { + method, + headers: headers ?? { + 'Content-Type': 'application/json; charset=utf-8', + }, + body, + qs: query, + uri: `https://slack.com/api${resource}`, + json: true, + }; + options = Object.assign({}, options, option); + if (Object.keys(body).length === 0) { + delete options.body; + } + if (Object.keys(query).length === 0) { + delete options.qs; + } + + const oAuth2Options: IOAuth2Options = { + tokenType: 'Bearer', + property: 'authed_user.access_token', + }; + + const credentialType = authenticationMethod === 'accessToken' ? 'slackApi' : 'slackOAuth2Api'; + const response = await this.helpers.requestWithAuthentication.call( + this, + credentialType, + options, + { + oauth2: oAuth2Options, + }, + ); + + if (response.ok === false) { + if (response.error === 'paid_teams_only') { + throw new NodeOperationError( + this.getNode(), + `Your current Slack plan does not include the resource '${ + this.getNodeParameter('resource', 0) as string + }'`, + { + description: + 'Hint: Upgrate to the Slack plan that includes the funcionality you want to use.', + }, + ); + } else if (response.error === 'missing_scope') { + throw new NodeOperationError( + this.getNode(), + 'Your Slack credential is missing required Oauth Scopes', + { + description: `Add the following scope(s) to your Slack App: ${response.needed}`, + }, + ); + } + throw new NodeOperationError( + this.getNode(), + 'Slack error response: ' + JSON.stringify(response.error), + ); + } + if (response.ts !== undefined) { + Object.assign(response, { message_timestamp: response.ts }); + delete response.ts; + } + return response; +} + +export async function slackApiRequestAllItems( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + endpoint: string, + // tslint:disable-next-line:no-any + body: any = {}, + query: IDataObject = {}, + // tslint:disable-next-line:no-any +): Promise { + const returnData: IDataObject[] = []; + let responseData; + query.page = 1; + //if the endpoint uses legacy pagination use count + //https://api.slack.com/docs/pagination#classic + if (endpoint.includes('files.list')) { + query.count = 100; + } else { + query.limit = 100; + } + do { + responseData = await slackApiRequest.call(this, method, endpoint, body, query); + query.cursor = _.get(responseData, 'response_metadata.next_cursor'); + query.page++; + returnData.push.apply( + returnData, + responseData[propertyName].matches ?? responseData[propertyName], + ); + } while ( + (responseData.response_metadata?.next_cursor !== undefined && + responseData.response_metadata.next_cursor !== '' && + responseData.response_metadata.next_cursor !== null) || + (responseData.paging?.pages !== undefined && + responseData.paging.page !== undefined && + responseData.paging.page < responseData.paging.pages) || + (responseData[propertyName].paging?.pages !== undefined && + responseData[propertyName].paging.page !== undefined && + responseData[propertyName].paging.page < responseData[propertyName].paging.pages) + ); + return returnData; +} + +// tslint:disable-next-line:no-any +export function validateJSON(json: string | undefined): any { + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = undefined; + } + return result; +} diff --git a/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts b/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts new file mode 100644 index 0000000000..a948941be6 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts @@ -0,0 +1,1138 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const messageOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['message'], + }, + }, + options: [ + { + name: 'Delete', + value: 'delete', + action: 'Delete a message', + }, + { + name: 'Get Permalink', + value: 'getPermalink', + action: 'Get a message permalink', + }, + { + name: 'Search', + value: 'search', + action: 'Search for messages', + }, + { + name: 'Send', + value: 'post', + action: 'Send a message', + }, + { + name: 'Update', + value: 'update', + action: 'Update a message', + }, + ], + default: 'post', + }, +]; + +export const messageFields: INodeProperties[] = [ + /* ----------------------------------------------------------------------- */ + /* message:getPermalink + /* ----------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + description: 'The Slack channel to get the message permalink from', + displayOptions: { + show: { + resource: ['message'], + operation: ['getPermalink'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + }, + { + displayName: 'Message Timestamp', + name: 'timestamp', + required: true, + type: 'number', + default: undefined, + displayOptions: { + show: { + resource: ['message'], + operation: ['getPermalink'], + }, + }, + description: 'Timestamp of the message to message', + placeholder: '1663233118.856619', + }, + + /* -------------------------------------------------------------------------- */ + /* message:post */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Send Message To', + name: 'select', + type: 'options', + required: true, + displayOptions: { + show: { + resource: ['message'], + operation: ['post'], + }, + }, + options: [ + { + name: 'Channel', + value: 'channel', + }, + { + name: 'User', + value: 'user', + }, + ], + default: '', + placeholder: 'Select...', + }, + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['post'], + resource: ['message'], + select: ['channel'], + }, + }, + required: true, + description: 'The Slack channel to send to', + }, + { + displayName: 'User', + name: 'user', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a user...', + displayOptions: { + show: { + operation: ['post'], + resource: ['message'], + select: ['user'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a user...', + typeOptions: { + searchListMethod: 'getUsers', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack User ID', + }, + }, + ], + placeholder: 'U123AB45JGM', + }, + { + displayName: 'By username', + name: 'username', + type: 'string', + placeholder: '@username', + }, + ], + }, + { + displayName: 'Message Type', + name: 'messageType', + type: 'options', + displayOptions: { + show: { + operation: ['post'], + resource: ['message'], + }, + }, + description: + 'Whether to send a simple text message, or use Slack’s Blocks UI builder for more sophisticated messages that include form fields, sections and more', + options: [ + { + name: 'Simple Text Message', + value: 'text', + description: 'Supports basic Markdown', + }, + { + name: 'Blocks', + value: 'block', + description: + "Combine text, buttons, form elements, dividers and more in Slack 's visual builder", + }, + { + name: 'Attachments', + value: 'attachment', + }, + ], + default: 'text', + }, + { + displayName: 'Message Text', + name: 'text', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: ['post'], + resource: ['message'], + messageType: ['text'], + }, + }, + description: + 'The message text to post. Supports markdown by default - this can be disabled in "Options".', + }, + { + displayName: 'Blocks', + name: 'blocksUi', + type: 'string', + required: true, + displayOptions: { + show: { + operation: ['post'], + resource: ['message'], + messageType: ['block'], + }, + }, + typeOptions: { + rows: 3, + }, + description: + "Enter the JSON output from Slack's visual Block Kit Builder here. You can then use expressions to add variable content to your blocks. To create blocks, use Slack's Block Kit Builder", + hint: "To create blocks, use Slack's Block Kit Builder", + default: '', + }, + { + displayName: 'This is a legacy Slack feature. Slack advises to instead use Blocks.', + name: 'noticeAttachments', + type: 'notice', + displayOptions: { + show: { + operation: ['post'], + resource: ['message'], + messageType: ['attachment'], + }, + }, + default: '', + }, + { + displayName: 'Attachments', + name: 'attachments', + type: 'collection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add attachment', + }, + displayOptions: { + show: { + operation: ['post'], + resource: ['message'], + messageType: ['attachment'], + }, + }, + default: {}, // TODO: Remove comment: has to make default array for the main property, check where that happens in UI + placeholder: 'Add attachment item', + options: [ + { + displayName: 'Fallback Text', + name: 'fallback', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Required plain-text summary of the attachment', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Title Link', + name: 'title_link', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Color', + name: 'color', + type: 'color', + default: '#ff0000', + description: 'Color of the line left of text', + }, + { + displayName: 'Pretext', + name: 'pretext', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text which appears before the message block', + }, + { + displayName: 'Author Name', + name: 'author_name', + type: 'string', + default: '', + description: 'Name that should appear', + }, + { + displayName: 'Author Link', + name: 'author_link', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Author Icon', + name: 'author_icon', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Icon which should appear for the user', + }, + { + displayName: 'Image URL', + name: 'image_url', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Thumbnail URL', + name: 'thumb_url', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Footer', + name: 'footer', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text of footer to add', + }, + { + displayName: 'Footer Icon', + name: 'footer_icon', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Icon which should appear next to footer', + }, + { + displayName: 'Message Timestamp', + name: 'ts', + type: 'number', + default: 0, + description: 'Timestamp of the message to post', + placeholder: '1663233118.856619', + }, + { + displayName: 'Fields', + name: 'fields', + placeholder: 'Add Fields', + description: 'Fields to add to message', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'item', + displayName: 'Item', + values: [ + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + { + displayName: 'Short', + name: 'short', + type: 'boolean', + default: true, + description: 'Whether items can be displayed next to each other', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'otherOptions', + type: 'collection', + displayOptions: { + show: { + operation: ['post'], + resource: ['message'], + }, + }, + default: {}, + description: 'Other options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Custom Bot Profile Photo', + name: 'botProfile', + type: 'fixedCollection', + default: { + imageValues: [ + { + profilePhotoType: '', + }, + ], + }, + description: + 'Set an image or an emoji as the Profile Photo (avatar) of the bot sending the message. Will not be used if sending message as a user.', + options: [ + { + name: 'imageValues', + displayName: 'Add Bot Profile Photo', + values: [ + { + displayName: 'Profile Photo Type', + name: 'profilePhotoType', + type: 'options', + options: [ + { + name: 'Image URL', + value: 'image', + }, + { + name: 'Emoji Code', + value: 'emoji', + }, + ], + default: '', + placeholder: 'Select a type…', + }, + { + displayName: 'Emoji Code', + name: 'icon_emoji', + type: 'string', + default: '', + displayOptions: { + show: { + profilePhotoType: ['emoji'], + }, + }, + description: + 'Only used if sending message as a bot. Use emoji codes like +1, not an actual emoji like 👍. List of common emoji codes', + }, + { + displayName: 'Image URL', + name: 'icon_url', + type: 'string', + default: '', + displayOptions: { + show: { + profilePhotoType: ['image'], + }, + }, + description: 'Only used if sending message as a bot', + }, + ], + }, + ], + }, + { + displayName: 'Link User and Channel Names', + name: 'link_names', + type: 'boolean', + default: false, + description: 'Whether to turn @users and #channels in message text into clickable links', + }, + { + displayName: 'Reply to a Message', + name: 'thread_ts', + type: 'fixedCollection', + default: {}, + placeholder: 'Reply to a Message', + description: "Provide another message's Timestamp value to make this message a reply", + options: [ + { + displayName: 'Reply to a Message', + name: 'replyValues', + values: [ + { + displayName: 'Message Timestamp to Reply To', + name: 'thread_ts', + type: 'number', + default: undefined, + placeholder: '1663233118.856619', + description: + 'Message timestamps are included in output data of Slack nodes, abbreviated to ts', + }, + { + displayName: 'Reply to Thread', + name: 'reply_broadcast', + type: 'boolean', + default: false, + description: + 'Whether the reply should be made visible to everyone in the channel or conversation', + }, + ], + }, + ], + }, + { + displayName: 'Use Markdown?', + name: 'mrkdwn', + type: 'boolean', + default: true, + description: 'Whether to use Slack Markdown to format the message', + }, + { + displayName: 'Unfurl Links', + name: 'unfurl_links', + type: 'boolean', + default: false, + description: 'Whether to enable unfurling of primarily text-based content', + }, + { + displayName: 'Unfurl Media', + name: 'unfurl_media', + type: 'boolean', + default: true, + description: 'Whether to disable unfurling of media content', + }, + { + displayName: 'Send as Ephemeral Message', + name: 'ephemeral', + type: 'fixedCollection', + default: {}, + displayOptions: { + show: { + '/select': ['channel'], + }, + }, + placeholder: 'Send as Ephemeral Message', + description: 'Whether to send a temporary, ephemeral message', + options: [ + { + displayName: 'Send as Ephemeral Message', + name: 'ephemeralValues', + values: [ + { + displayName: 'User to Send', + name: 'user', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a user...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a user...', + typeOptions: { + searchListMethod: 'getUsers', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack User ID', + }, + }, + ], + placeholder: 'U123AB45JGM', + }, + ], + }, + { + displayName: 'Send as Ephemeral Message', + name: 'ephemeral', + type: 'boolean', + default: true, + description: 'Whether to send a temporary, ephemeral message', + }, + ], + }, + ], + }, + { + displayName: 'Send as Ephemeral Message', + name: 'ephemeral', + type: 'boolean', + displayOptions: { + show: { + '/select': ['user'], + }, + }, + default: true, + description: 'Whether to send a temporary, ephemeral message', + }, + { + displayName: 'Send as User', + name: 'sendAsUser', + type: 'string', + displayOptions: { + show: { + '/authentication': ['accessToken'], + }, + }, + default: '', + description: + 'The message will be sent from this username (i.e. as if this individual sent the message). Add chat:write.customize scope on Slack API', + }, + ], + }, + + /* ----------------------------------------------------------------------- */ + /* message:update */ + /* ----------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + required: true, + displayOptions: { + show: { + resource: ['message'], + operation: ['update'], + }, + }, + description: 'The Slack channel to update the message from', + }, + { + displayName: 'Message Timestamp', + name: 'ts', + required: true, + type: 'number', + default: undefined, + displayOptions: { + show: { + resource: ['message'], + operation: ['update'], + }, + }, + description: 'Timestamp of the message to update', + placeholder: '1663233118.856619', + }, + { + displayName: 'Message Text', + name: 'text', + type: 'string', + default: '', + displayOptions: { + show: { + resource: ['message'], + operation: ['update'], + }, + }, + description: + 'The message text to update. Supports markdown by default - this can be disabled in "Options".', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: ['message'], + operation: ['update'], + }, + }, + options: [ + { + displayName: 'Link User and Channel Names', + name: 'link_names', + type: 'boolean', + default: false, + description: 'Whether to find and link channel names and usernames', + }, + { + displayName: 'Parse', + name: 'parse', + type: 'options', + options: [ + { + name: 'Client', + value: 'client', + }, + { + name: 'Full', + value: 'full', + }, + { + name: 'None', + value: 'none', + }, + ], + default: 'client', + description: 'Change how messages are treated', + }, + ], + }, + + /* ----------------------------------------------------------------------- */ + /* message:delete + /* ----------------------------------------------------------------------- */ + { + displayName: 'Delete Message From', + name: 'select', + type: 'options', + required: true, + displayOptions: { + show: { + resource: ['message'], + operation: ['delete'], + }, + }, + options: [ + { + name: 'Channel', + value: 'channel', + }, + { + name: 'User', + value: 'user', + }, + ], + default: '', + placeholder: 'Select...', + }, + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + displayOptions: { + show: { + operation: ['delete'], + resource: ['message'], + select: ['channel'], + }, + }, + required: true, + description: 'The Slack channel to delete the message from', + }, + { + displayName: 'User', + name: 'user', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a user...', + displayOptions: { + show: { + operation: ['delete'], + resource: ['message'], + select: ['user'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a user...', + typeOptions: { + searchListMethod: 'getUsers', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack User ID', + }, + }, + ], + placeholder: 'U123AB45JGM', + }, + ], + }, + { + displayName: 'Message Timestamp', + name: 'timestamp', + required: true, + type: 'number', + default: undefined, + displayOptions: { + show: { + resource: ['message'], + operation: ['delete'], + }, + }, + description: 'Timestamp of the message to delete', + placeholder: '1663233118.856619', + }, + + /* ----------------------------------------------------------------------- */ + /* message:search + /* ----------------------------------------------------------------------- */ + { + displayName: 'Search Query', + name: 'query', + type: 'string', + description: 'The text to search for within messages', + required: true, + default: '', + displayOptions: { + show: { + resource: ['message'], + operation: ['search'], + }, + }, + }, + { + displayName: 'Sort By', + name: 'sort', + description: 'How search results should be sorted. You can sort by.', + type: 'options', + displayOptions: { + show: { + resource: ['message'], + operation: ['search'], + }, + }, + options: [ + { + name: 'Newest', + value: 'desc', + }, + { + name: 'Oldest', + value: 'asc', + }, + { + name: 'Relevance Score', + value: 'relevance', + }, + ], + default: 'desc', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['message'], + operation: ['search'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['message'], + operation: ['search'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 50, + }, + default: 25, + description: 'Max number of results to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + resource: ['message'], + operation: ['search'], + }, + }, + options: [ + { + displayName: 'Search in Channel', + name: 'searchChannel', + type: 'multiOptions', + description: + 'Choose from the list, or specify IDs using an expression', + default: [], + placeholder: 'Select a channel...', + typeOptions: { + loadOptionsMethod: 'getChannelsName', + }, + }, + ], + default: {}, + }, +]; diff --git a/packages/nodes-base/nodes/Slack/V2/MessageInterface.ts b/packages/nodes-base/nodes/Slack/V2/MessageInterface.ts new file mode 100644 index 0000000000..69c7b9cb4b --- /dev/null +++ b/packages/nodes-base/nodes/Slack/V2/MessageInterface.ts @@ -0,0 +1,5 @@ +export interface IAttachment { + fields: { + item?: object[]; + }; +} diff --git a/packages/nodes-base/nodes/Slack/V2/ReactionDescription.ts b/packages/nodes-base/nodes/Slack/V2/ReactionDescription.ts new file mode 100644 index 0000000000..10d6823e72 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/V2/ReactionDescription.ts @@ -0,0 +1,131 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const reactionOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['reaction'], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Adds a reaction to a message', + action: 'Add a reaction', + }, + { + name: 'Get', + value: 'get', + description: 'Get the reactions of a message', + action: 'Get a reaction', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a reaction of a message', + action: 'Remove a reaction', + }, + ], + default: 'add', + }, +]; + +export const reactionFields: INodeProperties[] = [ + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + required: true, + displayOptions: { + show: { + resource: ['reaction'], + operation: ['add', 'get', 'remove'], + }, + }, + description: 'The Slack channel to get the reactions from', + }, + { + displayName: 'Message Timestamp', + name: 'timestamp', + required: true, + type: 'number', + default: undefined, + displayOptions: { + show: { + resource: ['reaction'], + operation: ['add', 'get', 'remove'], + }, + }, + description: 'Timestamp of the message to add, get or remove', + placeholder: '1663233118.856619', + }, + { + displayName: 'Emoji Code', + name: 'name', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: ['reaction'], + operation: ['add', 'remove'], + }, + }, + description: + 'Emoji code to use for the message reaction. Use emoji codes like +1, not an actual emoji like 👍. List of common emoji codes', + placeholder: '+1', + }, +]; diff --git a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts new file mode 100644 index 0000000000..6304df6bf1 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts @@ -0,0 +1,1342 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { IExecuteFunctions } from 'n8n-core'; + +import type { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodeListSearchItems, + INodeListSearchResult, + INodeParameterResourceLocator, + INodePropertyOptions, + INodeType, + INodeTypeBaseDescription, + INodeTypeDescription, + JsonObject, +} from 'n8n-workflow'; + +import { NodeOperationError } from 'n8n-workflow'; + +import { channelFields, channelOperations } from './ChannelDescription'; +import { messageFields, messageOperations } from './MessageDescription'; +import { starFields, starOperations } from './StarDescription'; +import { fileFields, fileOperations } from './FileDescription'; +import { reactionFields, reactionOperations } from './ReactionDescription'; +import { userGroupFields, userGroupOperations } from './UserGroupDescription'; +import { userFields, userOperations } from './UserDescription'; +import { slackApiRequest, slackApiRequestAllItems, validateJSON } from './GenericFunctions'; + +import moment from 'moment'; + +export class SlackV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + version: 2, + defaults: { + name: 'Slack', + }, + 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', + }, + + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Channel', + value: 'channel', + }, + { + name: 'File', + value: 'file', + }, + { + name: 'Message', + value: 'message', + }, + { + name: 'Reaction', + value: 'reaction', + }, + { + name: 'Star', + value: 'star', + }, + { + name: 'User', + value: 'user', + }, + { + name: 'User Group', + value: 'userGroup', + }, + ], + default: 'message', + }, + + ...channelOperations, + ...channelFields, + ...messageOperations, + ...messageFields, + ...starOperations, + ...starFields, + ...fileOperations, + ...fileFields, + ...reactionOperations, + ...reactionFields, + ...userOperations, + ...userFields, + ...userGroupOperations, + ...userGroupFields, + ], + }; + } + + methods = { + listSearch: { + async getChannels( + this: ILoadOptionsFunctions, + filter?: string, + ): Promise { + const qs = { types: 'public_channel,private_channel', limit: 1000 }; + const channels = (await slackApiRequestAllItems.call( + this, + 'channels', + 'GET', + '/conversations.list', + {}, + qs, + )) as Array<{ id: string; name: string }>; + const results: INodeListSearchItems[] = channels + .map((c) => ({ + name: c.name, + value: c.id, + })) + .filter( + (c) => + !filter || + c.name.toLowerCase().includes(filter.toLowerCase()) || + c.value?.toString() === filter, + ) + .sort((a, b) => { + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; + return 0; + }); + return { results }; + }, + async getUsers(this: ILoadOptionsFunctions, filter?: string): Promise { + const users = (await slackApiRequestAllItems.call( + this, + 'members', + 'GET', + '/users.list', + )) as Array<{ id: string; name: string }>; + const results: INodeListSearchItems[] = users + .map((c) => ({ + name: c.name, + value: c.id, + })) + .filter( + (c) => + !filter || + c.name.toLowerCase().includes(filter.toLowerCase()) || + c.value?.toString() === filter, + ) + .sort((a, b) => { + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; + return 0; + }); + return { results }; + }, + }, + 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 qs = { types: 'public_channel,private_channel', limit: 1000 }; + const channels = await slackApiRequestAllItems.call( + this, + 'channels', + 'GET', + '/conversations.list', + {}, + qs, + ); + 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; + }, + // Get all the users to display them to user so that he can + // select them easily + async getChannelsName(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { types: 'public_channel,private_channel', limit: 1000 }; + const channels = await slackApiRequestAllItems.call( + this, + 'channels', + 'GET', + '/conversations.list', + {}, + qs, + ); + for (const channel of channels) { + const channelName = channel.name; + returnData.push({ + name: channelName, + value: channelName, + }); + } + + 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 team fields to display them to user so that he can + // select them easily + async getTeamFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { + profile: { fields }, + } = await slackApiRequest.call(this, 'GET', '/team.profile.get'); + for (const field of fields) { + const fieldName = field.label; + const fieldId = field.id; + returnData.push({ + name: fieldName, + value: fieldId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + const length = items.length; + let qs: IDataObject; + let responseData; + const authentication = this.getNodeParameter('authentication', 0) as string; + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + for (let i = 0; i < length; i++) { + try { + responseData = { + error: 'Resource ' + resource + ' / operation ' + operation + ' not found!', + }; + qs = {}; + if (resource === 'channel') { + //https://api.slack.com/methods/conversations.archive + if (operation === 'archive') { + const channel = this.getNodeParameter( + 'channelId', + i, + {}, + { extractValue: true }, + ) 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, + {}, + { extractValue: true }, + ) 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') { + let channel = this.getNodeParameter( + 'channelId', + i, + {}, + { extractValue: true }, + ) as string; + channel = channel[0] === '#' ? channel.slice(1) : channel; + const channelVisibility = this.getNodeParameter('channelVisibility', i) as string; + const body: IDataObject = { + name: channel, + is_private: channelVisibility === 'private' ? true : false, + }; + 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, + {}, + { extractValue: true }, + ) as string; + const userId = this.getNodeParameter('userId', i) as string; + const body: IDataObject = { + 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, + {}, + { extractValue: true }, + ) 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, + {}, + { extractValue: true }, + ) 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); + const filters = this.getNodeParameter('filters', i); + if (filters.types) { + qs.types = (filters.types as string[]).join(','); + } + if (filters.excludeArchived) { + qs.exclude_archived = filters.excludeArchived as boolean; + } + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'channels', + 'GET', + '/conversations.list', + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + 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, + {}, + { extractValue: true }, + ) as string; + const returnAll = this.getNodeParameter('returnAll', i); + const filters = this.getNodeParameter('filters', i); + qs.channel = channel; + if (filters.inclusive) { + qs.inclusive = filters.inclusive as boolean; + } + if (filters.latest) { + qs.latest = new Date(filters.latest as string).getTime() / 1000; + } + if (filters.oldest) { + qs.oldest = new Date(filters.oldest as string).getTime() / 1000; + } + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'messages', + 'GET', + '/conversations.history', + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + 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, + {}, + { extractValue: true }, + ) as string; + const userIds = (this.getNodeParameter('userIds', i) as string[]).join(','); + const body: IDataObject = { + channel, + users: userIds, + }; + 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, + {}, + { extractValue: true }, + ) as string; + const body: IDataObject = { + channel, + }; + responseData = await slackApiRequest.call( + this, + 'POST', + '/conversations.leave', + body, + qs, + ); + } + //https://api.slack.com/methods/conversations.members + if (operation === 'member') { + const returnAll = this.getNodeParameter('returnAll', 0); + const resolveData = this.getNodeParameter('resolveData', 0); + qs.channel = this.getNodeParameter( + 'channelId', + i, + {}, + { extractValue: true }, + ) as string; + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'members', + 'GET', + '/conversations.members', + {}, + qs, + ); + responseData = responseData.map((member: string) => ({ member })); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await slackApiRequest.call( + this, + 'GET', + '/conversations.members', + {}, + qs, + ); + responseData = responseData.members.map((member: string) => ({ member })); + } + + if (resolveData) { + const data: IDataObject[] = []; + for (const { member } of responseData) { + const { user } = await slackApiRequest.call( + this, + 'GET', + '/users.info', + {}, + { user: member }, + ); + data.push(user); + } + responseData = data; + } + } + //https://api.slack.com/methods/conversations.open + if (operation === 'open') { + const options = this.getNodeParameter('options', i); + 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, + {}, + { extractValue: true }, + ) 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, + {}, + { extractValue: true }, + ) as string; + const ts = this.getNodeParameter('ts', i)?.toString() as string; + const returnAll = this.getNodeParameter('returnAll', i); + const filters = this.getNodeParameter('filters', i); + qs.channel = channel; + qs.ts = ts; + if (filters.inclusive) { + qs.inclusive = filters.inclusive as boolean; + } + if (filters.latest) { + qs.latest = new Date(filters.latest as string).getTime() / 1000; + } + if (filters.oldest) { + qs.oldest = new Date(filters.oldest as string).getTime() / 1000; + } + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'messages', + 'GET', + '/conversations.replies', + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + 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, + {}, + { extractValue: true }, + ) 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, + {}, + { extractValue: true }, + ) 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, + {}, + { extractValue: true }, + ) 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 select = this.getNodeParameter('select', i) as string; + const messageType = this.getNodeParameter('messageType', i) as string; + let target = + select === 'channel' + ? (this.getNodeParameter('channelId', i, undefined, { + extractValue: true, + }) as string) + : (this.getNodeParameter('user', i, undefined, { + extractValue: true, + }) as string); + // @ts-ignore + if (select === 'user' && this.getNodeParameter('user', i).mode === 'username') { + target = target.slice(0, 1) === '@' ? target : `@${target}`; + } + const { sendAsUser } = this.getNodeParameter('otherOptions', i) as IDataObject; + let content: IDataObject = {}; + switch (messageType) { + case 'text': + content = { text: this.getNodeParameter('text', i) as string }; + break; + case 'block': + content = JSON.parse(this.getNodeParameter('blocksUi', i) as string); + break; + case 'attachment': + content = { attachments: this.getNodeParameter('attachments', i) } as IDataObject; + break; + default: + throw new NodeOperationError( + this.getNode(), + `The message type "${messageType}" is not known!`, + ); + } + const body: IDataObject = { + channel: target, + ...content, + }; + if (authentication === 'accessToken' && sendAsUser !== '' && sendAsUser !== undefined) { + body.username = sendAsUser; + } + // Add all the other options to the request + const otherOptions = this.getNodeParameter('otherOptions', i) as IDataObject; + let action = 'postMessage'; + if (otherOptions.ephemeral) { + const ephemeral = otherOptions.ephemeral as IDataObject; + if (select === 'channel') { + const ephemeralValues = ephemeral.ephemeralValues as IDataObject; + const userRlc = ephemeralValues.user as INodeParameterResourceLocator; + body.user = + userRlc.value?.toString().slice(0, 1) !== '@' && userRlc.mode === 'username' + ? `@${userRlc.value}` + : userRlc.value; + action = 'postEphemeral'; + } else if (select === 'user') { + body.user = target; + action = 'postEphemeral'; + } + } + //@ts-ignore + const replyValues = otherOptions.thread_ts?.replyValues as IDataObject; + Object.assign(body, replyValues); + delete otherOptions.thread_ts; + delete otherOptions.ephemeral; + if (otherOptions.botProfile) { + const botProfile = otherOptions.botProfile as IDataObject; + const botProfileValues = botProfile.imageValues as IDataObject; + Object.assign( + body, + botProfileValues.profilePhotoType === 'image' + ? { icon_url: botProfileValues.icon_url } + : { icon_emoji: botProfileValues.icon_emoji }, + ); + } + delete otherOptions.botProfile; + Object.assign(body, otherOptions); + if ( + select === 'user' && + action === 'postEphemeral' && + (this.getNodeParameter('user', i) as INodeParameterResourceLocator)?.mode === + 'username' + ) { + throw new NodeOperationError( + this.getNode(), + 'You cannot send ephemeral messages using User type "By username". Please use "From List" or "By ID".', + ); + } else { + responseData = await slackApiRequest.call(this, 'POST', `/chat.${action}`, body, qs); + } + } + //https://api.slack.com/methods/chat.update + if (operation === 'update') { + const channel = this.getNodeParameter( + 'channelId', + i, + {}, + { extractValue: true }, + ) as string; + const text = this.getNodeParameter('text', i) as string; + const ts = this.getNodeParameter('ts', i)?.toString() as string; + const body: IDataObject = { + channel, + text, + ts, + }; + + const jsonParameters = this.getNodeParameter('jsonParameters', i, false); + if (jsonParameters) { + const blocksJson = this.getNodeParameter('blocksJson', i, []) as string; + + if (blocksJson !== '' && validateJSON(blocksJson) === undefined) { + throw new NodeOperationError(this.getNode(), 'Blocks it is not a valid json', { + itemIndex: i, + }); + } + if (blocksJson !== '') { + body.blocks = blocksJson; + } + } + // Add all the other options to the request + const updateFields = this.getNodeParameter('updateFields', i); + Object.assign(body, updateFields); + responseData = await slackApiRequest.call(this, 'POST', '/chat.update', body, qs); + } + //https://api.slack.com/methods/chat.delete + if (operation === 'delete') { + const select = this.getNodeParameter('select', i) as string; + let target = + select === 'channel' + ? (this.getNodeParameter('channelId', i, undefined, { + extractValue: true, + }) as string) + : (this.getNodeParameter('user', i, undefined, { + extractValue: true, + }) as string); + // @ts-ignore + if (select === 'user' && this.getNodeParameter('user', i).mode === 'username') { + target = target.slice(0, 1) === '@' ? target : `@${target}`; + } + const timestamp = this.getNodeParameter('timestamp', i)?.toString() as string; + const body: IDataObject = { + channel: target, + ts: timestamp, + }; + // Add all the other options to the request + responseData = await slackApiRequest.call(this, 'POST', '/chat.delete', body, qs); + } + //https://api.slack.com/methods/chat.getPermalink + if (operation === 'getPermalink') { + const channel = this.getNodeParameter( + 'channelId', + i, + {}, + { extractValue: true }, + ) as string; + const timestamp = this.getNodeParameter('timestamp', i)?.toString() as string; + qs = { + channel, + message_ts: timestamp, + }; + responseData = await slackApiRequest.call(this, 'GET', '/chat.getPermalink', {}, qs); + } + //https://api.slack.com/methods/search.messages + if (operation === 'search') { + let query = this.getNodeParameter('query', i) as string; + const sort = this.getNodeParameter('sort', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + const options = this.getNodeParameter('options', i); + if (options.searchChannel) { + const channel = options.searchChannel as IDataObject[]; + for (const channelItem of channel) { + query += ` in:${channelItem}`; + } + } + qs = { + query, + sort: sort === 'relevance' ? 'score' : 'timestamp', + sort_dir: sort === 'asc' ? 'asc' : 'desc', + }; + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'messages', + 'GET', + '/search.messages', + {}, + qs, + ); + } else { + qs.count = this.getNodeParameter('limit', i); + responseData = await slackApiRequest.call(this, 'POST', '/search.messages', {}, qs); + responseData = responseData.messages.matches; + } + } + } + if (resource === 'reaction') { + const channel = this.getNodeParameter( + 'channelId', + i, + {}, + { extractValue: true }, + ) as string; + const timestamp = this.getNodeParameter('timestamp', i)?.toString() as string; + //https://api.slack.com/methods/reactions.add + if (operation === 'add') { + const name = this.getNodeParameter('name', i) as string; + const body: IDataObject = { + channel, + name, + timestamp, + }; + responseData = await slackApiRequest.call(this, 'POST', '/reactions.add', body, qs); + } + //https://api.slack.com/methods/reactions.remove + if (operation === 'remove') { + const name = this.getNodeParameter('name', i) as string; + const body: IDataObject = { + channel, + name, + timestamp, + }; + responseData = await slackApiRequest.call(this, 'POST', '/reactions.remove', body, qs); + } + //https://api.slack.com/methods/reactions.get + if (operation === 'get') { + qs.channel = channel; + qs.timestamp = timestamp; + responseData = await slackApiRequest.call(this, 'GET', '/reactions.get', {}, qs); + } + } + if (resource === 'star') { + //https://api.slack.com/methods/stars.add + if (operation === 'add') { + const options = this.getNodeParameter('options', i); + const target = this.getNodeParameter('target', i) as string; + const channel = this.getNodeParameter( + 'channelId', + i, + {}, + { extractValue: true }, + ) as string; + const body: IDataObject = {}; + body.channel = channel; + + if (target === 'message') { + const timestamp = this.getNodeParameter('timestamp', i)?.toString() as string; + body.timestamp = timestamp; + } + if (target === 'file') { + const file = this.getNodeParameter('fileId', i) as string; + body.file = file; + } + if (options.fileComment) { + body.file_comment = options.fileComment 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); + 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); + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'items', + 'GET', + '/stars.list', + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + 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); + const binaryData = this.getNodeParameter('binaryData', i); + 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); + if ( + items[i].binary === undefined || + //@ts-ignore + items[i].binary[binaryPropertyName] === undefined + ) { + throw new NodeOperationError( + this.getNode(), + `No binary data property "${binaryPropertyName}" does not exists on item!`, + { itemIndex: i }, + ); + } + const binaryDataBuffer = await this.helpers.getBinaryDataBuffer( + i, + binaryPropertyName, + ); + body.file = { + //@ts-ignore + value: binaryDataBuffer, + 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); + const filters = this.getNodeParameter('filters', i); + 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(','); + } + if (filters.userId) { + qs.user = filters.userId as string; + } + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'files', + 'GET', + '/files.list', + {}, + qs, + ); + } else { + qs.count = this.getNodeParameter('limit', i); + 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 (resource === 'user') { + //https://api.slack.com/methods/users.info + if (operation === 'info') { + qs.user = this.getNodeParameter('user', i, undefined, { extractValue: true }) as string; + responseData = await slackApiRequest.call(this, 'GET', '/users.info', {}, qs); + responseData = responseData.user; + } + //https://api.slack.com/methods/users.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i); + if (returnAll) { + responseData = await slackApiRequestAllItems.call( + this, + 'members', + 'GET', + '/users.list', + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await slackApiRequest.call(this, 'GET', '/users.list', {}, qs); + responseData = responseData.members; + } + } + //https://api.slack.com/methods/users.getPresence + if (operation === 'getPresence') { + qs.user = this.getNodeParameter('user', i, undefined, { extractValue: true }) as string; + responseData = await slackApiRequest.call(this, 'GET', '/users.getPresence', {}, qs); + } + if (operation === 'updateProfile') { + const options = this.getNodeParameter('options', i); + const timezone = this.getTimezone(); + + const body: IDataObject = {}; + let status; + if (options.status) { + // @ts-ignore + status = options.status?.set_status[0] as IDataObject; + if (status.status_expiration === undefined) { + status.status_expiration = 0; + } else { + status.status_expiration = moment + .tz(status.status_expiration as string, timezone) + .unix(); + } + Object.assign(body, status); + delete options.status; + } + + if (options.customFieldUi) { + const customFields = (options.customFieldUi as IDataObject) + .customFieldValues as IDataObject[]; + + options.fields = {}; + + for (const customField of customFields) { + //@ts-ignore + options.fields[customField.id] = { + value: customField.value, + alt: customField.alt, + }; + } + } + Object.assign(body, options); + responseData = await slackApiRequest.call( + this, + 'POST', + '/users.profile.set', + { profile: body }, + qs, + ); + + responseData = responseData.profile; + } + } + if (resource === 'userGroup') { + //https://api.slack.com/methods/usergroups.create + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + + const options = this.getNodeParameter('options', i); + + const body: IDataObject = { + name, + }; + + Object.assign(body, options); + + responseData = await slackApiRequest.call(this, 'POST', '/usergroups.create', body, qs); + + responseData = responseData.usergroup; + } + //https://api.slack.com/methods/usergroups.enable + if (operation === 'enable') { + const userGroupId = this.getNodeParameter('userGroupId', i) as string; + + const options = this.getNodeParameter('options', i); + + const body: IDataObject = { + usergroup: userGroupId, + }; + + Object.assign(body, options); + + responseData = await slackApiRequest.call(this, 'POST', '/usergroups.enable', body, qs); + + responseData = responseData.usergroup; + } + //https://api.slack.com/methods/usergroups.disable + if (operation === 'disable') { + const userGroupId = this.getNodeParameter('userGroupId', i) as string; + + const options = this.getNodeParameter('options', i); + + const body: IDataObject = { + usergroup: userGroupId, + }; + + Object.assign(body, options); + + responseData = await slackApiRequest.call( + this, + 'POST', + '/usergroups.disable', + body, + qs, + ); + + responseData = responseData.usergroup; + } + + //https://api.slack.com/methods/usergroups.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i); + + const options = this.getNodeParameter('options', i); + + Object.assign(qs, options); + + responseData = await slackApiRequest.call(this, 'GET', '/usergroups.list', {}, qs); + + responseData = responseData.usergroups; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i); + + responseData = responseData.slice(0, limit); + } + } + + //https://api.slack.com/methods/usergroups.update + if (operation === 'update') { + const userGroupId = this.getNodeParameter('userGroupId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i); + + const body: IDataObject = { + usergroup: userGroupId, + }; + + Object.assign(body, updateFields); + + responseData = await slackApiRequest.call(this, 'POST', '/usergroups.update', body, qs); + + responseData = responseData.usergroup; + } + } + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { error: (error as JsonObject).message } }); + continue; + } + throw error; + } + } + return this.prepareOutputData(returnData); + } +} diff --git a/packages/nodes-base/nodes/Slack/V2/StarDescription.ts b/packages/nodes-base/nodes/Slack/V2/StarDescription.ts new file mode 100644 index 0000000000..d600202a9b --- /dev/null +++ b/packages/nodes-base/nodes/Slack/V2/StarDescription.ts @@ -0,0 +1,268 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const starOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['star'], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a star to an item', + action: 'Add a star', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a star from an item', + action: 'Delete a star', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many stars of autenticated user', + action: 'Get many stars', + }, + ], + default: 'add', + }, +]; + +export const starFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* star:add */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Item to Add Star', + name: 'target', + type: 'options', + required: true, + description: 'Choose whether to add a star to a message or a file', + default: '', + placeholder: 'Select...', + displayOptions: { + show: { + operation: ['add'], + resource: ['star'], + }, + }, + options: [ + { + name: 'Message', + value: 'message', + }, + { + name: 'File', + value: 'file', + }, + ], + }, + { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a channel...', + description: 'The Slack channel to add a star to', + displayOptions: { + show: { + resource: ['star'], + operation: ['add'], + target: ['message', 'file'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a channel...', + typeOptions: { + searchListMethod: 'getChannels', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack Channel ID', + }, + }, + ], + placeholder: 'C0122KQ70S7E', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A', + validation: [ + { + type: 'regex', + properties: { + regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + errorMessage: 'Not a valid Slack Channel URL', + }, + }, + ], + extractValue: { + type: 'regex', + regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})', + }, + }, + ], + }, + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + default: '', + displayOptions: { + show: { + resource: ['star'], + operation: ['add'], + target: ['file'], + }, + }, + description: 'File to add star to', + }, + { + displayName: 'Message Timestamp', + name: 'timestamp', + type: 'number', + default: undefined, + displayOptions: { + show: { + resource: ['star'], + operation: ['add'], + target: ['message'], + }, + }, + description: 'Timestamp of the message to add', + placeholder: '1663233118.856619', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: ['add'], + resource: ['star'], + }, + }, + default: {}, + description: 'Options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'File Comment', + name: 'fileComment', + type: 'string', + default: '', + description: 'File comment to add star to', + }, + ], + }, + + /* ----------------------------------------------------------------------- */ + /* star:delete */ + /* ----------------------------------------------------------------------- */ + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: ['delete'], + resource: ['star'], + }, + }, + default: {}, + description: 'Options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Channel Name or ID', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: '', + description: + 'Channel to add star to, or channel where the message to add star to was posted (used with timestamp). Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + default: '', + description: 'File to add star to', + }, + { + displayName: 'File Comment', + name: 'fileComment', + type: 'string', + default: '', + description: 'File comment to add star to', + }, + { + displayName: 'Message Timestamp', + name: 'timestamp', + type: 'number', + default: 0, + description: 'Timestamp of the message to delete', + placeholder: '1663233118.856619', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* star:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['star'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['star'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'Max number of results to return', + }, +]; diff --git a/packages/nodes-base/nodes/Slack/V2/UserDescription.ts b/packages/nodes-base/nodes/Slack/V2/UserDescription.ts new file mode 100644 index 0000000000..1ced8d7dae --- /dev/null +++ b/packages/nodes-base/nodes/Slack/V2/UserDescription.ts @@ -0,0 +1,297 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const userOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['user'], + }, + }, + options: [ + { + name: 'Get', + value: 'info', + description: 'Get information about a user', + action: 'Get information about a user', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get a list of many users', + action: 'Get many users', + }, + { + name: "Get User's Status", + value: 'getPresence', + description: 'Get online status of a user', + action: "Get a user's presence status", + }, + { + name: "Update User's Profile", + value: 'updateProfile', + description: "Update a user's profile", + action: "Update a user's profile", + }, + ], + default: 'info', + }, +]; + +export const userFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* user:info */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User', + name: 'user', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a user...', + description: 'The ID of the user to get information about', + displayOptions: { + show: { + operation: ['info'], + resource: ['user'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a user...', + typeOptions: { + searchListMethod: 'getUsers', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack User ID', + }, + }, + ], + placeholder: 'U123AB45JGM', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* user:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['user'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['user'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'Max number of results to return', + }, + + /* -------------------------------------------------------------------------- */ + /* user:getPresence */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User', + name: 'user', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + placeholder: 'Select a user...', + description: 'The ID of the user to get the online status of', + displayOptions: { + show: { + operation: ['getPresence'], + resource: ['user'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a user...', + typeOptions: { + searchListMethod: 'getUsers', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Slack User ID', + }, + }, + ], + placeholder: 'U123AB45JGM', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* user:update user profile */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['user'], + operation: ['updateProfile'], + }, + }, + options: [ + { + displayName: 'Custom Fields', + name: 'customFieldUi', + placeholder: 'Add Custom Fields', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'customFieldValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Name or ID', + name: 'id', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTeamFields', + }, + default: '', + description: + 'ID of the field to set. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Field Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the field to set', + }, + { + displayName: 'Alt', + name: 'alt', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + default: '', + description: 'This field can only be changed by admins for users on paid teams', + }, + { + displayName: 'First Name', + name: 'first_name', + type: 'string', + default: '', + }, + { + displayName: 'Last Name', + name: 'last_name', + type: 'string', + default: '', + }, + { + displayName: 'Set Status', + name: 'status', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + placeholder: 'Set Status', + options: [ + { + displayName: 'Set Status', + name: 'set_status', + values: [ + { + displayName: 'Status Emoji', + name: 'status_emoji', + type: 'string', + default: '', + description: + 'Is a string referencing an emoji enabled for the Slack team, such as :mountain_railway:', + }, + { + displayName: 'Status Expiration', + name: 'status_expiration', + type: 'dateTime', + default: '', + description: + 'The number of minutes to wait until this status expires and is cleared. Optional.', + }, + { + displayName: 'Status Text', + name: 'status_text', + type: 'string', + default: '', + description: 'Allows up to 100 characters, though we strongly encourage brevity', + }, + ], + }, + ], + }, + { + displayName: 'User ID', + name: 'user', + type: 'string', + default: '', + description: + 'ID of user to change. This argument may only be specified by team admins on paid teams.', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Slack/V2/UserGroupDescription.ts b/packages/nodes-base/nodes/Slack/V2/UserGroupDescription.ts new file mode 100644 index 0000000000..08727b5cb2 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/V2/UserGroupDescription.ts @@ -0,0 +1,329 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const userGroupOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['userGroup'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + action: 'Create a user group', + }, + { + name: 'Disable', + value: 'disable', + action: 'Disable a user group', + }, + { + name: 'Enable', + value: 'enable', + action: 'Enable a user group', + }, + { + name: 'Get Many', + value: 'getAll', + action: 'Get many user groups', + }, + { + name: 'Update', + value: 'update', + action: 'Update a user group', + }, + ], + default: 'create', + }, +]; + +export const userGroupFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* userGroup:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + displayOptions: { + show: { + operation: ['create'], + resource: ['userGroup'], + }, + }, + required: true, + description: 'A name for the User Group. Must be unique among User Groups.', + }, + { + displayName: 'Options', + name: 'Options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['userGroup'], + operation: ['create'], + }, + }, + options: [ + { + displayName: 'Channel Names or IDs', + name: 'channelIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: [], + description: + 'A comma-separated string of encoded channel IDs for which the User Group uses as a default. Choose from the list, or specify IDs using an expression.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A short description of the User Group', + }, + { + displayName: 'Handle', + name: 'handle', + type: 'string', + default: '', + description: 'A mention handle. Must be unique among channels, users and User Groups.', + }, + { + displayName: 'Include Count', + name: 'include_count', + type: 'boolean', + default: true, + description: 'Whether to include the number of users in each User Group', + }, + ], + }, + /* ----------------------------------------------------------------------- */ + /* userGroup:disable */ + /* ----------------------------------------------------------------------- */ + { + displayName: 'User Group ID', + name: 'userGroupId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: ['disable'], + resource: ['userGroup'], + }, + }, + required: true, + description: 'The encoded ID of the User Group to update', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['userGroup'], + operation: ['disable'], + }, + }, + options: [ + { + displayName: 'Include Count', + name: 'include_count', + type: 'boolean', + default: true, + description: 'Whether to include the number of users in each User Group', + }, + ], + }, + /* ----------------------------------------------------------------------- */ + /* userGroup:enable */ + /* ----------------------------------------------------------------------- */ + { + displayName: 'User Group ID', + name: 'userGroupId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: ['enable'], + resource: ['userGroup'], + }, + }, + required: true, + description: 'The encoded ID of the User Group to update', + }, + { + displayName: 'Options', + name: 'option', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['userGroup'], + operation: ['enable'], + }, + }, + options: [ + { + displayName: 'Include Count', + name: 'include_count', + type: 'boolean', + default: true, + description: 'Whether to include the number of users in each User Group', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* userGroup:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['userGroup'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['userGroup'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'Max number of results to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['userGroup'], + operation: ['getAll'], + }, + }, + options: [ + { + displayName: 'Include Count', + name: 'include_count', + type: 'boolean', + default: true, + description: 'Whether to include the number of users in each User Group', + }, + { + displayName: 'Include Disabled', + name: 'include_disabled', + type: 'boolean', + default: true, + description: 'Whether to include disabled User Groups', + }, + { + displayName: 'Include Users', + name: 'include_users', + type: 'boolean', + default: true, + description: 'Whether to include the list of users for each User Group', + }, + ], + }, + /* ----------------------------------------------------------------------- */ + /* userGroup:update */ + /* ----------------------------------------------------------------------- */ + { + displayName: 'User Group ID', + name: 'userGroupId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: ['update'], + resource: ['userGroup'], + }, + }, + required: true, + description: 'The encoded ID of the User Group to update', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['userGroup'], + operation: ['update'], + }, + }, + options: [ + { + displayName: 'Channel Names or IDs', + name: 'channels', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: [], + description: + 'A comma-separated string of encoded channel IDs for which the User Group uses as a default. Choose from the list, or specify IDs using an expression.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A short description of the User Group', + }, + { + displayName: 'Handle', + name: 'handle', + type: 'string', + default: '', + description: 'A mention handle. Must be unique among channels, users and User Groups.', + }, + { + displayName: 'Include Count', + name: 'include_count', + type: 'boolean', + default: true, + description: 'Whether to include the number of users in each User Group', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'A name for the User Group. Must be unique among User Groups.', + }, + ], + }, +];