From b5b60008d680cd843a418390d451743fc13cac9c Mon Sep 17 00:00:00 2001 From: Mike Quinlan Date: Tue, 19 Apr 2022 05:36:01 -0500 Subject: [PATCH] feat(Slack Node): Add blocks to slack message update (#2182) * Adding blocks to slack message update * Fixing lint * Adding blocks to slack message update * Fixing lint * :zap: added toggle to display json inputs in update operation * :zap: Improvements * feat(Markdown Node): Add new node to covert between Markdown <> HTML (#1728) * :sparkles: Markdown Node * Tweaked wording * :arrow_up: Bump showdown to latest version * :zap: Small improvement * :shirt: Fix linting issue * :zap: Small improvements * :hammer: added options, added continue on fail, some clean up * :zap: removed test code * :zap: added missing semicolumn * :hammer: wip * :hammer: replaced library for converting html to markdown, added options * :zap: lock file fix * :hammer: clean up Co-authored-by: sirdavidoff <1670123+sirdavidoff@users.noreply.github.com> Co-authored-by: Michael Kret Co-authored-by: Michael Kret Co-authored-by: ricardo Co-authored-by: Ricardo Espinoza Co-authored-by: sirdavidoff <1670123+sirdavidoff@users.noreply.github.com> Co-authored-by: Jan Oberhauser --- packages/cli/src/CredentialsHelper.ts | 25 +- .../credentials/SlackApi.credentials.ts | 26 +- .../nodes/Slack/GenericFunctions.ts | 26 +- .../nodes/Slack/MessageDescription.ts | 248 +++++++++++------- packages/nodes-base/nodes/Slack/Slack.node.ts | 65 ++--- packages/workflow/src/Interfaces.ts | 10 +- 6 files changed, 250 insertions(+), 150 deletions(-) diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index 2400b952c9418..230499adeffa5 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -6,7 +6,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ import { Credentials, NodeExecuteFunctions } from 'n8n-core'; - +// eslint-disable-next-line import/no-extraneous-dependencies +import { get } from 'lodash'; import { NodeVersionedType } from 'n8n-nodes-base'; import { @@ -633,8 +634,10 @@ export class CredentialsHelper extends ICredentialsHelper { mode, ); + let response: INodeExecutionData[][] | null | undefined; + try { - await routingNode.runNode( + response = await routingNode.runNode( inputData, runIndex, nodeTypeCopy, @@ -683,6 +686,24 @@ export class CredentialsHelper extends ICredentialsHelper { }; } + if ( + credentialTestFunction.testRequest.rules && + Array.isArray(credentialTestFunction.testRequest.rules) + ) { + // Special testing rules are defined so check all in order + for (const rule of credentialTestFunction.testRequest.rules) { + if (rule.type === 'responseSuccessBody') { + const responseData = response![0][0].json; + if (get(responseData, rule.properties.key) === rule.properties.value) { + return { + status: 'Error', + message: rule.properties.message, + }; + } + } + } + } + return { status: 'OK', message: 'Connection successful!', diff --git a/packages/nodes-base/credentials/SlackApi.credentials.ts b/packages/nodes-base/credentials/SlackApi.credentials.ts index ae32dabfb9544..588c4b8cb6883 100644 --- a/packages/nodes-base/credentials/SlackApi.credentials.ts +++ b/packages/nodes-base/credentials/SlackApi.credentials.ts @@ -1,9 +1,11 @@ import { + IAuthenticateBearer, + IAuthenticateQueryAuth, + ICredentialTestRequest, ICredentialType, INodeProperties, } from 'n8n-workflow'; - export class SlackApi implements ICredentialType { name = 'slackApi'; displayName = 'Slack API'; @@ -17,4 +19,26 @@ export class SlackApi implements ICredentialType { required: true, }, ]; + authenticate: IAuthenticateBearer = { + type: 'bearer', + properties: { + tokenPropertyName: 'accessToken', + }, + }; + test: ICredentialTestRequest = { + request: { + baseURL: 'https://slack.com', + url: '/api/users.profile.get', + }, + rules: [ + { + type: 'responseSuccessBody', + properties: { + key: 'ok', + value: false, + message: 'Invalid access token', + }, + }, + ], + }; } diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts index 6e6bdf799a864..bdb1c6a677d22 100644 --- a/packages/nodes-base/nodes/Slack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -11,6 +11,7 @@ import { import { IDataObject, IOAuth2Options, + JsonObject, NodeApiError, NodeOperationError, } from 'n8n-workflow'; @@ -36,23 +37,16 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu if (Object.keys(query).length === 0) { delete options.qs; } - try { - let response: any; // tslint:disable-line:no-any - if (authenticationMethod === 'accessToken') { - const credentials = await this.getCredentials('slackApi'); - options.headers!.Authorization = `Bearer ${credentials.accessToken}`; - //@ts-ignore - response = await this.helpers.request(options); - } else { + const oAuth2Options: IOAuth2Options = { + tokenType: 'Bearer', + property: 'authed_user.access_token', + }; - const oAuth2Options: IOAuth2Options = { - tokenType: 'Bearer', - property: 'authed_user.access_token', - }; - //@ts-ignore - response = await this.helpers.requestOAuth2.call(this, 'slackOAuth2Api', options, oAuth2Options); - } + try { + let response: any; // tslint:disable-line:no-any + const credentialType = authenticationMethod === 'accessToken' ? 'slackApi' : 'slackOAuth2Api'; + response = await this.helpers.requestWithAuthentication.call(this, credentialType, options, { oauth2: oAuth2Options }); if (response.ok === false) { if (response.error === 'paid_teams_only') { @@ -66,7 +60,7 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu return response; } catch (error) { - throw new NodeApiError(this.getNode(), error); + throw new NodeApiError(this.getNode(), error as JsonObject); } } diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index a1b680a6fb4a6..27e1c4aa8fbde 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -171,6 +171,97 @@ export const messageFields: INodeProperties[] = [ }, }, }, + { + displayName: 'Options', + name: 'otherOptions', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'post', + 'postEphemeral', + ], + resource: [ + 'message', + ], + }, + }, + default: {}, + description: 'Other options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Icon Emoji', + name: 'icon_emoji', + type: 'string', + default: '', + description: 'Emoji to use as the icon for this message. Overrides icon_url.', + }, + { + displayName: 'Icon URL', + name: 'icon_url', + type: 'string', + default: '', + description: 'URL to an image to use as the icon for this message.', + }, + { + displayName: 'Link Names', + name: 'link_names', + type: 'boolean', + default: false, + description: 'Find and link channel names and usernames.', + }, + { + displayName: 'Make Reply', + name: 'thread_ts', + type: 'string', + default: '', + description: 'Provide another message\'s ts value to make this message a reply.', + }, + { + displayName: 'Markdown', + name: 'mrkdwn', + type: 'boolean', + default: true, + description: 'Use Slack Markdown parsing.', + }, + { + displayName: 'Reply Broadcast', + name: 'reply_broadcast', + type: 'boolean', + default: false, + description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.', + }, + { + displayName: 'Unfurl Links', + name: 'unfurl_links', + type: 'boolean', + default: false, + description: 'Pass true to enable unfurling of primarily text-based content.', + }, + { + displayName: 'Unfurl Media', + name: 'unfurl_media', + type: 'boolean', + default: true, + description: 'Pass false to disable unfurling of media content.', + }, + { + 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).', + }, + ], + }, { displayName: 'Attachments', name: 'attachments', @@ -367,97 +458,6 @@ export const messageFields: INodeProperties[] = [ }, ], }, - { - displayName: 'Other Options', - name: 'otherOptions', - type: 'collection', - displayOptions: { - show: { - operation: [ - 'post', - 'postEphemeral', - ], - resource: [ - 'message', - ], - }, - }, - default: {}, - description: 'Other options to set', - placeholder: 'Add options', - options: [ - { - displayName: 'Icon Emoji', - name: 'icon_emoji', - type: 'string', - default: '', - description: 'Emoji to use as the icon for this message. Overrides icon_url.', - }, - { - displayName: 'Icon URL', - name: 'icon_url', - type: 'string', - default: '', - description: 'URL to an image to use as the icon for this message.', - }, - { - displayName: 'Link Names', - name: 'link_names', - type: 'boolean', - default: false, - description: 'Find and link channel names and usernames.', - }, - { - displayName: 'Make Reply', - name: 'thread_ts', - type: 'string', - default: '', - description: 'Provide another message\'s ts value to make this message a reply.', - }, - { - displayName: 'Markdown', - name: 'mrkdwn', - type: 'boolean', - default: true, - description: 'Use Slack Markdown parsing.', - }, - { - displayName: 'Reply Broadcast', - name: 'reply_broadcast', - type: 'boolean', - default: false, - description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.', - }, - { - displayName: 'Unfurl Links', - name: 'unfurl_links', - type: 'boolean', - default: false, - description: 'Pass true to enable unfurling of primarily text-based content.', - }, - { - displayName: 'Unfurl Media', - name: 'unfurl_media', - type: 'boolean', - default: true, - description: 'Pass false to disable unfurling of media content.', - }, - { - 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).', - }, - ], - }, /* ----------------------------------------------------------------------- */ /* message:update */ @@ -487,7 +487,7 @@ export const messageFields: INodeProperties[] = [ displayName: 'Text', name: 'text', type: 'string', - required: true, + required: false, default: '', displayOptions: { show: { @@ -519,6 +519,22 @@ export const messageFields: INodeProperties[] = [ }, description: `Timestamp of the message to be updated.`, }, + { + displayName: 'JSON parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'message', + ], + }, + }, + }, { displayName: 'Update Fields', name: 'updateFields', @@ -566,6 +582,54 @@ export const messageFields: INodeProperties[] = [ }, ], }, + { + displayName: 'Attachments', + name: 'attachmentsJson', + type: 'json', + default: '', + required: false, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + jsonParameters: [ + true, + ], + }, + }, + description: 'The attachments to add', + }, + { + displayName: 'Blocks', + name: 'blocksJson', + type: 'json', + default: '', + required: false, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + jsonParameters: [ + true, + ], + }, + }, + description: 'The blocks to add', + }, { displayName: 'Blocks', name: 'blocksUi', diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index 6115717cb66f7..af5a20c09b13f 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -1,4 +1,6 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { + IExecuteFunctions, +} from 'n8n-core'; import { ICredentialsDecrypted, @@ -10,6 +12,7 @@ import { INodePropertyOptions, INodeType, INodeTypeDescription, + JsonObject, NodeOperationError, } from 'n8n-workflow'; @@ -130,7 +133,6 @@ export class Slack implements INodeType { ], }, }, - testedBy: 'testSlackTokenAuth', }, { name: 'slackOAuth2Api', @@ -287,41 +289,6 @@ export class Slack implements INodeType { return returnData; }, }, - credentialTest: { - async testSlackTokenAuth(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { - - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - Authorization: `Bearer ${credential.data!.accessToken}`, - }, - uri: 'https://slack.com/api/users.profile.get', - json: true, - }; - - try { - const response = await this.helpers.request(options); - - if (!response.ok) { - return { - status: 'Error', - message: `${response.error}`, - }; - } - } catch (err) { - return { - status: 'Error', - message: `${err.message}`, - }; - } - - return { - status: 'OK', - message: 'Connection successful!', - }; - }, - }, }; async execute(this: IExecuteFunctions): Promise { @@ -840,6 +807,28 @@ export class Slack implements INodeType { } body['attachments'] = attachments; + const jsonParameters = this.getNodeParameter('jsonParameters', i, false) as boolean; + 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'); + } + 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'); + } + + if (attachmentsJson !== '') { + body.attachments = attachmentsJson; + } + } + // Add all the other options to the request const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; Object.assign(body, updateFields); @@ -1189,7 +1178,7 @@ export class Slack implements INodeType { } } catch (error) { if (this.continueOnFail()) { - returnData.push({ error: error.message }); + returnData.push({ error: (error as JsonObject).message }); continue; } throw error; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index c9b58b5795524..3389a01a769ef 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -248,9 +248,17 @@ export interface IAuthenticateRuleResponseCode extends IAuthenticateRuleBase { }; } +export interface IAuthenticateRuleResponseSuccessBody extends IAuthenticateRuleBase { + type: 'responseSuccessBody'; + properties: { + message: string; + key: string; + value: any; + }; +} export interface ICredentialTestRequest { request: IHttpRequestOptions; - rules?: IAuthenticateRuleResponseCode[]; + rules?: IAuthenticateRuleResponseCode[] | IAuthenticateRuleResponseSuccessBody[]; } export interface ICredentialTestRequestData {