From fe36c9c76a40ecdeed27f47232a7a6782f0bbd9a Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 5 Mar 2020 18:25:18 -0500 Subject: [PATCH 1/4] :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", From ba22ab02d8dda844cd571248ec207e28d705639e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 8 Mar 2020 18:22:33 -0400 Subject: [PATCH 2/4] :zap: Added missing resources --- .../credentials/SlackOAuth2Api.credentials.ts | 12 +- .../nodes/Slack/ChannelDescription.ts | 735 ++++++++++++++- .../nodes/Slack/ConversationDescription.ts | 846 ------------------ .../nodes-base/nodes/Slack/FileDescription.ts | 322 +++++++ .../nodes/Slack/GenericFunctions.ts | 19 +- .../nodes/Slack/MessageDescription.ts | 629 ++++++------- packages/nodes-base/nodes/Slack/Slack.node.ts | 408 ++++++++- .../nodes-base/nodes/Slack/StarDescription.ts | 185 ++++ 8 files changed, 1900 insertions(+), 1256 deletions(-) delete mode 100644 packages/nodes-base/nodes/Slack/ConversationDescription.ts create mode 100644 packages/nodes-base/nodes/Slack/FileDescription.ts create mode 100644 packages/nodes-base/nodes/Slack/StarDescription.ts diff --git a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts index 45399a3d77..6c5284ee44 100644 --- a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts @@ -4,6 +4,16 @@ import { } from 'n8n-workflow'; //https://api.slack.com/authentication/oauth-v2 +const userScopes = [ + 'chat:write', + 'conversations:history', + 'conversations:read', + 'files:read', + 'files:write', + 'stars:read', + 'stars:write', +] + export class SlackOAuth2Api implements ICredentialType { name = 'slackOAuth2Api'; @@ -35,7 +45,7 @@ export class SlackOAuth2Api implements ICredentialType { displayName: 'Auth URI Query Parameters', name: 'authQueryParameters', type: 'hidden' as NodePropertyTypes, - default: 'user_scope=chat:write', + default: `user_scope=${userScopes.join(' ')}`, }, { displayName: 'Authentication', diff --git a/packages/nodes-base/nodes/Slack/ChannelDescription.ts b/packages/nodes-base/nodes/Slack/ChannelDescription.ts index 88565716d5..cda5824105 100644 --- a/packages/nodes-base/nodes/Slack/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Slack/ChannelDescription.ts @@ -13,16 +13,86 @@ export const channelOperations = [ }, }, options: [ + { + name: 'Archive', + value: 'archive', + description: 'Archives a conversation.', + }, + { + name: 'Close', + value: 'close', + description: 'Closes a direct message or multi-person direct message.', + }, { name: 'Create', value: 'create', description: 'Initiates a public or private channel-based conversation', }, + { + name: 'Get', + value: 'get', + description: 'Get information about a channel.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all channels in a Slack team.', + }, + { + name: 'History', + value: 'history', + description: `Get a conversation's history of messages and events.`, + }, { name: 'Invite', value: 'invite', description: 'Invite a user to a channel', }, + { + name: 'Join', + value: 'join', + description: 'Joins an existing conversation.', + }, + { + name: 'Kick', + value: 'kick', + description: 'Removes a user from a channel.', + }, + { + name: 'Leave', + value: 'leave', + description: 'Leaves a conversation.', + }, + { + name: 'Open', + value: 'open', + description: 'Opens or resumes a direct message or multi-person direct message.', + }, + { + name: 'Rename', + value: 'rename', + description: 'Renames a conversation.', + }, + { + name: 'Replies', + value: 'replies', + description: 'Get a thread of messages posted to a channel', + }, + { + name: 'Set Purpose', + value: 'setPurpose', + description: 'Sets the purpose for a conversation.', + }, + { + name: 'Set Topic', + value: 'setTopic', + description: 'Sets the topic for a conversation.', + }, + { + name: 'Unarchive', + value: 'unarchive', + description: 'Unarchives a conversation.', + }, ], default: 'create', description: 'The operation to perform.', @@ -31,12 +101,60 @@ export const channelOperations = [ export const channelFields = [ +/* -------------------------------------------------------------------------- */ +/* channel:archive */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'archive' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'The name of the channel to archive.', + }, +/* -------------------------------------------------------------------------- */ +/* channel:close */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'close' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'The name of the channel to close.', + }, /* -------------------------------------------------------------------------- */ /* channel:create */ /* -------------------------------------------------------------------------- */ { - displayName: 'Name', - name: 'channel', + displayName: 'Channel', + name: 'channelId', type: 'string', default: '', placeholder: 'Channel name', @@ -75,6 +193,7 @@ export const channelFields = [ name: 'isPrivate', type: 'boolean', default: false, + description: 'Create a private channel instead of a public one', }, { displayName: 'Users', @@ -84,6 +203,7 @@ export const channelFields = [ loadOptionsMethod: 'getUsers', }, default: [], + description: `Required for workspace apps. A list of between 1 and 30 human users that will be added to the newly-created conversation`, }, ] }, @@ -91,11 +211,13 @@ export const channelFields = [ /* channel:invite */ /* -------------------------------------------------------------------------- */ { - displayName: 'Channel ID', - name: 'channel', - type: 'string', + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, default: '', - placeholder: 'myChannel', displayOptions: { show: { operation: [ @@ -111,10 +233,9 @@ export const channelFields = [ }, { displayName: 'User ID', - name: 'username', + name: 'userId', type: 'string', default: '', - placeholder: 'frank', displayOptions: { show: { operation: [ @@ -131,11 +252,114 @@ export const channelFields = [ /* -------------------------------------------------------------------------- */ /* channel:get */ /* -------------------------------------------------------------------------- */ - + { + displayName: 'Channel', + name: 'channelId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'get' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'Channel ID to learn more about', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + 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:delete */ +/* channel:kick */ /* -------------------------------------------------------------------------- */ - + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'kick' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The name of the channel to create.', + }, + { + displayName: 'User ID', + name: 'userId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'kick' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* channel:join */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'join' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + }, /* -------------------------------------------------------------------------- */ /* channel:getAll */ /* -------------------------------------------------------------------------- */ @@ -181,8 +405,8 @@ export const channelFields = [ description: 'How many results to return.', }, { - displayName: 'Options', - name: 'options', + displayName: 'Filters', + name: 'filters', type: 'collection', placeholder: 'Add Field', default: {}, @@ -198,12 +422,487 @@ export const channelFields = [ }, options: [ { - displayName: 'Fields', - name: 'fields', - type: 'string', - default: '', - description: 'Fields to include separated by ,', + displayName: 'Exclude Archived', + name: 'excludeArchived', + type: 'boolean', + default: false, + description: 'Set to true 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' + }, + { + name: 'mpim', + value: 'mpim' + }, + { + name: 'im', + value: 'im' + }, + ], + default: ['public_channel'], + description: 'Mix and match channel types', }, ] }, +/* -------------------------------------------------------------------------- */ +/* channel:history */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'history' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The name of the channel to create.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'history', + ], + }, + }, + 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: [ + 'history', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many 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: '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: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'leave' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'The name of the channel to leave.', + }, +/* -------------------------------------------------------------------------- */ +/* 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: 'Boolean, indicates you want the full IM channel definition in the response.', + }, + { + displayName: 'Users', + 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.`, + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* channel:rename */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'rename' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'The name of the 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: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'replies' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The name of the channel to create.', + }, + { + displayName: 'TS', + name: 'ts', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'replies' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: `Unique identifier of a thread's parent message.`, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'replies', + ], + }, + }, + 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: [ + 'replies', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many 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: '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: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'setPurpose' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'Conversation 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: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'setTopic' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'Conversation to set the topic of', + }, + { + displayName: 'Topic', + name: 'topic', + type: 'string', + displayOptions: { + show: { + operation: [ + 'setTopic' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'The new topic string. Does not support formatting or linkification.', + }, +/* -------------------------------------------------------------------------- */ +/* channel:unarchive */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'unarchive' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'The ID of the channel to unarchive.', + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/ConversationDescription.ts b/packages/nodes-base/nodes/Slack/ConversationDescription.ts deleted file mode 100644 index 62628cc654..0000000000 --- a/packages/nodes-base/nodes/Slack/ConversationDescription.ts +++ /dev/null @@ -1,846 +0,0 @@ -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/FileDescription.ts b/packages/nodes-base/nodes/Slack/FileDescription.ts new file mode 100644 index 0000000000..fe1bbe8f8e --- /dev/null +++ b/packages/nodes-base/nodes/Slack/FileDescription.ts @@ -0,0 +1,322 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const fileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'file', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a file info', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get & filters team files.', + }, + { + name: 'Upload', + value: 'upload', + description: 'Create or upload an existing file.', + }, + ], + default: 'upload', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const fileFields = [ + +/* -------------------------------------------------------------------------- */ +/* file:upload */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'upload' + ], + resource: [ + 'file', + ], + }, + }, + description: 'If 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: '', + description: 'The text content of the file to upload.', + }, + { + 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: 'Channels', + name: 'channelIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: [], + description: 'The channels to send the file to.', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + description: 'Filename of file.', + }, + { + displayName: 'Initial Comment', + name: 'initialComment', + type: 'string', + default: '', + description: 'The message text introducing the file in specified channels.', + }, + { + displayName: 'Thread TS', + name: 'threadTs', + type: 'string', + default: '', + description: `Provide another message's ts value to upload this file as a reply. Never use a reply's ts value; use its parent instead.`, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of file.', + }, + ], + }, +/* ----------------------------------------------------------------------- */ +/* file:getAll */ +/* ----------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'file', + ], + 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: [ + 'file', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'getAll' + ], + resource: [ + 'file', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + description: 'Channel containing the file to be listed.', + }, + { + displayName: 'Show Files Hidden By Limit', + name: 'showFilesHidden', + type: 'boolean', + default: false, + description: '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: 'TS From', + name: 'tsFrom', + type: 'string', + default: '', + description: 'Filter files created after this timestamp (inclusive).', + }, + { + displayName: 'TS 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: 'Spaces', + value: 'spaces', + }, + { + name: 'Snippets', + value: 'snippets', + }, + { + name: 'Images', + value: 'images', + }, + { + name: 'Google Docs', + value: 'gdocs', + }, + { + name: 'Zips', + value: 'zips', + }, + { + name: 'pdfs', + value: 'pdfs', + }, + ], + default: ['all'], + description: 'Filter files by type', + }, + { + displayName: 'User', + name: 'userId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'Filter files created by a single user.', + }, + ], + }, +/* ----------------------------------------------------------------------- */ +/* file:get */ +/* ----------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + displayOptions: { + show: { + resource: [ + 'file', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts index 57b5b00c11..b73de1cac5 100644 --- a/packages/nodes-base/nodes/Slack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -12,11 +12,11 @@ import { } 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 +export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: object = {}, query: object = {}, headers: {} | undefined = undefined, option: {} = {}): Promise { // tslint:disable-line:no-any const authenticationMethod = this.getNodeParameter('authentication', 0, 'accessToken') as string; - const options: OptionsWithUri = { + let options: OptionsWithUri = { method, - headers: { + headers: headers || { 'Content-Type': 'application/json; charset=utf-8' }, body, @@ -24,6 +24,7 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu uri: `https://slack.com/api${resource}`, json: true }; + options = Object.assign({}, options, option); if (Object.keys(body).length === 0) { delete options.body; } @@ -62,15 +63,23 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu 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; + query.page = 1; + query.count = 100; do { responseData = await slackApiRequest.call(this, method, endpoint, body, query); query.cursor = encodeURIComponent(_.get(responseData, 'response_metadata.next_cursor')); + query.page++; returnData.push.apply(returnData, responseData[propertyName]); } while ( - responseData.response_metadata !== undefined && + (responseData.response_metadata !== undefined && responseData.response_metadata.mext_cursor !== undefined && responseData.response_metadata.next_cursor !== "" && - responseData.response_metadata.next_cursor !== null + responseData.response_metadata.next_cursor !== null) || + (responseData.paging !== undefined && + responseData.paging.pages !== undefined && + responseData.paging.page !== undefined && + responseData.paging.page < responseData.paging.pages + ) ); return returnData; diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index 7f9dff3ac3..8c582503e5 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -18,6 +18,11 @@ export const messageOperations = [ value: 'post', description: 'Post a message into a channel', }, + { + name: 'Update', + value: 'update', + description: 'Updates a message.', + }, ], default: 'post', description: 'The operation to perform.', @@ -406,8 +411,8 @@ export const messageFields = [ /* message:update */ /* ----------------------------------------------------------------------- */ { - displayName: 'message ID', - name: 'messageId', + displayName: 'Channel ID', + name: 'channelId', type: 'string', required: true, default: '', @@ -421,7 +426,60 @@ export const messageFields = [ ] }, }, - description: 'Id of message that needs to be fetched', + description: 'Channel containing the message to be updated.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ] + }, + }, + description: `New text for the message, using the default formatting rules. It's not required when presenting attachments.`, + }, + { + displayName: 'TS', + name: 'ts', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ] + }, + }, + description: `Timestamp of the message to be updated.`, + }, + { + displayName: 'As User', + name: 'as_user', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'update' + ], + resource: [ + 'message', + ], + }, + }, + description: 'Pass true to update the message as the authed user. Bot users in this context are considered authed users.', }, { displayName: 'Update Fields', @@ -441,403 +499,228 @@ export const messageFields = [ }, 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', + displayName: 'Link Names', + name: 'link_names', type: 'boolean', default: false, - description: 'Indicates whether a popup reminder has been set for the message (true) or not (false).', + description: 'Find and link channel names and usernames.', }, { - 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', + displayName: 'Parse', + name: 'parse', type: 'options', options: [ { - name: 'January', - value: 'January' + name: 'Client', + value: 'client', }, { - name: 'February', - value: 'February' + name: 'Full', + value: 'full', }, { - name: 'March', - value: 'March' + name: 'None', + value: 'none', }, - { - 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.', + default: 'client', + description: 'Change how messages are treated', }, - { - 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', + displayName: 'Attachments', + name: 'attachments', type: 'collection', - placeholder: 'Add Field', - default: {}, + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add attachment', + }, displayOptions: { show: { + operation: [ + 'update' + ], resource: [ 'message', ], - operation: [ - 'getAll', - ], }, }, + 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', - type: 'string', - default: '', - description: 'Fields to include separated by ,', - }, - ] + 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.', + }, + ] + }, + ], + } + ], }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index c3b074c1e1..1afeb4ebd0 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -1,5 +1,6 @@ import { IExecuteFunctions, + BINARY_ENCODING, } from 'n8n-core'; import { IDataObject, @@ -18,9 +19,13 @@ import { messageFields, } from './MessageDescription'; import { - conversationOperations, - conversationFields, -} from './ConversationDescription'; + starOperations, + starFields, +} from './StarDescription'; +import { + fileOperations, + fileFields, +} from './FileDescription'; import { slackApiRequest, salckApiRequestAllItems, @@ -37,7 +42,7 @@ export class Slack implements INodeType { group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Sends data to Slack', + description: 'Consume Slack API', defaults: { name: 'Slack', color: '#BB2244', @@ -66,7 +71,7 @@ export class Slack implements INodeType { ], }, }, - } + }, ], properties: [ { @@ -95,10 +100,18 @@ export class Slack implements INodeType { name: 'Channel', value: 'channel', }, + { + name: 'File', + value: 'file', + }, { name: 'Message', value: 'message', }, + { + name: 'Star', + value: 'star', + }, ], default: 'message', description: 'The resource to operate on.', @@ -107,6 +120,10 @@ export class Slack implements INodeType { ...channelFields, ...messageOperations, ...messageFields, + ...starOperations, + ...starFields, + ...fileOperations, + ...fileFields, ], }; @@ -125,7 +142,21 @@ export class Slack implements INodeType { value: userId, }); } - console.log(users) + return returnData; + }, + // Get all the users to display them to user so that he can + // select them easily + async getChannels(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const channels = await salckApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list'); + for (const channel of channels) { + const channelName = channel.name; + const channelId = channel.id; + returnData.push({ + name: channelName, + value: channelId, + }); + } return returnData; }, } @@ -142,25 +173,208 @@ export class Slack implements INodeType { 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.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('channel', i) as string; + const channel = this.getNodeParameter('channelId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const body: IDataObject = { name: channel, }; - responseData = await slackApiRequest.call(this, 'POST', '/channels.create', body, qs); + if (additionalFields.isPrivate) { + body.is_private = additionalFields.isPrivate as boolean; + } + if (additionalFields.users) { + body.user_ids = (additionalFields.users as string[]).join(','); + } + responseData = await slackApiRequest.call(this, 'POST', '/conversations.create', body, qs); } - if (operation === 'invite') { - const channel = this.getNodeParameter('channel', i) as string; - const user = this.getNodeParameter('username', i) as string; + //https://api.slack.com/methods/conversations.kick + if (operation === 'kick') { + const channel = this.getNodeParameter('channelId', i) as string; + const userId = this.getNodeParameter('userId', i) as string; + const body: IDataObject = { + name: channel, + user: userId, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.kick', body, qs); + } + //https://api.slack.com/methods/conversations.join + if (operation === 'join') { + const channel = this.getNodeParameter('channelId', i) as string; const body: IDataObject = { channel, - user, }; - responseData = await slackApiRequest.call(this, 'POST', '/channels.invite', body, qs); + responseData = await slackApiRequest.call(this, 'POST', '/conversations.join', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.info + if (operation === 'get') { + const channel = this.getNodeParameter('channelId', i) as string; + qs.channel = channel, + responseData = await slackApiRequest.call(this, 'POST', '/conversations.info', {}, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.types) { + qs.types = (filters.types as string[]).join(','); + } + if (filters.excludeArchived) { + qs.exclude_archived = filters.excludeArchived as boolean; + } + if (returnAll === true) { + responseData = await salckApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/conversations.list', {}, qs); + responseData = responseData.channels; + } + } + //https://api.slack.com/methods/conversations.history + if (operation === 'history') { + const channel = this.getNodeParameter('channelId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.channel = channel; + if (filters.inclusive) { + qs.inclusive = filters.inclusive as boolean; + } + if (filters.latest) { + qs.latest = filters.latest as string; + } + if (filters.oldest) { + qs.oldest = filters.oldest as string; + } + if (returnAll === true) { + responseData = await salckApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.history', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/conversations.history', {}, qs); + responseData = responseData.messages; + } + } + //https://api.slack.com/methods/conversations.invite + if (operation === 'invite') { + const channel = this.getNodeParameter('channelId', i) as string; + const userId = this.getNodeParameter('userId', i) as string; + const body: IDataObject = { + channel, + user: userId, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.invite', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.leave + if (operation === 'leave') { + const channel = this.getNodeParameter('channelId', i) as string; + const body: IDataObject = { + channel, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.leave', body, qs); + } + //https://api.slack.com/methods/conversations.open + if (operation === 'open') { + const options = this.getNodeParameter('options', i) as IDataObject; + const body: IDataObject = {}; + if (options.channelId) { + body.channel = options.channelId as string; + } + if (options.returnIm) { + body.return_im = options.returnIm as boolean; + } + if (options.users) { + body.users = (options.users as string[]).join(','); + } + responseData = await slackApiRequest.call(this, 'POST', '/conversations.open', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.rename + if (operation === 'rename') { + const channel = this.getNodeParameter('channelId', i) as IDataObject; + const name = this.getNodeParameter('name', i) as IDataObject; + const body: IDataObject = { + channel, + name, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.rename', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.replies + if (operation === 'replies') { + const channel = this.getNodeParameter('channelId', i) as string; + const ts = this.getNodeParameter('ts', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.channel = channel; + qs.ts = ts; + if (filters.inclusive) { + qs.inclusive = filters.inclusive as boolean; + } + if (filters.latest) { + qs.latest = filters.latest as string; + } + if (filters.oldest) { + qs.oldest = filters.oldest as string; + } + if (returnAll === true) { + responseData = await salckApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.replies', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/conversations.replies', {}, qs); + responseData = responseData.messages; + } + } + //https://api.slack.com/methods/conversations.setPurpose + if (operation === 'setPurpose') { + const channel = this.getNodeParameter('channelId', i) as IDataObject; + const purpose = this.getNodeParameter('purpose', i) as IDataObject; + const body: IDataObject = { + channel, + purpose, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.setPurpose', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.setTopic + if (operation === 'setTopic') { + const channel = this.getNodeParameter('channelId', i) as IDataObject; + const topic = this.getNodeParameter('topic', i) as IDataObject; + const body: IDataObject = { + channel, + topic, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.setTopic', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.unarchive + if (operation === 'unarchive') { + const channel = this.getNodeParameter('channelId', i) as string; + const body: IDataObject = { + channel, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.unarchive', body, qs); } } if (resource === 'message') { + //https://api.slack.com/methods/chat.postMessage if (operation === 'post') { const channel = this.getNodeParameter('channel', i) as string; const text = this.getNodeParameter('text', i) as string; @@ -195,6 +409,174 @@ export class Slack implements INodeType { Object.assign(body, otherOptions); responseData = await slackApiRequest.call(this, 'POST', '/chat.postMessage', body, qs); } + //https://api.slack.com/methods/chat.update + if (operation === 'update') { + const channel = this.getNodeParameter('channel', i) as string; + const text = this.getNodeParameter('text', i) as string; + const ts = this.getNodeParameter('ts', i) as string; + const as_user = this.getNodeParameter('as_user', i) as boolean; + const attachments = this.getNodeParameter('attachments', i, []) as unknown as IAttachment[]; + const body: IDataObject = { + channel, + text, + ts, + as_user, + }; + // The node does save the fields data differently than the API + // expects so fix the data befre we send the request + for (const attachment of attachments) { + if (attachment.fields !== undefined) { + if (attachment.fields.item !== undefined) { + // Move the field-content up + // @ts-ignore + attachment.fields = attachment.fields.item; + } else { + // If it does not have any items set remove it + delete attachment.fields; + } + } + } + body['attachments'] = attachments; + + // Add all the other options to the request + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + Object.assign(body, updateFields); + responseData = await slackApiRequest.call(this, 'POST', '/chat.update', body, qs); + } + } + if (resource === 'star') { + //https://api.slack.com/methods/stars.add + if (operation === 'add') { + const options = this.getNodeParameter('options', i) as IDataObject; + const body: IDataObject = {}; + if (options.channelId) { + body.channel = options.channelId as string; + } + if (options.fileId) { + body.file = options.fileId as string; + } + if (options.fileComment) { + body.file_comment = options.fileComment as string; + } + if (options.timestamp) { + body.timestamp = options.timestamp as string; + } + responseData = await slackApiRequest.call(this, 'POST', '/stars.add', body, qs); + } + //https://api.slack.com/methods/stars.remove + if (operation === 'delete') { + const options = this.getNodeParameter('options', i) as IDataObject; + const body: IDataObject = {}; + if (options.channelId) { + body.channel = options.channelId as string; + } + if (options.fileId) { + body.file = options.fileId as string; + } + if (options.fileComment) { + body.file_comment = options.fileComment as string; + } + if (options.timestamp) { + body.timestamp = options.timestamp as string; + } + responseData = await slackApiRequest.call(this, 'POST', '/stars.remove', body, qs); + } + //https://api.slack.com/methods/stars.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + if (returnAll === true) { + responseData = await salckApiRequestAllItems.call(this, 'items', 'GET', '/stars.list', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/stars.list', {}, qs); + responseData = responseData.items; + } + } + } + if (resource === 'file') { + //https://api.slack.com/methods/files.upload + if (operation === 'upload') { + const options = this.getNodeParameter('options', i) as IDataObject; + const binaryData = this.getNodeParameter('binaryData', i) as boolean; + const body: IDataObject = {}; + if (options.channelIds) { + body.channels = (options.channelIds as string[]).join(','); + } + if (options.fileName) { + body.filename = options.fileName as string; + } + if (options.initialComment) { + body.initial_comment = options.initialComment as string; + } + if (options.threadTs) { + body.thread_ts = options.threadTs as string; + } + if (options.title) { + body.title = options.title as string; + } + if (binaryData) { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + if (items[i].binary === undefined + //@ts-ignore + || items[i].binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + body.file = { + //@ts-ignore + value: Buffer.from(items[i].binary[binaryPropertyName].data, BINARY_ENCODING), + options: { + //@ts-ignore + filename: items[i].binary[binaryPropertyName].fileName, + //@ts-ignore + contentType: items[i].binary[binaryPropertyName].mimeType, + } + } + responseData = await slackApiRequest.call(this, 'POST', '/files.upload', {}, qs, { 'Content-Type': 'multipart/form-data' }, { formData: body }); + responseData = responseData.file; + } else { + const fileContent = this.getNodeParameter('fileContent', i) as string; + body.content = fileContent; + responseData = await slackApiRequest.call(this, 'POST', '/files.upload', body, qs, { 'Content-Type': 'application/x-www-form-urlencoded' }, { form: body }); + responseData = responseData.file; + } + } + //https://api.slack.com/methods/files.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.channelId) { + qs.channel = filters.channelId as string; + } + if (filters.showFilesHidden) { + qs.show_files_hidden_by_limit = filters.showFilesHidden as boolean; + } + if (filters.tsFrom) { + qs.ts_from = filters.tsFrom as string; + } + if (filters.tsTo) { + qs.ts_to = filters.tsTo as string; + } + if (filters.types) { + qs.types = (filters.types as string[]).join(',') as string; + } + if (filters.userId) { + qs.user = filters.userId as string; + } + if (returnAll === true) { + responseData = await salckApiRequestAllItems.call(this, 'files', 'GET', '/files.list', {}, qs); + } else { + qs.count = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/files.list', {}, qs); + responseData = responseData.files; + } + } + //https://api.slack.com/methods/files.info + if (operation === 'get') { + const fileId = this.getNodeParameter('fileId', i) as string; + qs.file = fileId; + responseData = await slackApiRequest.call(this, 'GET', '/files.info', {}, qs); + responseData = responseData.file; + } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); diff --git a/packages/nodes-base/nodes/Slack/StarDescription.ts b/packages/nodes-base/nodes/Slack/StarDescription.ts new file mode 100644 index 0000000000..39174b6a87 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/StarDescription.ts @@ -0,0 +1,185 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const starOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'star', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a star to an item.', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a star from an item.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all stars for a user.', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const starFields = [ + +/* -------------------------------------------------------------------------- */ +/* star:add */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'add' + ], + resource: [ + 'star', + ], + }, + }, + default: {}, + description: 'Options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + description: 'Channel to add star to, or channel where the message to add star to was posted (used with timestamp).', + }, + { + 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: 'Timestamp', + name: 'timestamp', + type: 'string', + default: '', + description: 'Timestamp of the message 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 ID', + name: 'channelId', + type: 'string', + default: '', + description: 'Channel to add star to, or channel where the message to add star to was posted (used with timestamp).', + }, + { + 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: 'Timestamp', + name: 'timestamp', + type: 'string', + default: '', + description: 'Timestamp of the message to add star to.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* star:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'star', + ], + 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: [ + 'star', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, +] as INodeProperties[]; From 150aa7daee102a6078fa0ea49d7c50b2ac1b1761 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 15 Mar 2020 13:00:57 +0100 Subject: [PATCH 3/4] :bug: Fix issue that did not use actual node parameters loading options --- packages/cli/src/Server.ts | 2 +- packages/core/src/LoadNodeParameterOptions.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index bafe3120e2..91c318086f 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -550,7 +550,7 @@ class App { const nodeTypes = NodeTypes(); - const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, credentials); + const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, JSON.parse('' + req.query.currentNodeParameters), credentials); const workflowData = loadDataInstance.getWorkflowData() as IWorkflowBase; const workflowCredentials = await WorkflowCredentials(workflowData.nodes); diff --git a/packages/core/src/LoadNodeParameterOptions.ts b/packages/core/src/LoadNodeParameterOptions.ts index 8ff78e41cd..fc9bf46b03 100644 --- a/packages/core/src/LoadNodeParameterOptions.ts +++ b/packages/core/src/LoadNodeParameterOptions.ts @@ -1,6 +1,7 @@ import { INode, INodeCredentials, + INodeParameters, INodePropertyOptions, INodeTypes, IWorkflowExecuteAdditionalData, @@ -20,7 +21,7 @@ export class LoadNodeParameterOptions { workflow: Workflow; - constructor(nodeTypeName: string, nodeTypes: INodeTypes, credentials?: INodeCredentials) { + constructor(nodeTypeName: string, nodeTypes: INodeTypes, currentNodeParameters: INodeParameters, credentials?: INodeCredentials) { const nodeType = nodeTypes.getByName(nodeTypeName); if (nodeType === undefined) { @@ -28,8 +29,7 @@ export class LoadNodeParameterOptions { } const nodeData: INode = { - parameters: { - }, + parameters: currentNodeParameters, name: TEMP_NODE_NAME, type: nodeTypeName, typeVersion: 1, From ff7f0a5de5370be475c718985d6da1c1b9c9e24a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 15 Mar 2020 15:51:49 +0100 Subject: [PATCH 4/4] :zap: Fix some issues with Slack-Node --- packages/cli/BREAKING-CHANGES.md | 28 +++++++ packages/cli/src/Server.ts | 2 +- packages/core/src/NodeExecuteFunctions.ts | 18 ++--- .../credentials/SlackOAuth2Api.credentials.ts | 2 +- .../nodes/Slack/ChannelDescription.ts | 12 ++- .../nodes/Slack/GenericFunctions.ts | 4 +- .../nodes/Slack/MessageDescription.ts | 23 ++++-- packages/nodes-base/nodes/Slack/Slack.node.ts | 79 ++++++++++++------- .../nodes-base/nodes/Slack/StarDescription.ts | 12 ++- 9 files changed, 126 insertions(+), 54 deletions(-) diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 3effaa08ee..33e5118859 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,34 @@ This list shows all the versions which include breaking changes and how to upgrade +## ??? + +### What changed? + +To make it easier to use the data which the Slack-Node outputs we no longer return the whole +object the Slack-API returns if the only other property is `"ok": true`. In this case it returns +now directly the data under "channel". + +### When is action necessary? + +When you currently use the Slack-Node with Operations Channel -> Create and you use +any of the data the node outputs. + +### How to upgrade: + +All values that get referenced which were before under the property "channel" are now on the main level. +This means that these expressions have to get adjusted. + +Meaning if the expression used before was: +``` +{{ $node["Slack"].data["channel"]["id"] }} +``` +it has to get changed to: +``` +{{ $node["Slack"].data["id"] }} +``` + + ## 0.37.0 ### What changed? diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 91c318086f..9f4e9e77bc 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) => { + jwt.verify(token, getKey, {}, (err: Error, decoded: object) => { if (err) return ResponseHelper.jwtAuthAuthorizationError(res, "Invalid token"); next(); diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index f73eb2f068..80d5d0d135 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, tokenType?: string | undefined, property?: string) { +export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, tokenType?: string, property?: string) { const credentials = this.getCredentials(credentialsType) as ICredentialDataDecryptedObject; if (credentials === undefined) { @@ -134,7 +134,7 @@ export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string const oauthTokenData = credentials.oauthTokenData as clientOAuth2.Data; - const token = oAuthClient.createToken(get(oauthTokenData, property as string) || oauthTokenData.accessToken, oauthTokenData.refresToken, tokenType || oauthTokenData.tokenType, oauthTokenData); + const token = oAuthClient.createToken(get(oauthTokenData, property as string) || oauthTokenData.accessToken, oauthTokenData.refreshToken, 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,7 +412,7 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, @@ -466,7 +466,7 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, @@ -547,7 +547,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, @@ -629,7 +629,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, }, @@ -679,7 +679,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, }, @@ -737,7 +737,7 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, }, @@ -822,7 +822,7 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, 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 index 6c5284ee44..b56699fe68 100644 --- a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts @@ -12,7 +12,7 @@ const userScopes = [ 'files:write', 'stars:read', 'stars:write', -] +]; export class SlackOAuth2Api implements ICredentialType { diff --git a/packages/nodes-base/nodes/Slack/ChannelDescription.ts b/packages/nodes-base/nodes/Slack/ChannelDescription.ts index cda5824105..8e279bbeab 100644 --- a/packages/nodes-base/nodes/Slack/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Slack/ChannelDescription.ts @@ -234,7 +234,10 @@ export const channelFields = [ { displayName: 'User ID', name: 'userId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, default: '', displayOptions: { show: { @@ -321,9 +324,12 @@ export const channelFields = [ description: 'The name of the channel to create.', }, { - displayName: 'User ID', + displayName: 'User', name: 'userId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, displayOptions: { show: { operation: [ diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts index b73de1cac5..ff4769cb8e 100644 --- a/packages/nodes-base/nodes/Slack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -60,7 +60,7 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu } } -export async function salckApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function slackApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; let responseData; query.page = 1; @@ -73,7 +73,7 @@ export async function salckApiRequestAllItems(this: IExecuteFunctions | ILoadOpt } while ( (responseData.response_metadata !== undefined && responseData.response_metadata.mext_cursor !== undefined && - responseData.response_metadata.next_cursor !== "" && + responseData.response_metadata.next_cursor !== '' && responseData.response_metadata.next_cursor !== null) || (responseData.paging !== undefined && responseData.paging.pages !== undefined && diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index 8c582503e5..8b077e6bab 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -43,7 +43,7 @@ export const messageFields = [ displayOptions: { show: { operation: [ - 'post' + 'post', ], resource: [ 'message', @@ -64,7 +64,7 @@ export const messageFields = [ displayOptions: { show: { operation: [ - 'post' + 'post', ], resource: [ 'message', @@ -80,6 +80,9 @@ export const messageFields = [ default: false, displayOptions: { show: { + authentication: [ + 'accessToken', + ], operation: [ 'post' ], @@ -98,10 +101,10 @@ export const messageFields = [ displayOptions: { show: { as_user: [ - false + false, ], operation: [ - 'post' + 'post', ], resource: [ 'message', @@ -121,7 +124,7 @@ export const messageFields = [ displayOptions: { show: { operation: [ - 'post' + 'post', ], resource: [ 'message', @@ -411,9 +414,12 @@ export const messageFields = [ /* message:update */ /* ----------------------------------------------------------------------- */ { - displayName: 'Channel ID', + displayName: 'Channel', name: 'channelId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, required: true, default: '', displayOptions: { @@ -471,6 +477,9 @@ export const messageFields = [ default: false, displayOptions: { show: { + authentication: [ + 'accessToken', + ], operation: [ 'update' ], diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index 1afeb4ebd0..2b2061d4e3 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -11,24 +11,24 @@ import { INodePropertyOptions, } from 'n8n-workflow'; import { - channelOperations, channelFields, + channelOperations, } from './ChannelDescription'; import { - messageOperations, messageFields, + messageOperations, } from './MessageDescription'; import { - starOperations, starFields, + starOperations, } from './StarDescription'; import { - fileOperations, fileFields, + fileOperations, } from './FileDescription'; import { slackApiRequest, - salckApiRequestAllItems, + slackApiRequestAllItems, } from './GenericFunctions'; import { IAttachment, @@ -133,7 +133,7 @@ export class Slack implements INodeType { // select them easily async getUsers(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const users = await salckApiRequestAllItems.call(this, 'members', 'GET', '/users.list'); + const users = await slackApiRequestAllItems.call(this, 'members', 'GET', '/users.list'); for (const user of users) { const userName = user.name; const userId = user.id; @@ -142,13 +142,20 @@ export class Slack implements INodeType { value: userId, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, // Get all the users to display them to user so that he can // select them easily async getChannels(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const channels = await salckApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list'); + const channels = await slackApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list'); for (const channel of channels) { const channelName = channel.name; const channelId = channel.id; @@ -157,6 +164,13 @@ export class Slack implements INodeType { value: channelId, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, } @@ -168,10 +182,12 @@ export class Slack implements INodeType { const length = items.length as unknown as number; let qs: IDataObject; let responseData; + const authentication = this.getNodeParameter('authentication', 0) as string; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { qs = {}; - 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.archive if (operation === 'archive') { @@ -203,6 +219,7 @@ export class Slack implements INodeType { body.user_ids = (additionalFields.users as string[]).join(','); } responseData = await slackApiRequest.call(this, 'POST', '/conversations.create', body, qs); + responseData = responseData.channel; } //https://api.slack.com/methods/conversations.kick if (operation === 'kick') { @@ -241,7 +258,7 @@ export class Slack implements INodeType { qs.exclude_archived = filters.excludeArchived as boolean; } if (returnAll === true) { - responseData = await salckApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list', {}, qs); + responseData = await slackApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/conversations.list', {}, qs); @@ -264,7 +281,7 @@ export class Slack implements INodeType { qs.oldest = filters.oldest as string; } if (returnAll === true) { - responseData = await salckApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.history', {}, qs); + responseData = await slackApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.history', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/conversations.history', {}, qs); @@ -335,7 +352,7 @@ export class Slack implements INodeType { qs.oldest = filters.oldest as string; } if (returnAll === true) { - responseData = await salckApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.replies', {}, qs); + responseData = await slackApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.replies', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/conversations.replies', {}, qs); @@ -379,15 +396,18 @@ export class Slack implements INodeType { 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, + channel, text, - as_user, }; - if (as_user === false) { + + if (authentication === 'accessToken') { + body.as_user = this.getNodeParameter('as_user', i) as boolean; + } + if (body.as_user === false) { body.username = this.getNodeParameter('username', i) as string; } + // 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) { @@ -411,17 +431,20 @@ export class Slack implements INodeType { } //https://api.slack.com/methods/chat.update if (operation === 'update') { - const channel = this.getNodeParameter('channel', i) as string; + 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 as_user = this.getNodeParameter('as_user', i) as boolean; const attachments = this.getNodeParameter('attachments', i, []) as unknown as IAttachment[]; const body: IDataObject = { channel, text, ts, - as_user, }; + + if (authentication === 'accessToken') { + body.as_user = this.getNodeParameter('as_user', i) as boolean; + } + // The node does save the fields data differently than the API // expects so fix the data befre we send the request for (const attachment of attachments) { @@ -485,7 +508,7 @@ export class Slack implements INodeType { if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; if (returnAll === true) { - responseData = await salckApiRequestAllItems.call(this, 'items', 'GET', '/stars.list', {}, qs); + responseData = await slackApiRequestAllItems.call(this, 'items', 'GET', '/stars.list', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/stars.list', {}, qs); @@ -522,15 +545,15 @@ export class Slack implements INodeType { throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); } body.file = { + //@ts-ignore + value: Buffer.from(items[i].binary[binaryPropertyName].data, BINARY_ENCODING), + options: { //@ts-ignore - value: Buffer.from(items[i].binary[binaryPropertyName].data, BINARY_ENCODING), - options: { - //@ts-ignore - filename: items[i].binary[binaryPropertyName].fileName, - //@ts-ignore - contentType: items[i].binary[binaryPropertyName].mimeType, - } + 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 { @@ -563,7 +586,7 @@ export class Slack implements INodeType { qs.user = filters.userId as string; } if (returnAll === true) { - responseData = await salckApiRequestAllItems.call(this, 'files', 'GET', '/files.list', {}, qs); + responseData = await slackApiRequestAllItems.call(this, 'files', 'GET', '/files.list', {}, qs); } else { qs.count = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/files.list', {}, qs); diff --git a/packages/nodes-base/nodes/Slack/StarDescription.ts b/packages/nodes-base/nodes/Slack/StarDescription.ts index 39174b6a87..282025b1ea 100644 --- a/packages/nodes-base/nodes/Slack/StarDescription.ts +++ b/packages/nodes-base/nodes/Slack/StarDescription.ts @@ -26,7 +26,7 @@ export const starOperations = [ { name: 'Get All', value: 'getAll', - description: 'Get all stars for a user.', + description: 'Get all stars of autenticated user.', }, ], default: 'add', @@ -60,7 +60,10 @@ export const starFields = [ { displayName: 'Channel ID', name: 'channelId', - type: 'string', + 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).', }, @@ -111,7 +114,10 @@ export const starFields = [ { displayName: 'Channel ID', name: 'channelId', - type: 'string', + 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).', },