first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,866 @@
import type { INodeProperties } from 'n8n-workflow';
import { slackChannelModes } from './utils';
export const channelOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['channel'],
},
},
options: [
{
name: 'Archive',
value: 'archive',
description: 'Archives a conversation',
action: 'Archive a channel',
},
{
name: 'Close',
value: 'close',
description: 'Closes a direct message or multi-person direct message',
action: 'Close a channel',
},
{
name: 'Create',
value: 'create',
description: 'Initiates a public or private channel-based conversation',
action: 'Create a channel',
},
{
name: 'Get',
value: 'get',
description: 'Get information about a channel',
action: 'Get a channel',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many channels in a Slack team',
action: 'Get many channels',
},
{
name: 'History',
value: 'history',
description: "Get a conversation's history of messages and events",
action: 'Get the history of a channel',
},
{
name: 'Invite',
value: 'invite',
description: 'Invite a user to a channel',
action: 'Invite a user to a channel',
},
{
name: 'Join',
value: 'join',
description: 'Joins an existing conversation',
action: 'Join a channel',
},
{
name: 'Kick',
value: 'kick',
description: 'Removes a user from a channel',
action: 'Kick a user from a channel',
},
{
name: 'Leave',
value: 'leave',
description: 'Leaves a conversation',
action: 'Leave a channel',
},
{
name: 'Member',
value: 'member',
description: 'List members of a conversation',
action: 'Get members of a channel',
},
{
name: 'Open',
value: 'open',
description: 'Opens or resumes a direct message or multi-person direct message',
action: 'Open a channel',
},
{
name: 'Rename',
value: 'rename',
description: 'Renames a conversation',
action: 'Rename a channel',
},
{
name: 'Replies',
value: 'replies',
description: 'Get a thread of messages posted to a channel',
action: 'Get a thread of messages posted to a channel',
},
{
name: 'Set Purpose',
value: 'setPurpose',
description: 'Sets the purpose for a conversation',
action: 'Set the purpose of a channel',
},
{
name: 'Set Topic',
value: 'setTopic',
description: 'Sets the topic for a conversation',
action: 'Set the topic of a channel',
},
{
name: 'Unarchive',
value: 'unarchive',
description: 'Unarchives a conversation',
action: 'Unarchive a channel',
},
],
default: 'create',
},
];
export const channelFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* channel:archive */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
description: 'The Slack channel to archive',
displayOptions: {
show: {
operation: ['archive'],
resource: ['channel'],
},
},
modes: slackChannelModes,
},
/* -------------------------------------------------------------------------- */
/* channel:close */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
modes: slackChannelModes,
displayOptions: {
show: {
operation: ['close'],
resource: ['channel'],
},
},
required: true,
description: 'The Slack channel to close',
},
/* -------------------------------------------------------------------------- */
/* channel:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'string',
default: '',
placeholder: 'Channel name',
displayOptions: {
show: {
operation: ['create'],
resource: ['channel'],
},
},
required: true,
},
{
displayName: 'Channel Visibility',
name: 'channelVisibility',
type: 'options',
default: 'public',
required: true,
displayOptions: {
show: {
operation: ['create'],
resource: ['channel'],
},
},
options: [
{
name: 'Public Channel',
value: 'public',
},
{
name: 'Private Channel',
value: 'private',
},
],
description:
'Whether to create a Public or a Private Slack channel. <a href="https://slack.com/help/articles/360017938993-What-is-a-channel">More info</a>.',
},
/* -------------------------------------------------------------------------- */
/* channel:invite */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
modes: slackChannelModes,
displayOptions: {
show: {
operation: ['invite'],
resource: ['channel'],
},
},
required: true,
description: 'The Slack channel to invite to',
},
{
displayName: 'User Names or IDs',
name: 'userIds',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
default: [],
displayOptions: {
show: {
operation: ['invite'],
resource: ['channel'],
},
},
required: true,
description:
'The ID of the user to invite into channel. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
/* -------------------------------------------------------------------------- */
/* channel:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
modes: slackChannelModes,
required: true,
displayOptions: {
show: {
operation: ['get'],
resource: ['channel'],
},
},
description: 'The Slack channel to get',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['channel'],
operation: ['get'],
},
},
options: [
{
displayName: 'Include Num of Members',
name: 'includeNumMembers',
type: 'boolean',
default: false,
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:kick */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
modes: slackChannelModes,
displayOptions: {
show: {
operation: ['kick'],
resource: ['channel'],
},
},
required: true,
description: 'The Slack channel to kick the user from',
},
{
displayName: 'User Name or ID',
name: 'userId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
displayOptions: {
show: {
operation: ['kick'],
resource: ['channel'],
},
},
default: '',
},
/* -------------------------------------------------------------------------- */
/* channel:join */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
description: 'The Slack channel to join',
modes: slackChannelModes,
displayOptions: {
show: {
operation: ['join'],
resource: ['channel'],
},
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* channel:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['channel'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['channel'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['channel'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Exclude Archived',
name: 'excludeArchived',
type: 'boolean',
default: false,
description: 'Whether to exclude archived channels from the list',
},
{
displayName: 'Types',
name: 'types',
type: 'multiOptions',
options: [
{
name: 'Public Channel',
value: 'public_channel',
},
{
name: 'Private Channel',
value: 'private_channel',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'mpim',
value: 'mpim',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'im',
value: 'im',
},
],
default: ['public_channel'],
description: 'Mix and match channel types',
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:history */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
modes: slackChannelModes,
displayOptions: {
show: {
operation: ['history'],
resource: ['channel'],
},
},
required: true,
description: 'The Slack channel to get the history from',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['channel'],
operation: ['history'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['channel'],
operation: ['history'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['channel'],
operation: ['history'],
},
},
options: [
{
displayName: 'Inclusive',
name: 'inclusive',
type: 'boolean',
default: false,
description:
'Whether to include messages with latest or oldest timestamp in results only when either timestamp is specified',
},
{
displayName: 'Latest',
name: 'latest',
type: 'dateTime',
default: '',
description: 'End of time range of messages to include in results',
},
{
displayName: 'Oldest',
name: 'oldest',
type: 'dateTime',
default: '',
description: 'Start of time range of messages to include in results',
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:leave */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
modes: slackChannelModes,
displayOptions: {
show: {
operation: ['leave'],
resource: ['channel'],
},
},
required: true,
description: 'The Slack channel to leave from',
},
/* -------------------------------------------------------------------------- */
/* channel:member */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
description: 'The Slack channel to get the members from',
placeholder: 'Select a channel...',
modes: slackChannelModes,
displayOptions: {
show: {
operation: ['member'],
resource: ['channel'],
},
},
required: true,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['channel'],
operation: ['member'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
typeOptions: {
minValue: 1,
},
description: 'Max number of results to return',
default: 100,
placeholder: 'Limit',
displayOptions: {
show: {
operation: ['member'],
resource: ['channel'],
returnAll: [false],
},
},
},
{
displayName: 'Resolve Data',
name: 'resolveData',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['channel'],
operation: ['member'],
},
},
description:
'Whether to resolve the data automatically. By default the response only contain the ID to resource.',
},
/* -------------------------------------------------------------------------- */
/* channel:open */
/* -------------------------------------------------------------------------- */
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['channel'],
operation: ['open'],
},
},
options: [
{
displayName: 'Channel ID',
name: 'channelId',
type: 'string',
default: '',
description:
"Resume a conversation by supplying an im or mpim's ID. Or provide the users field instead.",
},
{
displayName: 'Return IM',
name: 'returnIm',
type: 'boolean',
default: false,
description: 'Whether you want the full IM channel definition in the response',
},
{
displayName: 'User Names or IDs',
name: 'users',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
default: [],
description:
'If only one user is included, this creates a 1:1 DM. The ordering of the users is preserved whenever a multi-person direct message is returned. Supply a channel when not supplying users. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:rename */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
modes: slackChannelModes,
displayOptions: {
show: {
operation: ['rename'],
resource: ['channel'],
},
},
required: true,
description: 'The Slack channel to rename',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
displayOptions: {
show: {
operation: ['rename'],
resource: ['channel'],
},
},
default: '',
required: true,
description: 'New name for conversation',
},
/* -------------------------------------------------------------------------- */
/* channel:replies */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
modes: slackChannelModes,
displayOptions: {
show: {
operation: ['replies'],
resource: ['channel'],
},
},
required: true,
description: 'The Slack channel to replies to',
},
{
displayName: 'Message Timestamp',
name: 'ts',
type: 'number',
default: undefined,
displayOptions: {
show: {
operation: ['replies'],
resource: ['channel'],
},
},
required: true,
description: 'Timestamp of the message to reply',
placeholder: '1663233118.856619',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['channel'],
operation: ['replies'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['channel'],
operation: ['replies'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['channel'],
operation: ['replies'],
},
},
options: [
{
displayName: 'Inclusive',
name: 'inclusive',
type: 'boolean',
default: false,
description:
'Whether to include messages with latest or oldest timestamp in results only when either timestamp is specified',
},
{
displayName: 'Latest',
name: 'latest',
type: 'string',
default: '',
description: 'End of time range of messages to include in results',
},
{
displayName: 'Oldest',
name: 'oldest',
type: 'string',
default: '',
description: 'Start of time range of messages to include in results',
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:setPurpose */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
modes: slackChannelModes,
displayOptions: {
show: {
operation: ['setPurpose'],
resource: ['channel'],
},
},
required: true,
description: 'The Slack channel to set the purpose of',
},
{
displayName: 'Purpose',
name: 'purpose',
type: 'string',
displayOptions: {
show: {
operation: ['setPurpose'],
resource: ['channel'],
},
},
default: '',
required: true,
description: 'A new, specialer purpose',
},
/* -------------------------------------------------------------------------- */
/* channel:setTopic */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
modes: slackChannelModes,
displayOptions: {
show: {
operation: ['setTopic'],
resource: ['channel'],
},
},
required: true,
description: 'The Slack channel to set the topic of',
},
{
displayName: 'Topic',
name: 'topic',
type: 'string',
displayOptions: {
show: {
operation: ['setTopic'],
resource: ['channel'],
},
},
default: '',
required: true,
},
/* -------------------------------------------------------------------------- */
/* channel:unarchive */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
modes: slackChannelModes,
displayOptions: {
show: {
operation: ['unarchive'],
resource: ['channel'],
},
},
required: true,
description: 'The Slack channel to unarchive',
},
];
@@ -0,0 +1,326 @@
import type { INodeProperties } from 'n8n-workflow';
export const fileOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['file'],
},
},
options: [
{
name: 'Get',
value: 'get',
action: 'Get a file',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get & filters team files',
action: 'Get many files',
},
{
name: 'Upload',
value: 'upload',
description: 'Create or upload an existing file',
action: 'Upload a file',
},
],
default: 'upload',
},
];
export const fileFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* file:upload */
/* -------------------------------------------------------------------------- */
{
displayName: 'Binary File',
name: 'binaryData',
type: 'boolean',
default: false,
displayOptions: {
show: {
operation: ['upload'],
resource: ['file'],
'@version': [2, 2.1],
},
},
description: 'Whether the data to upload should be taken from binary field',
},
{
displayName: 'File Content',
name: 'fileContent',
type: 'string',
default: '',
displayOptions: {
show: {
operation: ['upload'],
resource: ['file'],
binaryData: [false],
'@version': [2, 2.1],
},
},
placeholder: '',
},
{
displayName: 'File Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
operation: ['upload'],
resource: ['file'],
binaryData: [true],
'@version': [2, 2.1],
},
},
placeholder: '',
description: 'Name of the binary property which contains the data for the file to be uploaded',
},
{
displayName: 'File Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
operation: ['upload'],
resource: ['file'],
'@version': [{ _cnd: { gte: 2.2 } }],
},
},
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 option',
options: [
{
displayName: 'Channel Names or IDs',
name: 'channelIds',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
displayOptions: {
show: {
'@version': [2, 2.1],
},
},
default: [],
description:
'The channels to send the file to. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Channel Name or ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 2.2 } }],
},
},
default: [],
description:
'The channel to send the file to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'File Name',
name: 'fileName',
type: 'string',
default: '',
},
{
displayName: 'Initial Comment',
name: 'initialComment',
type: 'string',
default: '',
description: 'The message text introducing the file in specified channels',
},
{
displayName: 'Thread Timestamp',
name: 'threadTs',
type: 'string',
default: '',
description:
"Provide another message's Timestamp value to upload this file as a reply. Never use a reply's Timestamp value; use its parent instead.",
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
},
],
},
/* ----------------------------------------------------------------------- */
/* file:getAll */
/* ----------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['file'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['file'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['file'],
},
},
default: {},
placeholder: 'Add Field',
options: [
{
displayName: 'Channel Name or ID',
name: 'channelId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
description:
'Channel containing the file to be listed. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Show Files Hidden By Limit',
name: 'showFilesHidden',
type: 'boolean',
default: false,
description:
'Whether to show truncated file info for files hidden due to being too old, and the team who owns the file being over the file limit',
},
{
displayName: 'Message Timestamp From',
name: 'tsFrom',
type: 'string',
default: '',
description: 'Filter files created after this timestamp (inclusive)',
},
{
displayName: 'Message Timestamp To',
name: 'tsTo',
type: 'string',
default: '',
description: 'Filter files created before this timestamp (inclusive)',
},
{
displayName: 'Types',
name: 'types',
type: 'multiOptions',
options: [
{
name: 'All',
value: 'all',
},
{
name: 'Google Docs',
value: 'gdocs',
},
{
name: 'Images',
value: 'images',
},
{
name: 'PDFs',
value: 'pdfs',
},
{
name: 'Snippets',
value: 'snippets',
},
{
name: 'Spaces',
value: 'spaces',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'zips',
value: 'zips',
},
],
default: ['all'],
description: 'Filter files by type',
},
{
displayName: 'User Name or ID',
name: 'userId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
description:
'Filter files created by a single user. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
],
},
/* ----------------------------------------------------------------------- */
/* file:get */
/* ----------------------------------------------------------------------- */
{
displayName: 'File ID',
name: 'fileId',
type: 'string',
displayOptions: {
show: {
resource: ['file'],
operation: ['get'],
},
},
default: '',
},
];
@@ -0,0 +1,474 @@
import get from 'lodash/get';
import type {
IDataObject,
IExecuteFunctions,
ILoadOptionsFunctions,
IOAuth2Options,
IHttpRequestMethods,
IRequestOptions,
IWebhookFunctions,
} from 'n8n-workflow';
import { NodeOperationError, sleep } from 'n8n-workflow';
import type { SendAndWaitMessageBody } from './MessageInterface';
import { getSendAndWaitConfig } from '../../../utils/sendAndWait/utils';
import { createUtmCampaignLink } from '../../../utils/utilities';
interface RateLimitOptions {
/**
* The maximum number of times to retry the request if a rate limit error occurs.
*/
maxRetries?: number;
/**
* The delay in milliseconds to wait before retrying the request if 'retry-after' header is not present.
*/
fallbackDelay?: number;
/**
* What to do when a rate limit error occurs and maxRetries is exceeded.
* - 'throw' will throw an error
* - 'stop' will return the data collected so far with cursor/page info
*/
onFail?: 'throw' | 'stop';
}
function isDefined<T>(value: T | undefined | null | ''): value is NonNullable<T> {
return value !== undefined && value !== null && value !== '';
}
export async function slackApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
method: IHttpRequestMethods,
resource: string,
body: object = {},
query: IDataObject = {},
headers: {} | undefined = undefined,
option: Partial<IRequestOptions> = {},
// tslint:disable-next-line:no-any
): Promise<any> {
const authenticationMethod = this.getNodeParameter('authentication', 0, 'accessToken') as string;
let options: IRequestOptions = {
method,
headers: headers ?? {
'Content-Type': 'application/json; charset=utf-8',
},
body,
qs: query,
uri: resource.startsWith('https') ? resource : `https://slack.com/api${resource}`,
json: true,
};
options = Object.assign({}, options, option);
if (Object.keys(body).length === 0) {
delete options.body;
}
if (Object.keys(query).length === 0) {
delete options.qs;
}
const oAuth2Options: IOAuth2Options = {
tokenType: 'Bearer',
property: 'authed_user.access_token',
};
const credentialType = authenticationMethod === 'accessToken' ? 'slackApi' : 'slackOAuth2Api';
const response = await this.helpers.requestWithAuthentication.call(
this,
credentialType,
options,
{
oauth2: oAuth2Options,
},
);
const responseData = options.resolveWithFullResponse ? response.body : response;
// don't try to handle errors if simple responses are disabled
if (responseData.ok === false && options.simple !== false) {
if (responseData.error === 'paid_teams_only') {
throw new NodeOperationError(
this.getNode(),
`Your current Slack plan does not include the resource '${
this.getNodeParameter('resource', 0) as string
}'`,
{
description:
'Hint: Upgrade to a Slack plan that includes the functionality you want to use.',
level: 'warning',
},
);
} else if (responseData.error === 'missing_scope') {
throw new NodeOperationError(
this.getNode(),
'Your Slack credential is missing required Oauth Scopes',
{
description: `Add the following scope(s) to your Slack App: ${responseData.needed}`,
level: 'warning',
},
);
} else if (responseData.error === 'not_admin') {
throw new NodeOperationError(
this.getNode(),
'Need higher Role Level for this Operation (e.g. Owner or Admin Rights)',
{
description:
'Hint: Check the Role of your Slack App Integration. For more information see the Slack Documentation - https://slack.com/help/articles/360018112273-Types-of-roles-in-Slack',
level: 'warning',
},
);
}
throw new NodeOperationError(
this.getNode(),
'Slack error response: ' + JSON.stringify(responseData.error),
);
}
if (responseData.ts !== undefined) {
Object.assign(responseData, { message_timestamp: responseData.ts });
delete responseData.ts;
}
return response;
}
function hasNextPage(responseData: any, propertyName: string): boolean {
const nextCursorDefined = isDefined(responseData.response_metadata?.next_cursor);
const morePagesAvailable =
isDefined(responseData.paging?.pages) &&
isDefined(responseData.paging.page) &&
responseData.paging.page < responseData.paging.pages;
const morePropertyPagesAvailable =
isDefined(responseData[propertyName].paging?.pages) &&
isDefined(responseData[propertyName].paging.page) &&
responseData[propertyName].paging.page < responseData[propertyName].paging.pages;
return nextCursorDefined || morePagesAvailable || morePropertyPagesAvailable;
}
export async function slackApiRequestAllItemsWithRateLimit<TResponseData>(
context: IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
body: any = {},
query: IDataObject = {},
options: RateLimitOptions = {},
): Promise<{ data: TResponseData[]; cursor?: string; page?: string }> {
const { maxRetries = 3, fallbackDelay = 30_000, onFail = 'throw' } = options;
const returnData: TResponseData[] = [];
let responseData;
query.page = 1;
//if the endpoint uses legacy pagination use count
//https://api.slack.com/docs/pagination#classic
if (endpoint.includes('files.list')) {
query.count = 100;
} else {
query.limit = query.limit ?? 100;
}
do {
let retryCount = 0;
let requestSuccessful = false;
while (!requestSuccessful) {
const response = await slackApiRequest.call(
context,
method,
endpoint,
body as IDataObject,
query,
{},
{ resolveWithFullResponse: true, simple: false },
);
const getErrMsg = () =>
'Slack error response: ' +
JSON.stringify(response.body?.error ?? response.statusMessage ?? 'Unknown error');
if (response.statusCode === 200) {
retryCount = 0;
responseData = response.body;
requestSuccessful = true;
} else if (response.statusCode === 429) {
const shouldRetry = retryCount < maxRetries;
// if onFail='stop' we should wait, so that user don't hit rate limit when scrolling through results
if (shouldRetry || onFail === 'stop') {
// Extract Retry-After header (in seconds) and convert to milliseconds
const retryAfterHeader =
response.headers?.['retry-after'] ?? response.headers?.['Retry-After'];
const waitTime = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1000 : fallbackDelay;
await sleep(waitTime);
retryCount++;
}
if (shouldRetry) {
continue;
}
if (onFail === 'stop') {
// Return the data collected so far with cursor/page info
const result: { data: TResponseData[]; cursor?: string; page?: string } = {
data: returnData,
};
// Add cursor if available
if (query.cursor) {
result.cursor = query.cursor as string;
}
// Add nextPage if using legacy pagination
if (responseData?.paging?.page) {
result.page = String(responseData.paging.page);
} else if (responseData?.[propertyName]?.paging?.page) {
result.page = String(responseData[propertyName].paging.page);
} else if (query.page) {
result.page = String(query.page);
}
return result;
}
throw new NodeOperationError(context.getNode(), getErrMsg());
} else {
throw new NodeOperationError(context.getNode(), getErrMsg());
}
}
query.cursor = get(responseData, 'response_metadata.next_cursor');
query.page++;
returnData.push.apply(
returnData,
(responseData[propertyName].matches as TResponseData[]) ?? responseData[propertyName],
);
} while (hasNextPage(responseData, propertyName));
return { data: returnData };
}
export async function slackApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
// tslint:disable-next-line:no-any
body: any = {},
query: IDataObject = {},
// tslint:disable-next-line:no-any
): Promise<any> {
const returnData: IDataObject[] = [];
let responseData;
query.page = 1;
//if the endpoint uses legacy pagination use count
//https://api.slack.com/docs/pagination#classic
if (endpoint.includes('files.list')) {
query.count = 100;
} else {
query.limit = query.limit ?? 100;
}
do {
responseData = await slackApiRequest.call(this, method, endpoint, body as IDataObject, query);
query.cursor = get(responseData, 'response_metadata.next_cursor');
query.page++;
returnData.push.apply(
returnData,
(responseData[propertyName].matches as IDataObject[]) ?? responseData[propertyName],
);
} while (hasNextPage(responseData, propertyName));
return returnData;
}
export function getMessageContent(
this: IExecuteFunctions | ILoadOptionsFunctions,
i: number,
nodeVersion: number,
instanceId?: string,
) {
const includeLinkToWorkflow = this.getNodeParameter(
'otherOptions.includeLinkToWorkflow',
i,
nodeVersion >= 2.1 ? true : false,
) as IDataObject;
const { id } = this.getWorkflow();
const automatedMessage = `_Automated with this <${this.getInstanceBaseUrl()}workflow/${id}?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=${encodeURIComponent(
'n8n-nodes-base.slack',
)}${instanceId ? '_' + instanceId : ''}|n8n workflow>_`;
const messageType = this.getNodeParameter('messageType', i) as string;
let content: IDataObject = {};
const text = this.getNodeParameter('text', i, '') as string;
switch (messageType) {
case 'text':
content = {
text: includeLinkToWorkflow ? `${text}\n${automatedMessage}` : text,
};
break;
case 'block':
content = this.getNodeParameter('blocksUi', i, {}, { ensureType: 'object' }) as IDataObject;
if (includeLinkToWorkflow && Array.isArray(content.blocks)) {
content.blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: automatedMessage,
},
});
}
if (text) {
content.text = text;
}
break;
case 'attachment':
const attachmentsUI = this.getNodeParameter('attachments', i) as IDataObject[];
const attachments: IDataObject[] = [];
for (const attachment of attachmentsUI) {
if (attachment.fields !== undefined) {
if ((attachment?.fields as IDataObject)?.item) {
attachment.fields = (attachment?.fields as IDataObject)?.item as IDataObject[];
}
}
attachments.push(attachment);
}
content = { attachments } as IDataObject;
if (includeLinkToWorkflow && Array.isArray(content.attachments)) {
content.attachments.push({
text: automatedMessage,
});
}
break;
default:
throw new NodeOperationError(
this.getNode(),
`The message type "${messageType}" is not known!`,
);
}
return content;
}
// tslint:disable-next-line:no-any
export function validateJSON(json: string | undefined): any {
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = undefined;
}
return result;
}
export function getTarget(
context: IExecuteFunctions,
itemIndex: number,
idType: 'user' | 'channel',
): string {
let target = '';
if (idType === 'channel') {
target = context.getNodeParameter('channelId', itemIndex, undefined, {
extractValue: true,
}) as string;
} else {
target = context.getNodeParameter('user', itemIndex, undefined, {
extractValue: true,
}) as string;
}
if (
idType === 'user' &&
(context.getNodeParameter('user', itemIndex) as IDataObject).mode === 'username'
) {
target = target.slice(0, 1) === '@' ? target : `@${target}`;
}
return target;
}
export function processThreadOptions(threadOptions: IDataObject | undefined): IDataObject {
const result: IDataObject = {};
if (threadOptions?.replyValues) {
const replyValues = threadOptions.replyValues as IDataObject;
if (replyValues.thread_ts) {
result.thread_ts = String(replyValues.thread_ts);
}
if (replyValues.reply_broadcast !== undefined) {
result.reply_broadcast = replyValues.reply_broadcast;
}
}
return result;
}
export function createSendAndWaitMessageBody(context: IExecuteFunctions) {
const select = context.getNodeParameter('select', 0) as 'user' | 'channel';
const target = getTarget(context, 0, select);
const config = getSendAndWaitConfig(context);
const body: SendAndWaitMessageBody = {
channel: target,
blocks: [
{
type: 'divider',
},
{
type: 'section',
text: {
type: context.getNode().typeVersion > 2.2 ? 'mrkdwn' : 'plain_text',
text: config.message,
emoji: true,
},
},
{
type: 'section',
text: {
type: 'plain_text',
text: ' ',
},
},
{
type: 'divider',
},
{
type: 'actions',
elements: config.options.map((option) => {
return {
type: 'button',
style: option.style === 'primary' ? 'primary' : undefined,
text: {
type: 'plain_text',
text: option.label,
emoji: true,
},
url: option.url,
};
}),
},
],
};
const otherOptions = context.getNodeParameter('options', 0, {});
const threadParams = processThreadOptions(otherOptions?.thread_ts as IDataObject);
Object.assign(body, threadParams);
if (config.appendAttribution) {
const instanceId = context.getInstanceId();
const attributionText = 'This message was sent automatically with ';
const link = createUtmCampaignLink('n8n-nodes-base.slack', instanceId);
body.blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: `${attributionText} _<${link}|n8n>_`,
},
});
}
if (context.getNode().typeVersion > 2.2 && body.blocks?.[1]?.type === 'section') {
delete body.blocks[1].text.emoji;
}
return body;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,38 @@
export interface IAttachment {
fields: {
item?: object[];
};
}
// Used for SendAndWaitMessage
export interface TextBlock {
type: string;
text: string;
emoji?: boolean;
}
export interface SectionBlock {
type: 'section';
text: TextBlock;
}
export interface DividerBlock {
type: 'divider';
}
export interface ButtonElement {
type: 'button';
style?: 'primary';
text: TextBlock;
url: string;
}
export interface ActionsBlock {
type: 'actions';
elements: ButtonElement[];
}
export interface SendAndWaitMessageBody {
channel: string;
blocks: Array<DividerBlock | SectionBlock | ActionsBlock>;
}
@@ -0,0 +1,87 @@
import type { INodeProperties } from 'n8n-workflow';
import { slackChannelModes } from './utils';
export const reactionOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['reaction'],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Adds a reaction to a message',
action: 'Add a reaction',
},
{
name: 'Get',
value: 'get',
description: 'Get the reactions of a message',
action: 'Get a reaction',
},
{
name: 'Remove',
value: 'remove',
description: 'Remove a reaction of a message',
action: 'Remove a reaction',
},
],
default: 'add',
},
];
export const reactionFields: INodeProperties[] = [
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
modes: slackChannelModes,
required: true,
displayOptions: {
show: {
resource: ['reaction'],
operation: ['add', 'get', 'remove'],
},
},
description: 'The Slack channel to get the reactions from',
},
{
displayName: 'Message Timestamp',
name: 'timestamp',
required: true,
type: 'number',
default: undefined,
displayOptions: {
show: {
resource: ['reaction'],
operation: ['add', 'get', 'remove'],
},
},
description: 'Timestamp of the message to add, get or remove',
placeholder: '1663233118.856619',
},
{
displayName: 'Emoji Code',
name: 'name',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['reaction'],
operation: ['add', 'remove'],
},
},
description:
'Emoji code to use for the message reaction. Use emoji codes like +1, not an actual emoji like 👍. <a target="_blank" href=" https://www.webfx.com/tools/emoji-cheat-sheet/">List of common emoji codes</a>',
placeholder: '+1',
},
];
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,224 @@
import type { INodeProperties } from 'n8n-workflow';
import { slackChannelModes } from './utils';
export const starOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['star'],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add a star to an item',
action: 'Add a star',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a star from an item',
action: 'Delete a star',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many stars of autenticated user',
action: 'Get many stars',
},
],
default: 'add',
},
];
export const starFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* star:add */
/* -------------------------------------------------------------------------- */
{
displayName: 'Item to Add Star',
name: 'target',
type: 'options',
required: true,
description: 'Choose whether to add a star to a message or a file',
default: '',
placeholder: 'Select...',
displayOptions: {
show: {
operation: ['add'],
resource: ['star'],
},
},
options: [
{
name: 'Message',
value: 'message',
},
{
name: 'File',
value: 'file',
},
],
},
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
description: 'The Slack channel to add a star to',
displayOptions: {
show: {
resource: ['star'],
operation: ['add'],
target: ['message', 'file'],
},
},
modes: slackChannelModes,
},
{
displayName: 'File ID',
name: 'fileId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['star'],
operation: ['add'],
target: ['file'],
},
},
description: 'File to add star to',
},
{
displayName: 'Message Timestamp',
name: 'timestamp',
type: 'number',
default: undefined,
displayOptions: {
show: {
resource: ['star'],
operation: ['add'],
target: ['message'],
},
},
description: 'Timestamp of the message to add',
placeholder: '1663233118.856619',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
operation: ['add'],
resource: ['star'],
},
},
default: {},
description: 'Options to set',
placeholder: 'Add option',
options: [
{
displayName: 'File Comment',
name: 'fileComment',
type: 'string',
default: '',
description: 'File comment to add star to',
},
],
},
/* ----------------------------------------------------------------------- */
/* star:delete */
/* ----------------------------------------------------------------------- */
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
operation: ['delete'],
resource: ['star'],
},
},
default: {},
description: 'Options to set',
placeholder: 'Add option',
options: [
{
displayName: 'Channel Name or ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
default: '',
description:
'Channel to add star to, or channel where the message to add star to was posted (used with timestamp). Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'File ID',
name: 'fileId',
type: 'string',
default: '',
description: 'File to add star to',
},
{
displayName: 'File Comment',
name: 'fileComment',
type: 'string',
default: '',
description: 'File comment to add star to',
},
{
displayName: 'Message Timestamp',
name: 'timestamp',
type: 'number',
default: 0,
description: 'Timestamp of the message to delete',
placeholder: '1663233118.856619',
},
],
},
/* -------------------------------------------------------------------------- */
/* star:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['star'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['star'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'Max number of results to return',
},
];
@@ -0,0 +1,303 @@
import type { INodeProperties } from 'n8n-workflow';
export const userOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['user'],
},
},
options: [
{
name: 'Get',
value: 'info',
description: 'Get information about a user',
action: 'Get information about a user',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get a list of many users',
action: 'Get many users',
},
{
name: "Get User's Profile",
value: 'getProfile',
description: "Get a user's profile",
action: "Get a user's profile",
},
{
name: "Get User's Status",
value: 'getPresence',
description: 'Get online status of a user',
action: "Get a user's presence status",
},
{
name: "Update User's Profile",
value: 'updateProfile',
description: "Update a user's profile",
action: "Update a user's profile",
},
],
default: 'info',
},
];
export const userFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* user:info */
/* -------------------------------------------------------------------------- */
{
displayName: 'User',
name: 'user',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a user...',
description: 'The ID of the user to get information about',
displayOptions: {
show: {
operation: ['info', 'getProfile'],
resource: ['user'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a user...',
typeOptions: {
searchListMethod: 'getUsers',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '[a-zA-Z0-9]{2,}',
errorMessage: 'Not a valid Slack User ID',
},
},
],
placeholder: 'U123AB45JGM',
},
],
},
/* -------------------------------------------------------------------------- */
/* user:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'Max number of results to return',
},
/* -------------------------------------------------------------------------- */
/* user:getPresence */
/* -------------------------------------------------------------------------- */
{
displayName: 'User',
name: 'user',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a user...',
description: 'The ID of the user to get the online status of',
displayOptions: {
show: {
operation: ['getPresence'],
resource: ['user'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a user...',
typeOptions: {
searchListMethod: 'getUsers',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '[a-zA-Z0-9]{2,}',
errorMessage: 'Not a valid Slack User ID',
},
},
],
placeholder: 'U123AB45JGM',
},
],
},
/* -------------------------------------------------------------------------- */
/* user:update user profile */
/* -------------------------------------------------------------------------- */
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['user'],
operation: ['updateProfile'],
},
},
options: [
{
displayName: 'Custom Fields',
name: 'customFieldUi',
placeholder: 'Add Custom Fields',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'customFieldValues',
displayName: 'Custom Field',
values: [
{
displayName: 'Field Name or ID',
name: 'id',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTeamFields',
},
default: '',
description:
'ID of the field to set. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Field Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the field to set',
},
{
displayName: 'Alt',
name: 'alt',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
default: '',
description: 'This field can only be changed by admins for users on paid teams',
},
{
displayName: 'First Name',
name: 'first_name',
type: 'string',
default: '',
},
{
displayName: 'Last Name',
name: 'last_name',
type: 'string',
default: '',
},
{
displayName: 'Set Status',
name: 'status',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
placeholder: 'Set Status',
options: [
{
displayName: 'Set Status',
name: 'set_status',
values: [
{
displayName: 'Status Emoji',
name: 'status_emoji',
type: 'string',
default: '',
description:
'Is a string referencing an emoji enabled for the Slack team, such as :mountain_railway:',
},
{
displayName: 'Status Expiration',
name: 'status_expiration',
type: 'dateTime',
default: '',
description:
'The number of minutes to wait until this status expires and is cleared. Optional.',
},
{
displayName: 'Status Text',
name: 'status_text',
type: 'string',
default: '',
description: 'Allows up to 100 characters, though we strongly encourage brevity',
},
],
},
],
},
{
displayName: 'User ID',
name: 'user',
type: 'string',
default: '',
description:
'ID of user to change. This argument may only be specified by team admins on paid teams.',
},
],
},
];
@@ -0,0 +1,435 @@
import type { INodeProperties } from 'n8n-workflow';
export const userGroupOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['userGroup'],
},
},
options: [
{
name: 'Add Users',
value: 'updateUsers',
action: 'Add users to a user group',
},
{
name: 'Create',
value: 'create',
action: 'Create a user group',
},
{
name: 'Disable',
value: 'disable',
action: 'Disable a user group',
},
{
name: 'Enable',
value: 'enable',
action: 'Enable a user group',
},
{
name: 'Get Many',
value: 'getAll',
action: 'Get many user groups',
},
{
name: 'Get Users',
value: 'getUsers',
action: 'Get users from a user group',
},
{
name: 'Update',
value: 'update',
action: 'Update a user group',
},
],
default: 'create',
},
];
export const userGroupFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* userGroup:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
displayOptions: {
show: {
operation: ['create'],
resource: ['userGroup'],
},
},
required: true,
description: 'A name for the User Group. Must be unique among User Groups.',
},
{
displayName: 'Options',
name: 'Options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['userGroup'],
operation: ['create'],
},
},
options: [
{
displayName: 'Channel Names or IDs',
name: 'channelIds',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
default: [],
description:
'A comma-separated string of encoded channel IDs for which the User Group uses as a default. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
description: 'A short description of the User Group',
},
{
displayName: 'Handle',
name: 'handle',
type: 'string',
default: '',
description: 'A mention handle. Must be unique among channels, users and User Groups.',
},
{
displayName: 'Include Count',
name: 'include_count',
type: 'boolean',
default: true,
description: 'Whether to include the number of users in each User Group',
},
],
},
/* ----------------------------------------------------------------------- */
/* userGroup:disable */
/* ----------------------------------------------------------------------- */
{
displayName: 'User Group ID',
name: 'userGroupId',
type: 'string',
default: '',
displayOptions: {
show: {
operation: ['disable'],
resource: ['userGroup'],
},
},
required: true,
description: 'The encoded ID of the User Group to update',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['userGroup'],
operation: ['disable'],
},
},
options: [
{
displayName: 'Include Count',
name: 'include_count',
type: 'boolean',
default: true,
description: 'Whether to include the number of users in each User Group',
},
],
},
/* ----------------------------------------------------------------------- */
/* userGroup:enable */
/* ----------------------------------------------------------------------- */
{
displayName: 'User Group ID',
name: 'userGroupId',
type: 'string',
default: '',
displayOptions: {
show: {
operation: ['enable'],
resource: ['userGroup'],
},
},
required: true,
description: 'The encoded ID of the User Group to update',
},
{
displayName: 'Options',
name: 'option',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['userGroup'],
operation: ['enable'],
},
},
options: [
{
displayName: 'Include Count',
name: 'include_count',
type: 'boolean',
default: true,
description: 'Whether to include the number of users in each User Group',
},
],
},
/* -------------------------------------------------------------------------- */
/* userGroup:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['userGroup'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['userGroup'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['userGroup'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Include Count',
name: 'include_count',
type: 'boolean',
default: true,
description: 'Whether to include the number of users in each User Group',
},
{
displayName: 'Include Disabled',
name: 'include_disabled',
type: 'boolean',
default: true,
description: 'Whether to include disabled User Groups',
},
{
displayName: 'Include Users',
name: 'include_users',
type: 'boolean',
default: true,
description: 'Whether to include the list of users for each User Group',
},
],
},
/* ----------------------------------------------------------------------- */
/* userGroup:update */
/* ----------------------------------------------------------------------- */
{
displayName: 'User Group ID',
name: 'userGroupId',
type: 'string',
default: '',
displayOptions: {
show: {
operation: ['update'],
resource: ['userGroup'],
},
},
required: true,
description: 'The encoded ID of the User Group to update',
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['userGroup'],
operation: ['update'],
},
},
options: [
{
displayName: 'Channel Names or IDs',
name: 'channels',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
default: [],
description:
'A comma-separated string of encoded channel IDs for which the User Group uses as a default. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
description: 'A short description of the User Group',
},
{
displayName: 'Handle',
name: 'handle',
type: 'string',
default: '',
description: 'A mention handle. Must be unique among channels, users and User Groups.',
},
{
displayName: 'Include Count',
name: 'include_count',
type: 'boolean',
default: true,
description: 'Whether to include the number of users in each User Group',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'A name for the User Group. Must be unique among User Groups.',
},
],
},
/* ----------------------------------------------------------------------- */
/* userGroup:updateUsers */
/* ----------------------------------------------------------------------- */
{
displayName: 'User Group ID',
name: 'userGroupId',
type: 'string',
default: '',
displayOptions: {
show: {
operation: ['updateUsers'],
resource: ['userGroup'],
},
},
required: true,
description: 'The encoded ID of the User Group to update',
},
{
displayName: 'User Names or IDs',
name: 'users',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
default: [],
displayOptions: {
show: {
operation: ['updateUsers'],
resource: ['userGroup'],
},
},
required: true,
description:
'Users to add to the User Group. Existing users will be preserved. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['userGroup'],
operation: ['updateUsers'],
},
},
options: [
{
displayName: 'Include Count',
name: 'include_count',
type: 'boolean',
default: true,
description: 'Whether to include the number of users in the User Group',
},
],
},
/* ----------------------------------------------------------------------- */
/* userGroup:getUsers */
/* ----------------------------------------------------------------------- */
{
displayName: 'User Group ID',
name: 'userGroupId',
type: 'string',
default: '',
displayOptions: {
show: {
operation: ['getUsers'],
resource: ['userGroup'],
},
},
required: true,
description: 'The encoded ID of the User Group',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['userGroup'],
operation: ['getUsers'],
},
},
options: [
{
displayName: 'Resolve Data',
name: 'resolveData',
type: 'boolean',
default: true,
description: 'Whether to return full user objects instead of just user IDs',
},
],
},
];
@@ -0,0 +1,53 @@
/* eslint-disable n8n-nodes-base/node-param-default-missing */
import type { INodePropertyMode } from 'n8n-workflow';
export const slackChannelModes: INodePropertyMode[] = [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a channel...',
typeOptions: {
searchListMethod: 'getChannels',
searchable: true,
slowLoadNotice: {
message: 'If loading is slow, try selecting a channel using "By ID" or "By URL"',
timeout: 15000,
},
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '[a-zA-Z0-9]{2,}',
errorMessage: 'Not a valid Slack Channel ID',
},
},
],
placeholder: 'C0122KQ70S7E',
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
placeholder: 'https://app.slack.com/client/TS9594PZK/B0556F47Z3A',
validation: [
{
type: 'regex',
properties: {
regex: 'http(s)?://app.slack.com/client/.*/([a-zA-Z0-9]{2,})',
errorMessage: 'Not a valid Slack Channel URL',
},
},
],
extractValue: {
type: 'regex',
regex: 'https://app.slack.com/client/.*/([a-zA-Z0-9]{2,})',
},
},
];