From fe36c9c76a40ecdeed27f47232a7a6782f0bbd9a Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 5 Mar 2020 18:25:18 -0500 Subject: [PATCH] :zap: setup --- packages/cli/src/Server.ts | 2 +- packages/core/src/NodeExecuteFunctions.ts | 32 +- .../credentials/SlackOAuth2Api.credentials.ts | 47 + .../nodes/Slack/ChannelDescription.ts | 209 +++++ .../nodes/Slack/ConversationDescription.ts | 846 ++++++++++++++++++ .../nodes/Slack/GenericFunctions.ts | 77 ++ .../nodes/Slack/MessageDescription.ts | 843 +++++++++++++++++ .../nodes/Slack/MessageInterface.ts | 7 + packages/nodes-base/nodes/Slack/Slack.node.ts | 714 +++------------ packages/nodes-base/package.json | 1 + 10 files changed, 2167 insertions(+), 611 deletions(-) create mode 100644 packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Slack/ChannelDescription.ts create mode 100644 packages/nodes-base/nodes/Slack/ConversationDescription.ts create mode 100644 packages/nodes-base/nodes/Slack/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Slack/MessageDescription.ts create mode 100644 packages/nodes-base/nodes/Slack/MessageInterface.ts diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 4fe1ef70d7..bafe3120e2 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -203,7 +203,7 @@ class App { }); } - jwt.verify(token, getKey, {}, (err: Error, decoded: string) => { + jwt.verify(token, getKey, {}, (err: Error) => { if (err) return ResponseHelper.jwtAuthAuthorizationError(res, "Invalid token"); next(); diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 69dd2c92e1..f73eb2f068 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -115,7 +115,7 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m * @param {IWorkflowExecuteAdditionalData} additionalData * @returns */ -export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData) { +export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, tokenType?: string | undefined, property?: string) { const credentials = this.getCredentials(credentialsType) as ICredentialDataDecryptedObject; if (credentials === undefined) { @@ -133,8 +133,8 @@ export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string }); const oauthTokenData = credentials.oauthTokenData as clientOAuth2.Data; - const token = oAuthClient.createToken(oauthTokenData); + const token = oAuthClient.createToken(get(oauthTokenData, property as string) || oauthTokenData.accessToken, oauthTokenData.refresToken, tokenType || oauthTokenData.tokenType, oauthTokenData); // Signs the request by adding authorization headers or query parameters depending // on the token-type used. const newRequestOptions = token.sign(requestOptions as clientOAuth2.RequestObject); @@ -412,8 +412,8 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, }, @@ -466,8 +466,8 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, }, @@ -547,8 +547,8 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, }, @@ -629,8 +629,8 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, }, }; @@ -679,8 +679,8 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, }, }; @@ -737,8 +737,8 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, }, }; @@ -822,8 +822,8 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, }, diff --git a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts new file mode 100644 index 0000000000..45399a3d77 --- /dev/null +++ b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts @@ -0,0 +1,47 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +//https://api.slack.com/authentication/oauth-v2 + +export class SlackOAuth2Api implements ICredentialType { + name = 'slackOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Slack OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://slack.com/oauth/v2/authorize', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://slack.com/api/oauth.v2.access', + }, + //https://api.slack.com/scopes + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'chat:write', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'user_scope=chat:write', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Slack/ChannelDescription.ts b/packages/nodes-base/nodes/Slack/ChannelDescription.ts new file mode 100644 index 0000000000..88565716d5 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/ChannelDescription.ts @@ -0,0 +1,209 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const channelOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'channel', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Initiates a public or private channel-based conversation', + }, + { + name: 'Invite', + value: 'invite', + description: 'Invite a user to a channel', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const channelFields = [ + +/* -------------------------------------------------------------------------- */ +/* channel:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'channel', + type: 'string', + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'create' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The name of the channel to create.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + }, + { + displayName: 'Users', + name: 'users', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* channel:invite */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel ID', + name: 'channel', + type: 'string', + default: '', + placeholder: 'myChannel', + displayOptions: { + show: { + operation: [ + 'invite' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The ID of the channel to invite user to.', + }, + { + displayName: 'User ID', + name: 'username', + type: 'string', + default: '', + placeholder: 'frank', + displayOptions: { + show: { + operation: [ + 'invite' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The ID of the user to invite into channel.', + }, +/* -------------------------------------------------------------------------- */ +/* channel:get */ +/* -------------------------------------------------------------------------- */ + +/* -------------------------------------------------------------------------- */ +/* channel:delete */ +/* -------------------------------------------------------------------------- */ + +/* -------------------------------------------------------------------------- */ +/* channel:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned 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: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/ConversationDescription.ts b/packages/nodes-base/nodes/Slack/ConversationDescription.ts new file mode 100644 index 0000000000..62628cc654 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/ConversationDescription.ts @@ -0,0 +1,846 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const conversationOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + authentication: [ + 'accessToken', + ], + resource: [ + 'conversation', + ], + }, + }, + options: [ + { + name: 'Post', + value: 'post', + description: 'Post a conversation into a channel', + }, + ], + default: 'post', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const conversationFields = [ + +/* -------------------------------------------------------------------------- */ +/* conversation:post */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channel', + type: 'string', + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'conversation', + ], + }, + }, + required: true, + description: 'The channel to send the conversation to.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'conversation', + ], + }, + }, + description: 'The text to send.', + }, + { + displayName: 'As User', + name: 'as_user', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'conversation', + ], + }, + }, + description: 'Post the conversation as authenticated user instead of bot.', + }, + { + displayName: 'User Name', + name: 'username', + type: 'string', + default: '', + displayOptions: { + show: { + as_user: [ + false + ], + operation: [ + 'post' + ], + resource: [ + 'conversation', + ], + }, + }, + description: 'Set the bot\'s user name.', + }, + { + displayName: 'Attachments', + name: 'attachments', + type: 'collection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add attachment', + }, + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'conversation', + ], + }, + }, + default: {}, // TODO: Remove comment: has to make default array for the main property, check where that happens in UI + description: 'The attachment to add', + 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: '', + description: 'Text to send.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Title of the conversation.', + }, + { + displayName: 'Title Link', + name: 'title_link', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Link of the title.', + }, + { + 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 conversation 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: '', + description: 'Link for the author.', + }, + { + 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: '', + description: 'URL of image.', + }, + { + displayName: 'Thumbnail URL', + name: 'thumb_url', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'URL of thumbnail.', + }, + { + 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: 'Timestamp', + name: 'ts', + type: 'dateTime', + default: '', + description: 'Time conversation relates to.', + }, + { + displayName: 'Fields', + name: 'fields', + placeholder: 'Add Fields', + description: 'Fields to add to conversation.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'item', + displayName: 'Item', + values: [ + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the item.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the item.', + }, + { + displayName: 'Short', + name: 'short', + type: 'boolean', + default: true, + description: 'If items can be displayed next to each other.', + }, + ] + }, + ], + } + ], + }, + { + displayName: 'Other Options', + name: 'otherOptions', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'conversation', + ], + }, + }, + default: {}, + description: 'Other options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Icon Emoji', + name: 'icon_emoji', + type: 'string', + displayOptions: { + show: { + '/as_user': [ + false + ], + '/operation': [ + 'post' + ], + '/resource': [ + 'conversation', + ], + }, + }, + default: '', + description: 'Emoji to use as the icon for this conversation. Overrides icon_url.', + }, + { + displayName: 'Icon URL', + name: 'icon_url', + type: 'string', + displayOptions: { + show: { + '/as_user': [ + false + ], + '/operation': [ + 'post' + ], + '/resource': [ + 'conversation', + ], + }, + }, + default: '', + description: 'URL to an image to use as the icon for this conversation.', + }, + { + displayName: 'Make Reply', + name: 'thread_ts', + type: 'string', + default: '', + description: 'Provide another conversation\'s ts value to make this conversation a reply.', + }, + { + 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: '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: 'Link Names', + name: 'link_names', + type: 'boolean', + default: false, + description: 'Find and link channel names and usernames.', + }, + ], + }, +/* ----------------------------------------------------------------------- */ +/* conversation:update */ +/* ----------------------------------------------------------------------- */ + { + displayName: 'conversation ID', + name: 'conversationId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'update', + ] + }, + }, + description: 'Id of conversation that needs to be fetched', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Activity Date', + name: 'activityDate', + type: 'dateTime', + default: '', + description: `Represents the due date of the conversation.
+ This field has a timestamp that is always set to midnight
+ in the Coordinated Universal Time (UTC) time zone.`, + }, + { + displayName: 'Call Disposition', + name: 'callDisposition', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Represents the result of a given call, for example, “we'll call back,” or “call
+ unsuccessful.” Limit is 255 characters. Not subject to field-level security, available for any user
+ in an organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Call Duration In Seconds', + name: 'callDurationInSeconds', + type: 'number', + default: '', + description: `Duration of the call in seconds. Not subject to field-level security,
+ available for any user in an organization with Salesforce CRM Call Cente`, + }, + { + displayName: 'Call Object', + name: 'callObject', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Name of a call center. Limit is 255 characters.
+ Not subject to field-level security, available for any user in an
+ organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Call Type', + name: 'callType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getconversationCallTypes', + }, + description: 'The type of call being answered: Inbound, Internal, or Outbound.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Contains a text description of the conversation.', + }, + { + displayName: 'Is ReminderSet', + name: 'isReminderSet', + type: 'boolean', + default: false, + description: 'Indicates whether a popup reminder has been set for the conversation (true) or not (false).', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the User who owns the record.', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getconversationPriorities', + }, + description: `Indicates the importance or urgency of a conversation, such as high or low.`, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getconversationStatuses', + }, + description: 'The current status of the conversation, such as In Progress or Completed.', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getconversationSubjects', + }, + description: 'The subject line of the conversation, such as “Call” or “Send Quote.” Limit: 255 characters.', + }, + { + displayName: 'Recurrence Day Of Month', + name: 'recurrenceDayOfMonth', + type: 'number', + default: '', + description: 'The day of the month in which the conversation repeats.', + }, + { + displayName: 'Recurrence Day Of Week Mask', + name: 'recurrenceDayOfWeekMask', + type: 'number', + default: '', + description: `The day or days of the week on which the conversation repeats.
+ This field contains a bitmask. The values are as follows: Sunday = 1 Monday = 2
+ Tuesday = 4 Wednesday = 8 Thursday = 16 Friday = 32 Saturday = 64
+ Multiple days are represented as the sum of their numerical values.
+ For example, Tuesday and Thursday = 4 + 16 = 20.`, + }, + { + displayName: 'Recurrence End Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The last date on which the conversation repeats. This field has a timestamp that
+ is always set to midnight in the Coordinated Universal Time (UTC) time zone.`, + }, + { + displayName: 'Recurrence Instance', + name: 'recurrenceInstance', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getconversationRecurrenceInstances', + }, + default: '', + description: `The frequency of the recurring conversation. For example, “2nd” or “3rd.”`, + }, + { + displayName: 'Recurrence Interval', + name: 'recurrenceInterval', + type: 'number', + default: '', + description: 'The interval between recurring conversations.', + }, + { + displayName: 'Recurrence Month Of Year', + name: 'recurrenceMonthOfYear', + type: 'options', + options: [ + { + name: 'January', + value: 'January' + }, + { + name: 'February', + value: 'February' + }, + { + name: 'March', + value: 'March' + }, + { + name: 'April', + value: 'April' + }, + { + name: 'May', + value: 'May' + }, + { + name: 'June', + value: 'June' + }, + { + name: 'July', + value: 'July' + }, + { + name: 'August', + value: 'August' + }, + { + name: 'September', + value: 'September' + }, + { + name: 'October', + value: 'October' + }, + { + name: 'November', + value: 'November' + }, + { + name: 'December', + value: 'December' + } + ], + default: '', + description: 'The month of the year in which the conversation repeats.', + }, + { + displayName: 'Recurrence Start Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The date when the recurring conversation begins.
+ Must be a date and time before RecurrenceEndDateOnly.`, + }, + { + displayName: 'Recurrence Regenerated Type', + name: 'recurrenceRegeneratedType', + type: 'options', + default: '', + options: [ + { + name: 'After due date', + value: 'RecurrenceRegenerateAfterDueDate' + }, + { + name: 'After date completed', + value: 'RecurrenceRegenerateAfterToday' + }, + { + name: '(conversation Closed)', + value: 'RecurrenceRegenerated' + } + ], + description: `Represents what triggers a repeating conversation to repeat.
+ Add this field to a page layout together with the RecurrenceInterval field,
+ which determines the number of days between the triggering date (due date or close date)
+ and the due date of the next repeating conversation in the series.Label is Repeat This conversation.`, + }, + { + displayName: 'Recurrence Type', + name: 'recurrenceType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getconversationRecurrenceTypes' + }, + description: 'Website for the conversation.', + }, + { + displayName: 'Recurrence TimeZone SidKey', + name: 'recurrenceTimeZoneSidKey', + type: 'string', + default: '', + description: `The time zone associated with the recurring conversation.
+ For example, “UTC-8:00” for Pacific Standard Time.`, + }, + { + displayName: 'Reminder Date Time', + name: 'reminderDateTime', + type: 'dateTime', + default: '', + description: `Represents the time when the reminder is scheduled to fire,
+ if IsReminderSet is set to true. If IsReminderSet is set to false, then the
+ user may have deselected the reminder checkbox in the Salesforce user interface,
+ or the reminder has already fired at the time indicated by the value.`, + }, + { + displayName: 'What Id', + name: 'whatId', + type: 'string', + default: '', + description: `The WhatId represents nonhuman objects such as accounts, opportunities,
+ campaigns, cases, or custom objects. WhatIds are polymorphic. Polymorphic means a
+ WhatId is equivalent to the ID of a related object.`, + }, + { + displayName: 'Who Id', + name: 'whoId', + type: 'string', + default: '', + description: `The WhoId represents a human such as a lead or a contact.
+ WhoIds are polymorphic. Polymorphic means a WhoId is equivalent to a contact’s ID or a lead’s ID.`, + }, + ] + }, + +/* -------------------------------------------------------------------------- */ +/* conversation:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'conversation ID', + name: 'conversationId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'get', + ] + }, + }, + description: 'Id of conversation that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* conversation:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'conversation ID', + name: 'conversationId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'delete', + ] + }, + }, + description: 'Id of conversation that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* conversation:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts new file mode 100644 index 0000000000..57b5b00c11 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -0,0 +1,77 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { + IDataObject + } from 'n8n-workflow'; + import * as _ from 'lodash'; + +export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: object = {}, query: object = {}): Promise { // tslint:disable-line:no-any + const authenticationMethod = this.getNodeParameter('authentication', 0, 'accessToken') as string; + const options: OptionsWithUri = { + method, + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body, + qs: query, + uri: `https://slack.com/api${resource}`, + json: true + }; + if (Object.keys(body).length === 0) { + delete options.body; + } + if (Object.keys(query).length === 0) { + delete options.qs; + } + try { + if (authenticationMethod === 'accessToken') { + const credentials = this.getCredentials('slackApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + options.headers!.Authorization = `Bearer ${credentials.accessToken}`; + //@ts-ignore + return await this.helpers.request(options); + } else { + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'slackOAuth2Api', options, 'bearer', 'authed_user.access_token'); + } + } catch (error) { + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The Slack credentials are not valid!'); + } + + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`Slack error response [${error.statusCode}]: ${error.response.body.message}`); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} + +export async function salckApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + const returnData: IDataObject[] = []; + let responseData; + do { + responseData = await slackApiRequest.call(this, method, endpoint, body, query); + query.cursor = encodeURIComponent(_.get(responseData, 'response_metadata.next_cursor')); + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.response_metadata !== undefined && + responseData.response_metadata.mext_cursor !== undefined && + responseData.response_metadata.next_cursor !== "" && + responseData.response_metadata.next_cursor !== null + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts new file mode 100644 index 0000000000..7f9dff3ac3 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -0,0 +1,843 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const messageOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'message', + ], + }, + }, + options: [ + { + name: 'Post', + value: 'post', + description: 'Post a message into a channel', + }, + ], + default: 'post', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const messageFields = [ + +/* -------------------------------------------------------------------------- */ +/* message:post */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channel', + type: 'string', + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + required: true, + description: 'The channel to send the message to.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + description: 'The text to send.', + }, + { + displayName: 'As User', + name: 'as_user', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + description: 'Post the message as authenticated user instead of bot.', + }, + { + displayName: 'User Name', + name: 'username', + type: 'string', + default: '', + displayOptions: { + show: { + as_user: [ + false + ], + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + description: 'Set the bot\'s user name.', + }, + { + displayName: 'Attachments', + name: 'attachments', + type: 'collection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add attachment', + }, + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + default: {}, // TODO: Remove comment: has to make default array for the main property, check where that happens in UI + description: 'The attachment to add', + 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: '', + description: 'Text to send.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Title of the message.', + }, + { + displayName: 'Title Link', + name: 'title_link', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Link of the title.', + }, + { + 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: '', + description: 'Link for the author.', + }, + { + 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: '', + description: 'URL of image.', + }, + { + displayName: 'Thumbnail URL', + name: 'thumb_url', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'URL of thumbnail.', + }, + { + 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: 'Timestamp', + name: 'ts', + type: 'dateTime', + default: '', + description: 'Time message relates to.', + }, + { + 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: '', + description: 'Title of the item.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the item.', + }, + { + displayName: 'Short', + name: 'short', + type: 'boolean', + default: true, + description: 'If items can be displayed next to each other.', + }, + ] + }, + ], + } + ], + }, + { + displayName: 'Other Options', + name: 'otherOptions', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + default: {}, + description: 'Other options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Icon Emoji', + name: 'icon_emoji', + type: 'string', + displayOptions: { + show: { + '/as_user': [ + false + ], + '/operation': [ + 'post' + ], + '/resource': [ + 'message', + ], + }, + }, + default: '', + description: 'Emoji to use as the icon for this message. Overrides icon_url.', + }, + { + displayName: 'Icon URL', + name: 'icon_url', + type: 'string', + displayOptions: { + show: { + '/as_user': [ + false + ], + '/operation': [ + 'post' + ], + '/resource': [ + 'message', + ], + }, + }, + default: '', + description: 'URL to an image to use as the icon for this message.', + }, + { + displayName: 'Make Reply', + name: 'thread_ts', + type: 'string', + default: '', + description: 'Provide another message\'s ts value to make this message a reply.', + }, + { + 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: '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: 'Link Names', + name: 'link_names', + type: 'boolean', + default: false, + description: 'Find and link channel names and usernames.', + }, + ], + }, +/* ----------------------------------------------------------------------- */ +/* message:update */ +/* ----------------------------------------------------------------------- */ + { + displayName: 'message ID', + name: 'messageId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ] + }, + }, + description: 'Id of message that needs to be fetched', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Activity Date', + name: 'activityDate', + type: 'dateTime', + default: '', + description: `Represents the due date of the message.
+ This field has a timestamp that is always set to midnight
+ in the Coordinated Universal Time (UTC) time zone.`, + }, + { + displayName: 'Call Disposition', + name: 'callDisposition', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Represents the result of a given call, for example, “we'll call back,” or “call
+ unsuccessful.” Limit is 255 characters. Not subject to field-level security, available for any user
+ in an organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Call Duration In Seconds', + name: 'callDurationInSeconds', + type: 'number', + default: '', + description: `Duration of the call in seconds. Not subject to field-level security,
+ available for any user in an organization with Salesforce CRM Call Cente`, + }, + { + displayName: 'Call Object', + name: 'callObject', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Name of a call center. Limit is 255 characters.
+ Not subject to field-level security, available for any user in an
+ organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Call Type', + name: 'callType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getmessageCallTypes', + }, + description: 'The type of call being answered: Inbound, Internal, or Outbound.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Contains a text description of the message.', + }, + { + displayName: 'Is ReminderSet', + name: 'isReminderSet', + type: 'boolean', + default: false, + description: 'Indicates whether a popup reminder has been set for the message (true) or not (false).', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the User who owns the record.', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getmessagePriorities', + }, + description: `Indicates the importance or urgency of a message, such as high or low.`, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getmessageStatuses', + }, + description: 'The current status of the message, such as In Progress or Completed.', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getmessageSubjects', + }, + description: 'The subject line of the message, such as “Call” or “Send Quote.” Limit: 255 characters.', + }, + { + displayName: 'Recurrence Day Of Month', + name: 'recurrenceDayOfMonth', + type: 'number', + default: '', + description: 'The day of the month in which the message repeats.', + }, + { + displayName: 'Recurrence Day Of Week Mask', + name: 'recurrenceDayOfWeekMask', + type: 'number', + default: '', + description: `The day or days of the week on which the message repeats.
+ This field contains a bitmask. The values are as follows: Sunday = 1 Monday = 2
+ Tuesday = 4 Wednesday = 8 Thursday = 16 Friday = 32 Saturday = 64
+ Multiple days are represented as the sum of their numerical values.
+ For example, Tuesday and Thursday = 4 + 16 = 20.`, + }, + { + displayName: 'Recurrence End Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The last date on which the message repeats. This field has a timestamp that
+ is always set to midnight in the Coordinated Universal Time (UTC) time zone.`, + }, + { + displayName: 'Recurrence Instance', + name: 'recurrenceInstance', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getmessageRecurrenceInstances', + }, + default: '', + description: `The frequency of the recurring message. For example, “2nd” or “3rd.”`, + }, + { + displayName: 'Recurrence Interval', + name: 'recurrenceInterval', + type: 'number', + default: '', + description: 'The interval between recurring messages.', + }, + { + displayName: 'Recurrence Month Of Year', + name: 'recurrenceMonthOfYear', + type: 'options', + options: [ + { + name: 'January', + value: 'January' + }, + { + name: 'February', + value: 'February' + }, + { + name: 'March', + value: 'March' + }, + { + name: 'April', + value: 'April' + }, + { + name: 'May', + value: 'May' + }, + { + name: 'June', + value: 'June' + }, + { + name: 'July', + value: 'July' + }, + { + name: 'August', + value: 'August' + }, + { + name: 'September', + value: 'September' + }, + { + name: 'October', + value: 'October' + }, + { + name: 'November', + value: 'November' + }, + { + name: 'December', + value: 'December' + } + ], + default: '', + description: 'The month of the year in which the message repeats.', + }, + { + displayName: 'Recurrence Start Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The date when the recurring message begins.
+ Must be a date and time before RecurrenceEndDateOnly.`, + }, + { + displayName: 'Recurrence Regenerated Type', + name: 'recurrenceRegeneratedType', + type: 'options', + default: '', + options: [ + { + name: 'After due date', + value: 'RecurrenceRegenerateAfterDueDate' + }, + { + name: 'After date completed', + value: 'RecurrenceRegenerateAfterToday' + }, + { + name: '(message Closed)', + value: 'RecurrenceRegenerated' + } + ], + description: `Represents what triggers a repeating message to repeat.
+ Add this field to a page layout together with the RecurrenceInterval field,
+ which determines the number of days between the triggering date (due date or close date)
+ and the due date of the next repeating message in the series.Label is Repeat This message.`, + }, + { + displayName: 'Recurrence Type', + name: 'recurrenceType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getmessageRecurrenceTypes' + }, + description: 'Website for the message.', + }, + { + displayName: 'Recurrence TimeZone SidKey', + name: 'recurrenceTimeZoneSidKey', + type: 'string', + default: '', + description: `The time zone associated with the recurring message.
+ For example, “UTC-8:00” for Pacific Standard Time.`, + }, + { + displayName: 'Reminder Date Time', + name: 'reminderDateTime', + type: 'dateTime', + default: '', + description: `Represents the time when the reminder is scheduled to fire,
+ if IsReminderSet is set to true. If IsReminderSet is set to false, then the
+ user may have deselected the reminder checkbox in the Salesforce user interface,
+ or the reminder has already fired at the time indicated by the value.`, + }, + { + displayName: 'What Id', + name: 'whatId', + type: 'string', + default: '', + description: `The WhatId represents nonhuman objects such as accounts, opportunities,
+ campaigns, cases, or custom objects. WhatIds are polymorphic. Polymorphic means a
+ WhatId is equivalent to the ID of a related object.`, + }, + { + displayName: 'Who Id', + name: 'whoId', + type: 'string', + default: '', + description: `The WhoId represents a human such as a lead or a contact.
+ WhoIds are polymorphic. Polymorphic means a WhoId is equivalent to a contact’s ID or a lead’s ID.`, + }, + ] + }, + +/* -------------------------------------------------------------------------- */ +/* message:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'message ID', + name: 'messageId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'get', + ] + }, + }, + description: 'Id of message that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* message:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'message ID', + name: 'messageId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'delete', + ] + }, + }, + description: 'Id of message that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* message:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/MessageInterface.ts b/packages/nodes-base/nodes/Slack/MessageInterface.ts new file mode 100644 index 0000000000..caff7c0765 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/MessageInterface.ts @@ -0,0 +1,7 @@ + +export interface IAttachment { + fields: { + item?: object[]; + }; +} + diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index c20f8ba12b..c3b074c1e1 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -1,16 +1,33 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { + IExecuteFunctions, + } from 'n8n-core'; import { IDataObject, INodeTypeDescription, INodeExecutionData, INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, } from 'n8n-workflow'; - -interface Attachment { - fields: { - item?: object[]; - }; -} +import { + channelOperations, + channelFields, +} from './ChannelDescription'; +import { + messageOperations, + messageFields, +} from './MessageDescription'; +import { + conversationOperations, + conversationFields, +} from './ConversationDescription'; +import { + slackApiRequest, + salckApiRequestAllItems, +} from './GenericFunctions'; +import { + IAttachment, +} from './MessageInterface'; export class Slack implements INodeType { description: INodeTypeDescription = { @@ -31,9 +48,44 @@ export class Slack implements INodeType { { name: 'slackApi', required: true, + displayOptions: { + show: { + authentication: [ + 'accessToken', + ], + }, + }, + }, + { + name: 'slackOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oauth2', + ], + }, + }, } ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Access Token', + value: 'accessToken', + }, + { + name: 'OAuth2', + value: 'oauth2', + }, + ], + default: 'accessToken', + description: 'The resource to operate on.', + }, { displayName: 'Resource', name: 'resource', @@ -51,586 +103,77 @@ export class Slack implements INodeType { default: 'message', description: 'The resource to operate on.', }, - - - - // ---------------------------------- - // operations - // ---------------------------------- - { - displayName: 'Operation', - name: 'operation', - type: 'options', - displayOptions: { - show: { - resource: [ - 'channel', - ], - }, - }, - options: [ - { - name: 'Create', - value: 'create', - description: 'Create a new channel', - }, - { - name: 'Invite', - value: 'invite', - description: 'Invite a user to a channel', - }, - ], - default: 'create', - description: 'The operation to perform.', - }, - - { - displayName: 'Operation', - name: 'operation', - type: 'options', - displayOptions: { - show: { - resource: [ - 'message', - ], - }, - }, - options: [ - { - name: 'Post', - value: 'post', - description: 'Post a message into a channel', - }, - ], - default: 'post', - description: 'The operation to perform.', - }, - - - - // ---------------------------------- - // channel - // ---------------------------------- - - // ---------------------------------- - // channel:create - // ---------------------------------- - { - displayName: 'Name', - name: 'channel', - type: 'string', - default: '', - placeholder: 'Channel name', - displayOptions: { - show: { - operation: [ - 'create' - ], - resource: [ - 'channel', - ], - }, - }, - required: true, - description: 'The name of the channel to create.', - }, - - // ---------------------------------- - // channel:invite - // ---------------------------------- - { - displayName: 'Channel ID', - name: 'channel', - type: 'string', - default: '', - placeholder: 'myChannel', - displayOptions: { - show: { - operation: [ - 'invite' - ], - resource: [ - 'channel', - ], - }, - }, - required: true, - description: 'The ID of the channel to invite user to.', - }, - { - displayName: 'User ID', - name: 'username', - type: 'string', - default: '', - placeholder: 'frank', - displayOptions: { - show: { - operation: [ - 'invite' - ], - resource: [ - 'channel', - ], - }, - }, - required: true, - description: 'The ID of the user to invite into channel.', - }, - - - - // ---------------------------------- - // message - // ---------------------------------- - - // ---------------------------------- - // message:post - // ---------------------------------- - { - displayName: 'Channel', - name: 'channel', - type: 'string', - default: '', - placeholder: 'Channel name', - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'message', - ], - }, - }, - required: true, - description: 'The channel to send the message to.', - }, - { - displayName: 'Text', - name: 'text', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'message', - ], - }, - }, - description: 'The text to send.', - }, - { - displayName: 'As User', - name: 'as_user', - type: 'boolean', - default: false, - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'message', - ], - }, - }, - description: 'Post the message as authenticated user instead of bot.', - }, - { - displayName: 'User Name', - name: 'username', - type: 'string', - default: '', - displayOptions: { - show: { - as_user: [ - false - ], - operation: [ - 'post' - ], - resource: [ - 'message', - ], - }, - }, - description: 'Set the bot\'s user name.', - }, - { - displayName: 'Attachments', - name: 'attachments', - type: 'collection', - typeOptions: { - multipleValues: true, - multipleValueButtonText: 'Add attachment', - }, - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'message', - ], - }, - }, - default: {}, // TODO: Remove comment: has to make default array for the main property, check where that happens in UI - description: 'The attachment to add', - 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: '', - description: 'Text to send.', - }, - { - displayName: 'Title', - name: 'title', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Title of the message.', - }, - { - displayName: 'Title Link', - name: 'title_link', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Link of the title.', - }, - { - 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: '', - description: 'Link for the author.', - }, - { - 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: '', - description: 'URL of image.', - }, - { - displayName: 'Thumbnail URL', - name: 'thumb_url', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'URL of thumbnail.', - }, - { - 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: 'Timestamp', - name: 'ts', - type: 'dateTime', - default: '', - description: 'Time message relates to.', - }, - { - 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: '', - description: 'Title of the item.', - }, - { - displayName: 'Value', - name: 'value', - type: 'string', - default: '', - description: 'Value of the item.', - }, - { - displayName: 'Short', - name: 'short', - type: 'boolean', - default: true, - description: 'If items can be displayed next to each other.', - }, - ] - }, - ], - } - ], - }, - { - displayName: 'Other Options', - name: 'otherOptions', - type: 'collection', - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'message', - ], - }, - }, - default: {}, - description: 'Other options to set', - placeholder: 'Add options', - options: [ - { - displayName: 'Icon Emoji', - name: 'icon_emoji', - type: 'string', - displayOptions: { - show: { - '/as_user': [ - false - ], - '/operation': [ - 'post' - ], - '/resource': [ - 'message', - ], - }, - }, - default: '', - description: 'Emoji to use as the icon for this message. Overrides icon_url.', - }, - { - displayName: 'Icon URL', - name: 'icon_url', - type: 'string', - displayOptions: { - show: { - '/as_user': [ - false - ], - '/operation': [ - 'post' - ], - '/resource': [ - 'message', - ], - }, - }, - default: '', - description: 'URL to an image to use as the icon for this message.', - }, - { - displayName: 'Make Reply', - name: 'thread_ts', - type: 'string', - default: '', - description: 'Provide another message\'s ts value to make this message a reply.', - }, - { - 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: '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: 'Link Names', - name: 'link_names', - type: 'boolean', - default: false, - description: 'Find and link channel names and usernames.', - }, - ], - }, + ...channelOperations, + ...channelFields, + ...messageOperations, + ...messageFields, ], }; - + 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 salckApiRequestAllItems.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, + }); + } + console.log(users) + return returnData; + }, + } + }; async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; - - const credentials = this.getCredentials('slackApi'); - - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - - const baseUrl = `https://slack.com/api/`; - let operation: string; - let resource: string; - let requestMethod = 'POST'; - - // For Post - let body: IDataObject; - // For Query string + const length = items.length as unknown as number; let qs: IDataObject; - - for (let i = 0; i < items.length; i++) { - let endpoint = ''; - body = {}; + let responseData; + for (let i = 0; i < length; i++) { qs = {}; - - resource = this.getNodeParameter('resource', i) as string; - operation = this.getNodeParameter('operation', i) as string; - + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; if (resource === 'channel') { + //https://api.slack.com/methods/conversations.create if (operation === 'create') { - // ---------------------------------- - // channel:create - // ---------------------------------- - - requestMethod = 'POST'; - endpoint = 'channels.create'; - - body.name = this.getNodeParameter('channel', i) as string; - } else if (operation === 'invite') { - // ---------------------------------- - // channel:invite - // ---------------------------------- - - requestMethod = 'POST'; - endpoint = 'channels.invite'; - - body.channel = this.getNodeParameter('channel', i) as string; - body.user = this.getNodeParameter('username', i) as string; + const channel = this.getNodeParameter('channel', i) as string; + const body: IDataObject = { + name: channel, + }; + responseData = await slackApiRequest.call(this, 'POST', '/channels.create', body, qs); } - } else if (resource === 'message') { + if (operation === 'invite') { + const channel = this.getNodeParameter('channel', i) as string; + const user = this.getNodeParameter('username', i) as string; + const body: IDataObject = { + channel, + user, + }; + responseData = await slackApiRequest.call(this, 'POST', '/channels.invite', body, qs); + } + } + if (resource === 'message') { if (operation === 'post') { - // ---------------------------------- - // message:post - // ---------------------------------- - - requestMethod = 'POST'; - endpoint = 'chat.postMessage'; - - body.channel = this.getNodeParameter('channel', i) as string; - body.text = this.getNodeParameter('text', i) as string; - body.as_user = this.getNodeParameter('as_user', i) as boolean; - if (body.as_user === false) { + const channel = this.getNodeParameter('channel', i) as string; + const text = this.getNodeParameter('text', i) as string; + const attachments = this.getNodeParameter('attachments', i, []) as unknown as IAttachment[]; + const as_user = this.getNodeParameter('as_user', i) as boolean; + const body: IDataObject = { + channel: channel, + text, + as_user, + }; + if (as_user === false) { body.username = this.getNodeParameter('username', i) as string; } - - const attachments = this.getNodeParameter('attachments', i, []) as unknown as Attachment[]; - // 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) { @@ -650,32 +193,15 @@ export class Slack implements INodeType { // Add all the other options to the request const otherOptions = this.getNodeParameter('otherOptions', i) as IDataObject; Object.assign(body, otherOptions); + responseData = await slackApiRequest.call(this, 'POST', '/chat.postMessage', body, qs); } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); } else { - throw new Error(`The resource "${resource}" is not known!`); + returnData.push(responseData as IDataObject); } - - const options = { - method: requestMethod, - body, - qs, - uri: `${baseUrl}/${endpoint}`, - headers: { - Authorization: `Bearer ${credentials.accessToken }`, - 'content-type': 'application/json; charset=utf-8' - }, - json: true - }; - - const responseData = await this.helpers.request(options); - - if (!responseData.ok) { - throw new Error(`Request to Slack did fail with error: "${responseData.error}"`); - } - - returnData.push(responseData as IDataObject); } - return [this.helpers.returnJsonArray(returnData)]; } } diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 746f91f723..45d5012c8e 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -68,6 +68,7 @@ "dist/credentials/ShopifyApi.credentials.js", "dist/credentials/SalesforceOAuth2Api.credentials.js", "dist/credentials/SlackApi.credentials.js", + "dist/credentials/SlackOAuth2Api.credentials.js", "dist/credentials/Smtp.credentials.js", "dist/credentials/StripeApi.credentials.js", "dist/credentials/TelegramApi.credentials.js",