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 5a1c0703bf..9f4e9e77bc 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -203,9 +203,7 @@ class App { }); } - jwt.verify(token, getKey, {}, (err: Error, - //decoded: string - ) => { + jwt.verify(token, getKey, {}, (err: Error, decoded: object) => { if (err) return ResponseHelper.jwtAuthAuthorizationError(res, "Invalid token"); next(); @@ -552,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, diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 69dd2c92e1..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) { +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) { @@ -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.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,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, 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, 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, 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, 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, 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, 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, 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..b56699fe68 --- /dev/null +++ b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts @@ -0,0 +1,57 @@ +import { + ICredentialType, + NodePropertyTypes, +} 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'; + 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=${userScopes.join(' ')}`, + }, + { + 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..8e279bbeab --- /dev/null +++ b/packages/nodes-base/nodes/Slack/ChannelDescription.ts @@ -0,0 +1,914 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const channelOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'channel', + ], + }, + }, + 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.', + }, +] as INodeProperties[]; + +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: 'Channel', + name: 'channelId', + 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, + description: 'Create a private channel instead of a public one', + }, + { + displayName: 'Users', + name: 'users', + type: 'multiOptions', + typeOptions: { + 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`, + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* channel:invite */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'invite' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The ID of the channel to invite user to.', + }, + { + displayName: 'User ID', + name: 'userId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'invite' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The ID of the user to invite into channel.', + }, +/* -------------------------------------------------------------------------- */ +/* 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: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', + name: 'userId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + 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 */ +/* -------------------------------------------------------------------------- */ + { + 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: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Exclude Archived', + name: 'excludeArchived', + type: 'boolean', + default: false, + description: '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/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 new file mode 100644 index 0000000000..ff4769cb8e --- /dev/null +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -0,0 +1,86 @@ +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 = {}, headers: {} | undefined = undefined, option: {} = {}): Promise { // tslint:disable-line:no-any + const authenticationMethod = this.getNodeParameter('authentication', 0, 'accessToken') as string; + let options: OptionsWithUri = { + method, + headers: headers || { + 'Content-Type': 'application/json; charset=utf-8' + }, + body, + qs: query, + uri: `https://slack.com/api${resource}`, + json: true + }; + options = Object.assign({}, options, option); + if (Object.keys(body).length === 0) { + delete options.body; + } + if (Object.keys(query).length === 0) { + delete options.qs; + } + 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 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; + 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.mext_cursor !== undefined && + responseData.response_metadata.next_cursor !== '' && + 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 new file mode 100644 index 0000000000..8b077e6bab --- /dev/null +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -0,0 +1,735 @@ +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', + }, + { + name: 'Update', + value: 'update', + description: 'Updates a message.', + }, + ], + 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: { + authentication: [ + 'accessToken', + ], + 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: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ] + }, + }, + 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: { + authentication: [ + 'accessToken', + ], + 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', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Link Names', + name: 'link_names', + type: 'boolean', + default: false, + description: 'Find and link channel names and usernames.', + }, + { + displayName: 'Parse', + name: 'parse', + type: 'options', + options: [ + { + name: 'Client', + value: 'client', + }, + { + name: 'Full', + value: 'full', + }, + { + name: 'None', + value: 'none', + }, + ], + default: 'client', + description: 'Change how messages are treated', + }, + ], + }, + { + displayName: 'Attachments', + name: 'attachments', + type: 'collection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add attachment', + }, + displayOptions: { + show: { + operation: [ + 'update' + ], + 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.', + }, + ] + }, + ], + } + ], + }, +] 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..2b2061d4e3 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -1,16 +1,38 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { + IExecuteFunctions, + BINARY_ENCODING, + } from 'n8n-core'; import { IDataObject, INodeTypeDescription, INodeExecutionData, INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, } from 'n8n-workflow'; - -interface Attachment { - fields: { - item?: object[]; - }; -} +import { + channelFields, + channelOperations, +} from './ChannelDescription'; +import { + messageFields, + messageOperations, +} from './MessageDescription'; +import { + starFields, + starOperations, +} from './StarDescription'; +import { + fileFields, + fileOperations, +} from './FileDescription'; +import { + slackApiRequest, + slackApiRequestAllItems, +} from './GenericFunctions'; +import { + IAttachment, +} from './MessageInterface'; export class Slack implements INodeType { description: INodeTypeDescription = { @@ -20,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', @@ -31,9 +53,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', @@ -43,594 +100,314 @@ 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.', }, - - - - // ---------------------------------- - // 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, + ...starOperations, + ...starFields, + ...fileOperations, + ...fileFields, ], }; + methods = { + loadOptions: { + // Get all the users to display them to user so that he can + // select them easily + async getUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const users = await slackApiRequestAllItems.call(this, 'members', 'GET', '/users.list'); + for (const user of users) { + const userName = user.name; + const userId = user.id; + returnData.push({ + name: userName, + value: userId, + }); + } + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + + return returnData; + }, + // Get all the users to display them to user so that he can + // select them easily + async getChannels(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const channels = await slackApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list'); + for (const channel of channels) { + const channelName = channel.name; + const channelId = channel.id; + returnData.push({ + name: channelName, + value: channelId, + }); + } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + + return returnData; + }, + } + }; async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; - - const 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; + 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 < items.length; i++) { - let endpoint = ''; - body = {}; + for (let i = 0; i < length; i++) { qs = {}; - - resource = this.getNodeParameter('resource', i) as string; - operation = this.getNodeParameter('operation', i) as string; - if (resource === 'channel') { - 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; + //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); } - } else if (resource === 'message') { + //https://api.slack.com/methods/conversations.close + if (operation === 'close') { + const channel = this.getNodeParameter('channelId', i) as string; + const body: IDataObject = { + channel, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.close', body, qs); + } + //https://api.slack.com/methods/conversations.create + if (operation === 'create') { + const channel = this.getNodeParameter('channelId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + name: channel, + }; + if (additionalFields.isPrivate) { + body.is_private = additionalFields.isPrivate as boolean; + } + if (additionalFields.users) { + body.user_ids = (additionalFields.users as string[]).join(','); + } + responseData = await slackApiRequest.call(this, 'POST', '/conversations.create', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.kick + if (operation === 'kick') { + const channel = this.getNodeParameter('channelId', i) as string; + const userId = this.getNodeParameter('userId', i) as string; + const body: IDataObject = { + name: channel, + user: userId, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.kick', body, qs); + } + //https://api.slack.com/methods/conversations.join + if (operation === 'join') { + const channel = this.getNodeParameter('channelId', i) as string; + const body: IDataObject = { + channel, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.join', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.info + if (operation === 'get') { + const channel = this.getNodeParameter('channelId', i) as string; + qs.channel = channel, + responseData = await slackApiRequest.call(this, 'POST', '/conversations.info', {}, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.types) { + qs.types = (filters.types as string[]).join(','); + } + if (filters.excludeArchived) { + qs.exclude_archived = filters.excludeArchived as boolean; + } + if (returnAll === true) { + responseData = await slackApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/conversations.list', {}, qs); + responseData = responseData.channels; + } + } + //https://api.slack.com/methods/conversations.history + if (operation === 'history') { + const channel = this.getNodeParameter('channelId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.channel = channel; + if (filters.inclusive) { + qs.inclusive = filters.inclusive as boolean; + } + if (filters.latest) { + qs.latest = filters.latest as string; + } + if (filters.oldest) { + qs.oldest = filters.oldest as string; + } + if (returnAll === true) { + responseData = await slackApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.history', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/conversations.history', {}, qs); + responseData = responseData.messages; + } + } + //https://api.slack.com/methods/conversations.invite + if (operation === 'invite') { + const channel = this.getNodeParameter('channelId', i) as string; + const userId = this.getNodeParameter('userId', i) as string; + const body: IDataObject = { + channel, + user: userId, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.invite', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.leave + if (operation === 'leave') { + const channel = this.getNodeParameter('channelId', i) as string; + const body: IDataObject = { + channel, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.leave', body, qs); + } + //https://api.slack.com/methods/conversations.open + if (operation === 'open') { + const options = this.getNodeParameter('options', i) as IDataObject; + const body: IDataObject = {}; + if (options.channelId) { + body.channel = options.channelId as string; + } + if (options.returnIm) { + body.return_im = options.returnIm as boolean; + } + if (options.users) { + body.users = (options.users as string[]).join(','); + } + responseData = await slackApiRequest.call(this, 'POST', '/conversations.open', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.rename + if (operation === 'rename') { + const channel = this.getNodeParameter('channelId', i) as IDataObject; + const name = this.getNodeParameter('name', i) as IDataObject; + const body: IDataObject = { + channel, + name, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.rename', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.replies + if (operation === 'replies') { + const channel = this.getNodeParameter('channelId', i) as string; + const ts = this.getNodeParameter('ts', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.channel = channel; + qs.ts = ts; + if (filters.inclusive) { + qs.inclusive = filters.inclusive as boolean; + } + if (filters.latest) { + qs.latest = filters.latest as string; + } + if (filters.oldest) { + qs.oldest = filters.oldest as string; + } + if (returnAll === true) { + responseData = await slackApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.replies', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/conversations.replies', {}, qs); + responseData = responseData.messages; + } + } + //https://api.slack.com/methods/conversations.setPurpose + if (operation === 'setPurpose') { + const channel = this.getNodeParameter('channelId', i) as IDataObject; + const purpose = this.getNodeParameter('purpose', i) as IDataObject; + const body: IDataObject = { + channel, + purpose, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.setPurpose', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.setTopic + if (operation === 'setTopic') { + const channel = this.getNodeParameter('channelId', i) as IDataObject; + const topic = this.getNodeParameter('topic', i) as IDataObject; + const body: IDataObject = { + channel, + topic, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.setTopic', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.unarchive + if (operation === 'unarchive') { + const channel = this.getNodeParameter('channelId', i) as string; + const body: IDataObject = { + channel, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.unarchive', body, qs); + } + } + if (resource === 'message') { + //https://api.slack.com/methods/chat.postMessage if (operation === 'post') { - // ---------------------------------- - // message:post - // ---------------------------------- + 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 body: IDataObject = { + channel, + text, + }; - 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 (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; } - 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 +427,186 @@ 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); } + //https://api.slack.com/methods/chat.update + if (operation === 'update') { + const channel = this.getNodeParameter('channelId', i) as string; + const text = this.getNodeParameter('text', i) as string; + const ts = this.getNodeParameter('ts', i) as string; + const attachments = this.getNodeParameter('attachments', i, []) as unknown as IAttachment[]; + const body: IDataObject = { + channel, + text, + ts, + }; + + if (authentication === 'accessToken') { + body.as_user = this.getNodeParameter('as_user', i) as boolean; + } + + // The node does save the fields data differently than the API + // expects so fix the data befre we send the request + for (const attachment of attachments) { + if (attachment.fields !== undefined) { + if (attachment.fields.item !== undefined) { + // Move the field-content up + // @ts-ignore + attachment.fields = attachment.fields.item; + } else { + // If it does not have any items set remove it + delete attachment.fields; + } + } + } + body['attachments'] = attachments; + + // Add all the other options to the request + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + Object.assign(body, updateFields); + responseData = await slackApiRequest.call(this, 'POST', '/chat.update', body, qs); + } + } + if (resource === 'star') { + //https://api.slack.com/methods/stars.add + if (operation === 'add') { + const options = this.getNodeParameter('options', i) as IDataObject; + const body: IDataObject = {}; + if (options.channelId) { + body.channel = options.channelId as string; + } + if (options.fileId) { + body.file = options.fileId as string; + } + if (options.fileComment) { + body.file_comment = options.fileComment as string; + } + if (options.timestamp) { + body.timestamp = options.timestamp as string; + } + responseData = await slackApiRequest.call(this, 'POST', '/stars.add', body, qs); + } + //https://api.slack.com/methods/stars.remove + if (operation === 'delete') { + const options = this.getNodeParameter('options', i) as IDataObject; + const body: IDataObject = {}; + if (options.channelId) { + body.channel = options.channelId as string; + } + if (options.fileId) { + body.file = options.fileId as string; + } + if (options.fileComment) { + body.file_comment = options.fileComment as string; + } + if (options.timestamp) { + body.timestamp = options.timestamp as string; + } + responseData = await slackApiRequest.call(this, 'POST', '/stars.remove', body, qs); + } + //https://api.slack.com/methods/stars.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + if (returnAll === true) { + responseData = await slackApiRequestAllItems.call(this, 'items', 'GET', '/stars.list', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/stars.list', {}, qs); + responseData = responseData.items; + } + } + } + if (resource === 'file') { + //https://api.slack.com/methods/files.upload + if (operation === 'upload') { + const options = this.getNodeParameter('options', i) as IDataObject; + const binaryData = this.getNodeParameter('binaryData', i) as boolean; + const body: IDataObject = {}; + if (options.channelIds) { + body.channels = (options.channelIds as string[]).join(','); + } + if (options.fileName) { + body.filename = options.fileName as string; + } + if (options.initialComment) { + body.initial_comment = options.initialComment as string; + } + if (options.threadTs) { + body.thread_ts = options.threadTs as string; + } + if (options.title) { + body.title = options.title as string; + } + if (binaryData) { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + if (items[i].binary === undefined + //@ts-ignore + || items[i].binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + body.file = { + //@ts-ignore + value: Buffer.from(items[i].binary[binaryPropertyName].data, BINARY_ENCODING), + options: { + //@ts-ignore + filename: items[i].binary[binaryPropertyName].fileName, + //@ts-ignore + contentType: items[i].binary[binaryPropertyName].mimeType, + } + }; + responseData = await slackApiRequest.call(this, 'POST', '/files.upload', {}, qs, { 'Content-Type': 'multipart/form-data' }, { formData: body }); + responseData = responseData.file; + } else { + const fileContent = this.getNodeParameter('fileContent', i) as string; + body.content = fileContent; + responseData = await slackApiRequest.call(this, 'POST', '/files.upload', body, qs, { 'Content-Type': 'application/x-www-form-urlencoded' }, { form: body }); + responseData = responseData.file; + } + } + //https://api.slack.com/methods/files.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.channelId) { + qs.channel = filters.channelId as string; + } + if (filters.showFilesHidden) { + qs.show_files_hidden_by_limit = filters.showFilesHidden as boolean; + } + if (filters.tsFrom) { + qs.ts_from = filters.tsFrom as string; + } + if (filters.tsTo) { + qs.ts_to = filters.tsTo as string; + } + if (filters.types) { + qs.types = (filters.types as string[]).join(',') as string; + } + if (filters.userId) { + qs.user = filters.userId as string; + } + if (returnAll === true) { + responseData = await slackApiRequestAllItems.call(this, 'files', 'GET', '/files.list', {}, qs); + } else { + qs.count = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/files.list', {}, qs); + responseData = responseData.files; + } + } + //https://api.slack.com/methods/files.info + if (operation === 'get') { + const fileId = this.getNodeParameter('fileId', i) as string; + qs.file = fileId; + responseData = await slackApiRequest.call(this, 'GET', '/files.info', {}, qs); + responseData = responseData.file; + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); } else { - 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/nodes/Slack/StarDescription.ts b/packages/nodes-base/nodes/Slack/StarDescription.ts new file mode 100644 index 0000000000..282025b1ea --- /dev/null +++ b/packages/nodes-base/nodes/Slack/StarDescription.ts @@ -0,0 +1,191 @@ +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 of autenticated 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: '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).', + }, + { + 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: '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).', + }, + { + 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[]; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b8152aed6c..50eafb6ad6 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -69,6 +69,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",