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,59 @@
{
"node": "n8n-nodes-base.slack",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "HITL"],
"subcategories": {
"HITL": ["Human in the Loop"]
},
"alias": ["human", "form", "wait", "hitl", "approval"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/slack/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.slack/"
}
],
"generic": [
{
"label": "6 e-commerce workflows to power up your Shopify s",
"icon": "store",
"url": "https://n8n.io/blog/no-code-ecommerce-workflow-automations/"
},
{
"label": "Automate your data processing pipeline in 9 steps",
"icon": "⚙️",
"url": "https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/"
},
{
"label": "How to get started with CRM automation (with 3 no-code workflow ideas",
"icon": "👥",
"url": "https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/"
},
{
"label": "5 tasks you can automate with the new Notion API ",
"icon": "⚡️",
"url": "https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/"
},
{
"label": "Build your own virtual assistant with n8n: A step by step guide",
"icon": "👦",
"url": "https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/"
},
{
"label": "How to automatically give kudos to contributors with GitHub, Slack, and n8n",
"icon": "👏",
"url": "https://n8n.io/blog/how-to-automatically-give-kudos-to-contributors-with-github-slack-and-n8n/"
},
{
"label": "How Common Knowledge use workflow automation for activism",
"icon": "✨",
"url": "https://n8n.io/blog/automations-for-activists/"
}
]
}
}
@@ -0,0 +1,30 @@
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
import { VersionedNodeType } from 'n8n-workflow';
import { SlackV1 } from './V1/SlackV1.node';
import { SlackV2 } from './V2/SlackV2.node';
export class Slack extends VersionedNodeType {
constructor() {
const baseDescription: INodeTypeBaseDescription = {
displayName: 'Slack',
name: 'slack',
icon: 'file:slack.svg',
group: ['output'],
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Slack API',
defaultVersion: 2.4,
};
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
1: new SlackV1(baseDescription),
2: new SlackV2(baseDescription),
2.1: new SlackV2(baseDescription),
2.2: new SlackV2(baseDescription),
2.3: new SlackV2(baseDescription),
2.4: new SlackV2(baseDescription),
};
super(nodeVersions, baseDescription);
}
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.slackTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/slack/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.slacktrigger/"
}
]
}
}
@@ -0,0 +1,431 @@
import {
type INodeListSearchItems,
type ILoadOptionsFunctions,
type INodeListSearchResult,
type INodePropertyOptions,
type IHookFunctions,
type IWebhookFunctions,
type IDataObject,
type INodeType,
type INodeTypeDescription,
type IWebhookResponseData,
type IBinaryKeyData,
NodeConnectionTypes,
} from 'n8n-workflow';
import { downloadFile, getChannelInfo, getUserInfo, verifySignature } from './SlackTriggerHelpers';
import { slackApiRequestAllItems } from './V2/GenericFunctions';
export class SlackTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Slack Trigger',
name: 'slackTrigger',
icon: 'file:slack.svg',
group: ['trigger'],
version: 1,
subtitle: '={{$parameter["eventFilter"].join(", ")}}',
description: 'Handle Slack events via webhooks',
defaults: {
name: 'Slack Trigger',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
credentials: [
{
name: 'slackApi',
required: true,
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden',
default: 'accessToken',
},
{
displayName:
'Set up a webhook in your Slack app to enable this node. <a href="https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.slacktrigger/#configure-a-webhook-in-slack" target="_blank">More info</a>. We also recommend setting up a <a href="https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.slacktrigger/#verify-the-webhook" target="_blank">signing secret</a> to ensure the authenticity of requests.',
name: 'notice',
type: 'notice',
default: '',
},
{
displayName: 'Trigger On',
name: 'trigger',
type: 'multiOptions',
options: [
{
name: 'Any Event',
value: 'any_event',
description: 'Triggers on any event',
},
{
name: 'Bot / App Mention',
value: 'app_mention',
description: 'When your bot or app is mentioned in a channel the app is added to',
},
{
name: 'File Made Public',
value: 'file_public',
description: 'When a file is made public',
},
{
name: 'File Shared',
value: 'file_share',
description: 'When a file is shared in a channel the app is added to',
},
{
name: 'New Message Posted to Channel',
value: 'message',
description: 'When a message is posted to a channel the app is added to',
},
{
name: 'New Public Channel Created',
value: 'channel_created',
description: 'When a new public channel is created',
},
{
name: 'New User',
value: 'team_join',
description: 'When a new user is added to Slack',
},
{
name: 'Reaction Added',
value: 'reaction_added',
description: 'When a reaction is added to a message the app is added to',
},
],
default: [],
},
{
displayName: 'Watch Whole Workspace',
name: 'watchWorkspace',
type: 'boolean',
default: false,
description:
'Whether to watch for the event in the whole workspace, rather than a specific channel',
displayOptions: {
show: {
trigger: ['any_event', 'message', 'reaction_added', 'file_share', 'app_mention'],
},
},
},
{
displayName:
'This will use one execution for every event in any channel your bot is in, use with caution',
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
trigger: ['any_event', 'message', 'reaction_added', 'file_share', 'app_mention'],
watchWorkspace: [true],
},
},
},
{
displayName: 'Channel to Watch',
name: 'channelId',
type: 'resourceLocator',
required: true,
default: { mode: 'list', value: '' },
placeholder: 'Select a channel...',
description:
'The Slack channel to listen to events from. Applies to events: Bot/App mention, File Shared, New Message Posted on Channel, Reaction Added.',
displayOptions: {
show: {
watchWorkspace: [false],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a channel...',
typeOptions: {
searchListMethod: 'getChannels',
searchable: true,
},
},
{
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,})',
},
},
],
},
{
displayName: 'Download Files',
name: 'downloadFiles',
type: 'boolean',
default: false,
description: 'Whether to download the files and add it to the output',
displayOptions: {
show: {
trigger: ['any_event', 'file_share'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Resolve IDs',
name: 'resolveIds',
type: 'boolean',
default: false,
description: 'Whether to resolve the IDs to their respective names and return them',
},
{
displayName: 'Usernames or IDs to Ignore',
name: 'userIds',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
default: [],
description:
'A comma-separated string of encoded user IDs. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
],
},
],
};
methods = {
listSearch: {
async getChannels(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
const qs = { types: 'public_channel,private_channel' };
const channels = (await slackApiRequestAllItems.call(
this,
'channels',
'GET',
'/conversations.list',
{},
qs,
)) as Array<{ id: string; name: string }>;
const results: INodeListSearchItems[] = channels
.map((c) => ({
name: c.name,
value: c.id,
}))
.filter(
(c) =>
!filter ||
c.name.toLowerCase().includes(filter.toLowerCase()) ||
c.value?.toString() === filter,
)
.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
return 0;
});
return { results };
},
},
loadOptions: {
async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const users = (await slackApiRequestAllItems.call(
this,
'members',
'GET',
'/users.list',
)) as Array<{ id: string; name: string }>;
for (const user of users) {
returnData.push({
name: user.name,
value: user.id,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
});
return returnData;
},
},
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
return true;
},
async create(this: IHookFunctions): Promise<boolean> {
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const filters = this.getNodeParameter('trigger', []) as string[];
const req = this.getRequestObject();
const options = this.getNodeParameter('options', {}) as IDataObject;
const binaryData: IBinaryKeyData = {};
const watchWorkspace = this.getNodeParameter('watchWorkspace', false) as boolean;
let eventChannel: string = '';
const isSignatureValid = await verifySignature.call(this);
if (!isSignatureValid) {
const res = this.getResponseObject();
res.status(401).send('Unauthorized').end();
return {
noWebhookResponse: true,
};
}
if (req.body.type === 'url_verification') {
// Check if the request is a challenge request
const res = this.getResponseObject();
res.status(200).json({ challenge: req.body.challenge }).end();
return {
noWebhookResponse: true,
};
}
// Check if the event type is in the filters
const eventType = req.body.event.type as string;
if (
!filters.includes('file_share') &&
!filters.includes('any_event') &&
!filters.includes(eventType)
) {
return {};
}
if (eventType !== 'team_join') {
eventChannel =
req.body.event.channel ?? req.body.event.item?.channel ?? req.body.event.channel_id;
// Check for single channel
if (!watchWorkspace) {
if (
eventChannel !==
(this.getNodeParameter('channelId', {}, { extractValue: true }) as string)
) {
return {};
}
}
}
// Check if user should be ignored
if (options.userIds) {
const userIds = options.userIds as string[];
if (userIds.includes(req.body.event.user)) {
return {};
}
}
if (options.resolveIds) {
if (req.body.event.user) {
if (req.body.event.type === 'reaction_added') {
req.body.event.user_resolved = await getUserInfo.call(this, req.body.event.user);
req.body.event.item_user_resolved = await getUserInfo.call(
this,
req.body.event.item_user,
);
} else if (req.body.event.type === 'team_join') {
req.body.event.user_resolved = req.body.event.user.name;
} else {
req.body.event.user_resolved = await getUserInfo.call(this, req.body.event.user);
}
}
if (eventChannel) {
const channel = await getChannelInfo.call(this, eventChannel);
const channelResolved = channel;
req.body.event.channel_resolved = channelResolved;
}
}
if (
req.body.event.subtype === 'file_share' &&
(filters.includes('file_share') || filters.includes('any_event'))
) {
if (this.getNodeParameter('downloadFiles', false) as boolean) {
for (let i = 0; i < req.body.event.files.length; i++) {
const file = (await downloadFile.call(
this,
req.body.event.files[i].url_private_download,
)) as Buffer;
binaryData[`file_${i}`] = await this.helpers.prepareBinaryData(
file,
req.body.event.files[i].name,
req.body.event.files[i].mimetype,
);
}
}
}
return {
workflowData: [
[
{
json: req.body.event,
binary: Object.keys(binaryData).length ? binaryData : undefined,
},
],
],
};
}
}
@@ -0,0 +1,126 @@
import { createHmac } from 'crypto';
import type { IHttpRequestOptions, IWebhookFunctions } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { slackApiRequest } from './V2/GenericFunctions';
import { verifySignature as verifySignatureGeneric } from '../../utils/webhook-signature-verification';
export async function getUserInfo(this: IWebhookFunctions, userId: string): Promise<any> {
const user = await slackApiRequest.call(
this,
'GET',
'/users.info',
{},
{
user: userId,
},
);
return user.user.name;
}
export async function getChannelInfo(this: IWebhookFunctions, channelId: string): Promise<any> {
const channel = await slackApiRequest.call(
this,
'GET',
'/conversations.info',
{},
{
channel: channelId,
},
);
return channel.channel.name;
}
export async function downloadFile(this: IWebhookFunctions, url: string): Promise<any> {
let options: IHttpRequestOptions = {
method: 'GET',
url,
};
const requestOptions = {
encoding: 'arraybuffer',
returnFullResponse: true,
json: false,
useStream: true,
};
options = Object.assign({}, options, requestOptions);
const response = await this.helpers.requestWithAuthentication.call(this, 'slackApi', options);
if (response.ok === false) {
if (response.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 (response.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: ${response.needed}`,
level: 'warning',
},
);
}
throw new NodeOperationError(
this.getNode(),
'Slack error response: ' + JSON.stringify(response.error),
);
}
return response;
}
export async function verifySignature(this: IWebhookFunctions): Promise<boolean> {
const credential = await this.getCredentials('slackApi');
const req = this.getRequestObject();
const timestamp = req.header('x-slack-request-timestamp');
if (!timestamp) {
return false;
}
const signatureSecret = credential.signatureSecret;
try {
const isValid = verifySignatureGeneric({
getExpectedSignature: () => {
if (!signatureSecret || typeof signatureSecret !== 'string' || !req.rawBody) {
return null;
}
const hmac = createHmac('sha256', signatureSecret);
if (Buffer.isBuffer(req.rawBody)) {
hmac.update(`v0:${timestamp}:`);
hmac.update(req.rawBody);
} else {
const rawBodyString =
typeof req.rawBody === 'string' ? req.rawBody : JSON.stringify(req.rawBody);
hmac.update(`v0:${timestamp}:${rawBodyString}`);
}
const computedSignature = `v0=${hmac.digest('hex')}`;
return computedSignature;
},
skipIfNoExpectedSignature: !signatureSecret || typeof signatureSecret !== 'string',
getActualSignature: () => {
const actualSignature = req.header('x-slack-signature');
return typeof actualSignature === 'string' ? actualSignature : null;
},
getTimestamp: () => timestamp,
});
return isValid;
} catch (error) {
return false;
}
}
@@ -0,0 +1,893 @@
import type { INodeProperties } from 'n8n-workflow';
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 or ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
displayOptions: {
show: {
operation: ['archive'],
resource: ['channel'],
},
},
default: '',
required: true,
description:
'The name of the channel to archive. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
/* -------------------------------------------------------------------------- */
/* channel:close */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel Name or ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
displayOptions: {
show: {
operation: ['close'],
resource: ['channel'],
},
},
default: '',
required: true,
description:
'The name of the channel to close. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
/* -------------------------------------------------------------------------- */
/* 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: 'Whether to create a private channel instead of a public one',
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:invite */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel Name or ID',
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. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
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: '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 or ID',
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. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
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 or ID',
name: 'channelId',
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: '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: '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 or ID',
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. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
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 or ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
displayOptions: {
show: {
operation: ['leave'],
resource: ['channel'],
},
},
default: '',
required: true,
description:
'The name of the channel to leave. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
/* -------------------------------------------------------------------------- */
/* channel:member */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel Name or ID',
name: 'channelId',
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: 'getChannels',
},
default: '',
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 or ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
displayOptions: {
show: {
operation: ['rename'],
resource: ['channel'],
},
},
default: '',
required: true,
description:
'The name of the channel to rename. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
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 or ID',
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. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
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: '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 or ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
displayOptions: {
show: {
operation: ['setPurpose'],
resource: ['channel'],
},
},
default: '',
required: true,
description:
'Conversation to set the purpose of. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
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 or ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
displayOptions: {
show: {
operation: ['setTopic'],
resource: ['channel'],
},
},
default: '',
required: true,
description:
'Conversation to set the topic of. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
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 or ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
displayOptions: {
show: {
operation: ['unarchive'],
resource: ['channel'],
},
},
default: '',
required: true,
description:
'The ID of the channel to unarchive. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
];
@@ -0,0 +1,290 @@
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',
description: 'Get a file info',
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'],
},
},
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],
},
},
placeholder: '',
description: 'The text content of the file to upload',
},
{
displayName: 'Input Binary Field',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
operation: ['upload'],
resource: ['file'],
binaryData: [true],
},
},
placeholder: '',
hint: 'The name of the input binary field containing 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',
},
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: '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: '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: 'Timestamp From',
name: 'tsFrom',
type: 'string',
default: '',
description: 'Filter files created after this timestamp (inclusive)',
},
{
displayName: '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,128 @@
import get from 'lodash/get';
import type {
IExecuteFunctions,
ILoadOptionsFunctions,
IDataObject,
IOAuth2Options,
JsonObject,
IHttpRequestMethods,
IRequestOptions,
} from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
export async function slackApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
resource: string,
body: object = {},
query: IDataObject = {},
headers: IDataObject | undefined = undefined,
option: IDataObject = {},
): 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: `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',
};
try {
const credentialType = authenticationMethod === 'accessToken' ? 'slackApi' : 'slackOAuth2Api';
const response = await this.helpers.requestWithAuthentication.call(
this,
credentialType,
options,
{
oauth2: oAuth2Options,
},
);
if (response.ok === false) {
if (response.error === 'paid_teams_only') {
throw new NodeOperationError(
this.getNode(),
`Your current Slack plan does not include the resource '${this.getNodeParameter(
'resource',
0,
)}'`,
{
description:
'Hint: Upgrate to the Slack plan that includes the funcionality you want to use.',
},
);
}
throw new NodeOperationError(
this.getNode(),
'Slack error response: ' + JSON.stringify(response),
);
}
return response;
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function slackApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
body: any = {},
query: IDataObject = {},
): 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 = 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] as IDataObject[]);
} while (
(responseData.response_metadata?.next_cursor !== undefined &&
responseData.response_metadata.next_cursor !== '' &&
responseData.response_metadata.next_cursor !== null) ||
(responseData.paging?.pages !== undefined &&
responseData.paging.page !== undefined &&
responseData.paging.page < responseData.paging.pages)
);
return returnData;
}
export function validateJSON(json: string | undefined): any {
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = undefined;
}
return result;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
export interface IAttachment {
fields: {
item?: object[];
};
}
@@ -0,0 +1,86 @@
import type { INodeProperties } from 'n8n-workflow';
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 or ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
required: true,
default: '',
displayOptions: {
show: {
resource: ['reaction'],
operation: ['add', 'get', 'remove'],
},
},
description:
'Channel containing the message. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Emoji',
name: 'name',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['reaction'],
operation: ['add', 'remove'],
},
},
description: 'Name of emoji',
placeholder: '+1',
},
{
displayName: 'Timestamp',
name: 'timestamp',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['reaction'],
operation: ['add', 'get', 'remove'],
},
},
description: 'Timestamp of the message',
},
];
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,177 @@
import type { INodeProperties } from 'n8n-workflow';
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: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
operation: ['add'],
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 Comment',
name: 'fileComment',
type: 'string',
default: '',
description: 'File comment to add star to',
},
{
displayName: 'File ID',
name: 'fileId',
type: 'string',
default: '',
description: 'File 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 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: '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: '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,114 @@
import type { INodeProperties } from 'n8n-workflow';
export const userOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['user'],
},
},
options: [
{
name: 'Info',
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 Presence',
value: 'getPresence',
description: 'Get online status of a user',
action: "Get a user's presence status",
},
],
default: 'info',
},
];
export const userFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* user:info */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'user',
type: 'string',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
default: '',
displayOptions: {
show: {
operation: ['info'],
resource: ['user'],
},
},
required: true,
description: 'The ID of the user to get information about',
},
/* -------------------------------------------------------------------------- */
/* 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 ID',
name: 'user',
type: 'string',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
default: '',
displayOptions: {
show: {
operation: ['getPresence'],
resource: ['user'],
},
},
required: true,
description: 'The ID of the user to get the online status of',
},
];
@@ -0,0 +1,334 @@
import type { INodeProperties } from 'n8n-workflow';
export const userGroupOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['userGroup'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a user group',
action: 'Create a user group',
},
{
name: 'Disable',
value: 'disable',
description: 'Disable a user group',
action: 'Disable a user group',
},
{
name: 'Enable',
value: 'enable',
description: 'Enable a user group',
action: 'Enable a user group',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many user groups',
action: 'Get many user groups',
},
{
name: 'Update',
value: 'update',
description: 'Update a user group',
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: 'Additional Fields',
name: 'additionalFields',
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: 'Additional Fields',
name: 'additionalFields',
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: 'Additional Fields',
name: 'additionalFields',
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: 'Additional Fields',
name: 'additionalFields',
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.',
},
],
},
];
@@ -0,0 +1,177 @@
import type { INodeProperties } from 'n8n-workflow';
export const userProfileOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['userProfile'],
},
},
options: [
{
name: 'Get',
value: 'get',
description: "Get your user's profile",
action: 'Get a user profile',
},
{
name: 'Update',
value: 'update',
description: "Update user's profile",
action: 'Update a user profile',
},
],
default: 'get',
},
];
export const userProfileFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* userProfile:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['userProfile'],
operation: ['update'],
},
},
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: '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:
'Is an integer specifying seconds since the epoch, more commonly known as "UNIX time". Providing 0 or omitting this field results in a custom status that will not expire.',
},
{
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.',
},
],
},
/* -------------------------------------------------------------------------- */
/* userProfile:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['userProfile'],
operation: ['get'],
},
},
options: [
{
displayName: 'Include Labels',
name: 'include_labels',
type: 'boolean',
default: false,
description: 'Whether to include labels for each ID in custom profile fields',
},
{
displayName: 'User ID',
name: 'user',
type: 'string',
default: '',
description: 'User to retrieve profile info for',
},
],
},
];
@@ -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,})',
},
},
];
@@ -0,0 +1,109 @@
{
"type": "object",
"properties": {
"context_team_id": {
"type": "string"
},
"created": {
"type": "integer"
},
"creator": {
"type": "string"
},
"id": {
"type": "string"
},
"is_archived": {
"type": "boolean"
},
"is_channel": {
"type": "boolean"
},
"is_ext_shared": {
"type": "boolean"
},
"is_general": {
"type": "boolean"
},
"is_group": {
"type": "boolean"
},
"is_im": {
"type": "boolean"
},
"is_member": {
"type": "boolean"
},
"is_mpim": {
"type": "boolean"
},
"is_org_shared": {
"type": "boolean"
},
"is_pending_ext_shared": {
"type": "boolean"
},
"is_private": {
"type": "boolean"
},
"is_shared": {
"type": "boolean"
},
"last_read": {
"type": "string"
},
"name": {
"type": "string"
},
"name_normalized": {
"type": "string"
},
"parent_conversation": {
"type": "null"
},
"priority": {
"type": "integer"
},
"purpose": {
"type": "object",
"properties": {
"creator": {
"type": "string"
},
"last_set": {
"type": "integer"
},
"value": {
"type": "string"
}
}
},
"shared_team_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"topic": {
"type": "object",
"properties": {
"creator": {
"type": "string"
},
"last_set": {
"type": "integer"
},
"value": {
"type": "string"
}
}
},
"unlinked": {
"type": "integer"
},
"updated": {
"type": "integer"
}
},
"version": 2
}
@@ -0,0 +1,159 @@
{
"type": "object",
"properties": {
"context_team_id": {
"type": "string"
},
"created": {
"type": "integer"
},
"creator": {
"type": "string"
},
"id": {
"type": "string"
},
"is_archived": {
"type": "boolean"
},
"is_channel": {
"type": "boolean"
},
"is_ext_shared": {
"type": "boolean"
},
"is_general": {
"type": "boolean"
},
"is_group": {
"type": "boolean"
},
"is_im": {
"type": "boolean"
},
"is_member": {
"type": "boolean"
},
"is_mpim": {
"type": "boolean"
},
"is_org_shared": {
"type": "boolean"
},
"is_pending_ext_shared": {
"type": "boolean"
},
"is_private": {
"type": "boolean"
},
"is_shared": {
"type": "boolean"
},
"last_read": {
"type": "string"
},
"name": {
"type": "string"
},
"name_normalized": {
"type": "string"
},
"parent_conversation": {
"type": "null"
},
"previous_names": {
"type": "array",
"items": {
"type": "string"
}
},
"properties": {
"type": "object",
"properties": {
"canvas": {
"type": "object",
"properties": {
"file_id": {
"type": "string"
},
"is_empty": {
"type": "boolean"
},
"quip_thread_id": {
"type": "string"
}
}
},
"tabs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"label": {
"type": "string"
},
"type": {
"type": "string"
}
}
}
},
"tabz": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
}
}
}
},
"purpose": {
"type": "object",
"properties": {
"creator": {
"type": "string"
},
"last_set": {
"type": "integer"
},
"value": {
"type": "string"
}
}
},
"shared_team_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"topic": {
"type": "object",
"properties": {
"creator": {
"type": "string"
},
"last_set": {
"type": "integer"
},
"value": {
"type": "string"
}
}
},
"unlinked": {
"type": "integer"
},
"updated": {
"type": "integer"
}
},
"version": 1
}
@@ -0,0 +1,162 @@
{
"type": "object",
"properties": {
"context_team_id": {
"type": "string"
},
"created": {
"type": "integer"
},
"creator": {
"type": "string"
},
"id": {
"type": "string"
},
"is_archived": {
"type": "boolean"
},
"is_channel": {
"type": "boolean"
},
"is_ext_shared": {
"type": "boolean"
},
"is_general": {
"type": "boolean"
},
"is_group": {
"type": "boolean"
},
"is_im": {
"type": "boolean"
},
"is_member": {
"type": "boolean"
},
"is_mpim": {
"type": "boolean"
},
"is_org_shared": {
"type": "boolean"
},
"is_pending_ext_shared": {
"type": "boolean"
},
"is_private": {
"type": "boolean"
},
"is_shared": {
"type": "boolean"
},
"name": {
"type": "string"
},
"name_normalized": {
"type": "string"
},
"num_members": {
"type": "integer"
},
"parent_conversation": {
"type": "null"
},
"previous_names": {
"type": "array",
"items": {
"type": "string"
}
},
"properties": {
"type": "object",
"properties": {
"canvas": {
"type": "object",
"properties": {
"file_id": {
"type": "string"
},
"is_empty": {
"type": "boolean"
},
"quip_thread_id": {
"type": "string"
}
}
},
"tabs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"label": {
"type": "string"
},
"type": {
"type": "string"
}
}
}
},
"tabz": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
}
},
"use_case": {
"type": "string"
}
}
},
"purpose": {
"type": "object",
"properties": {
"creator": {
"type": "string"
},
"last_set": {
"type": "integer"
},
"value": {
"type": "string"
}
}
},
"shared_team_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"topic": {
"type": "object",
"properties": {
"creator": {
"type": "string"
},
"last_set": {
"type": "integer"
},
"value": {
"type": "string"
}
}
},
"unlinked": {
"type": "integer"
},
"updated": {
"type": "integer"
}
},
"version": 1
}
@@ -0,0 +1,77 @@
{
"type": "object",
"properties": {
"blocks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"block_id": {
"type": "string"
},
"elements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"elements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"style": {
"type": "object",
"properties": {
"bold": {
"type": "boolean"
},
"italic": {
"type": "boolean"
}
}
},
"text": {
"type": "string"
},
"type": {
"type": "string"
}
}
}
},
"type": {
"type": "string"
}
}
}
},
"type": {
"type": "string"
}
}
}
},
"bot_id": {
"type": "string"
},
"client_msg_id": {
"type": "string"
},
"team": {
"type": "string"
},
"text": {
"type": "string"
},
"ts": {
"type": "string"
},
"type": {
"type": "string"
},
"user": {
"type": "string"
}
},
"version": 8
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,109 @@
{
"type": "object",
"properties": {
"context_team_id": {
"type": "string"
},
"created": {
"type": "integer"
},
"creator": {
"type": "string"
},
"id": {
"type": "string"
},
"is_archived": {
"type": "boolean"
},
"is_channel": {
"type": "boolean"
},
"is_ext_shared": {
"type": "boolean"
},
"is_general": {
"type": "boolean"
},
"is_group": {
"type": "boolean"
},
"is_im": {
"type": "boolean"
},
"is_member": {
"type": "boolean"
},
"is_mpim": {
"type": "boolean"
},
"is_org_shared": {
"type": "boolean"
},
"is_pending_ext_shared": {
"type": "boolean"
},
"is_private": {
"type": "boolean"
},
"is_shared": {
"type": "boolean"
},
"name": {
"type": "string"
},
"name_normalized": {
"type": "string"
},
"parent_conversation": {
"type": "null"
},
"previous_names": {
"type": "array",
"items": {
"type": "string"
}
},
"purpose": {
"type": "object",
"properties": {
"creator": {
"type": "string"
},
"last_set": {
"type": "integer"
},
"value": {
"type": "string"
}
}
},
"shared_team_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"topic": {
"type": "object",
"properties": {
"creator": {
"type": "string"
},
"last_set": {
"type": "integer"
},
"value": {
"type": "string"
}
}
},
"unlinked": {
"type": "integer"
},
"updated": {
"type": "integer"
}
},
"version": 3
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"member": {
"type": "string"
}
},
"version": 3
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,136 @@
{
"type": "object",
"properties": {
"app_id": {
"type": "string"
},
"blocks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"block_id": {
"type": "string"
},
"elements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"elements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"type": {
"type": "string"
}
}
}
},
"type": {
"type": "string"
}
}
}
},
"type": {
"type": "string"
}
}
}
},
"bot_id": {
"type": "string"
},
"bot_profile": {
"type": "object",
"properties": {
"app_id": {
"type": "string"
},
"deleted": {
"type": "boolean"
},
"icons": {
"type": "object",
"properties": {
"image_36": {
"type": "string"
},
"image_48": {
"type": "string"
},
"image_72": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"team_id": {
"type": "string"
},
"updated": {
"type": "integer"
},
"user_id": {
"type": "string"
}
}
},
"client_msg_id": {
"type": "string"
},
"is_locked": {
"type": "boolean"
},
"latest_reply": {
"type": "string"
},
"parent_user_id": {
"type": "string"
},
"reply_count": {
"type": "integer"
},
"reply_users": {
"type": "array",
"items": {
"type": "string"
}
},
"reply_users_count": {
"type": "integer"
},
"subscribed": {
"type": "boolean"
},
"team": {
"type": "string"
},
"text": {
"type": "string"
},
"thread_ts": {
"type": "string"
},
"ts": {
"type": "string"
},
"type": {
"type": "string"
},
"user": {
"type": "string"
}
},
"version": 3
}
@@ -0,0 +1,109 @@
{
"type": "object",
"properties": {
"context_team_id": {
"type": "string"
},
"created": {
"type": "integer"
},
"creator": {
"type": "string"
},
"id": {
"type": "string"
},
"is_archived": {
"type": "boolean"
},
"is_channel": {
"type": "boolean"
},
"is_ext_shared": {
"type": "boolean"
},
"is_general": {
"type": "boolean"
},
"is_group": {
"type": "boolean"
},
"is_im": {
"type": "boolean"
},
"is_member": {
"type": "boolean"
},
"is_mpim": {
"type": "boolean"
},
"is_org_shared": {
"type": "boolean"
},
"is_pending_ext_shared": {
"type": "boolean"
},
"is_private": {
"type": "boolean"
},
"is_shared": {
"type": "boolean"
},
"name": {
"type": "string"
},
"name_normalized": {
"type": "string"
},
"parent_conversation": {
"type": "null"
},
"previous_names": {
"type": "array",
"items": {
"type": "string"
}
},
"purpose": {
"type": "object",
"properties": {
"creator": {
"type": "string"
},
"last_set": {
"type": "integer"
},
"value": {
"type": "string"
}
}
},
"shared_team_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"topic": {
"type": "object",
"properties": {
"creator": {
"type": "string"
},
"last_set": {
"type": "integer"
},
"value": {
"type": "string"
}
}
},
"unlinked": {
"type": "integer"
},
"updated": {
"type": "integer"
}
},
"version": 1
}
@@ -0,0 +1,134 @@
{
"type": "object",
"properties": {
"aac": {
"type": "string"
},
"audio_wave_samples": {
"type": "array",
"items": {
"type": "integer"
}
},
"channels": {
"type": "array",
"items": {
"type": "string"
}
},
"comments_count": {
"type": "integer"
},
"created": {
"type": "integer"
},
"display_as_bot": {
"type": "boolean"
},
"duration_ms": {
"type": "integer"
},
"editable": {
"type": "boolean"
},
"external_type": {
"type": "string"
},
"file_access": {
"type": "string"
},
"filetype": {
"type": "string"
},
"groups": {
"type": "array",
"items": {
"type": "string"
}
},
"has_more_shares": {
"type": "boolean"
},
"has_rich_preview": {
"type": "boolean"
},
"id": {
"type": "string"
},
"ims": {
"type": "array",
"items": {
"type": "string"
}
},
"is_external": {
"type": "boolean"
},
"is_public": {
"type": "boolean"
},
"is_starred": {
"type": "boolean"
},
"media_display_type": {
"type": "string"
},
"mimetype": {
"type": "string"
},
"mode": {
"type": "string"
},
"name": {
"type": "string"
},
"permalink": {
"type": "string"
},
"permalink_public": {
"type": "string"
},
"pretty_type": {
"type": "string"
},
"public_url_shared": {
"type": "boolean"
},
"size": {
"type": "integer"
},
"subtype": {
"type": "string"
},
"timestamp": {
"type": "integer"
},
"title": {
"type": "string"
},
"transcription": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
},
"url_private": {
"type": "string"
},
"url_private_download": {
"type": "string"
},
"user": {
"type": "string"
},
"user_team": {
"type": "string"
},
"username": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,96 @@
{
"type": "object",
"properties": {
"channels": {
"type": "array",
"items": {
"type": "string"
}
},
"comments_count": {
"type": "integer"
},
"created": {
"type": "integer"
},
"display_as_bot": {
"type": "boolean"
},
"editable": {
"type": "boolean"
},
"external_type": {
"type": "string"
},
"filetype": {
"type": "string"
},
"groups": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"type": "string"
},
"ims": {
"type": "array",
"items": {
"type": "string"
}
},
"is_external": {
"type": "boolean"
},
"is_public": {
"type": "boolean"
},
"media_display_type": {
"type": "string"
},
"mimetype": {
"type": "string"
},
"mode": {
"type": "string"
},
"name": {
"type": "string"
},
"permalink": {
"type": "string"
},
"pretty_type": {
"type": "string"
},
"public_url_shared": {
"type": "boolean"
},
"size": {
"type": "integer"
},
"timestamp": {
"type": "integer"
},
"title": {
"type": "string"
},
"url_private": {
"type": "string"
},
"url_private_download": {
"type": "string"
},
"user": {
"type": "string"
},
"user_team": {
"type": "string"
},
"username": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,105 @@
{
"type": "object",
"properties": {
"channels": {
"type": "array",
"items": {
"type": "string"
}
},
"comments_count": {
"type": "integer"
},
"created": {
"type": "integer"
},
"display_as_bot": {
"type": "boolean"
},
"editable": {
"type": "boolean"
},
"external_type": {
"type": "string"
},
"file_access": {
"type": "string"
},
"filetype": {
"type": "string"
},
"groups": {
"type": "array",
"items": {
"type": "string"
}
},
"has_more_shares": {
"type": "boolean"
},
"has_rich_preview": {
"type": "boolean"
},
"id": {
"type": "string"
},
"is_external": {
"type": "boolean"
},
"is_public": {
"type": "boolean"
},
"is_starred": {
"type": "boolean"
},
"media_display_type": {
"type": "string"
},
"mimetype": {
"type": "string"
},
"mode": {
"type": "string"
},
"name": {
"type": "string"
},
"permalink": {
"type": "string"
},
"permalink_public": {
"type": "string"
},
"pretty_type": {
"type": "string"
},
"public_url_shared": {
"type": "boolean"
},
"size": {
"type": "integer"
},
"timestamp": {
"type": "integer"
},
"title": {
"type": "string"
},
"url_private": {
"type": "string"
},
"url_private_download": {
"type": "string"
},
"user": {
"type": "string"
},
"user_team": {
"type": "string"
},
"username": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,15 @@
{
"type": "object",
"properties": {
"channel": {
"type": "string"
},
"message_timestamp": {
"type": "string"
},
"ok": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,15 @@
{
"type": "object",
"properties": {
"channel": {
"type": "string"
},
"ok": {
"type": "boolean"
},
"permalink": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,128 @@
{
"type": "object",
"properties": {
"channel": {
"type": "string"
},
"message": {
"type": "object",
"properties": {
"app_id": {
"type": "string"
},
"blocks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"block_id": {
"type": "string"
},
"elements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"elements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"style": {
"type": "object",
"properties": {
"italic": {
"type": "boolean"
}
}
},
"text": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
},
"type": {
"type": "string"
}
}
}
},
"type": {
"type": "string"
}
}
}
},
"bot_id": {
"type": "string"
},
"bot_profile": {
"type": "object",
"properties": {
"app_id": {
"type": "string"
},
"deleted": {
"type": "boolean"
},
"icons": {
"type": "object",
"properties": {
"image_36": {
"type": "string"
},
"image_48": {
"type": "string"
},
"image_72": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"team_id": {
"type": "string"
},
"updated": {
"type": "integer"
}
}
},
"team": {
"type": "string"
},
"text": {
"type": "string"
},
"ts": {
"type": "string"
},
"type": {
"type": "string"
},
"user": {
"type": "string"
}
}
},
"message_timestamp": {
"type": "string"
},
"ok": {
"type": "boolean"
}
},
"version": 3
}
@@ -0,0 +1,110 @@
{
"type": "object",
"properties": {
"blocks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"block_id": {
"type": "string"
},
"elements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"elements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"type": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
}
},
"type": {
"type": "string"
}
}
}
},
"type": {
"type": "string"
}
}
}
},
"channel": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"is_channel": {
"type": "boolean"
},
"is_ext_shared": {
"type": "boolean"
},
"is_group": {
"type": "boolean"
},
"is_im": {
"type": "boolean"
},
"is_mpim": {
"type": "boolean"
},
"is_org_shared": {
"type": "boolean"
},
"is_pending_ext_shared": {
"type": "boolean"
},
"is_private": {
"type": "boolean"
},
"is_shared": {
"type": "boolean"
},
"name": {
"type": "string"
}
}
},
"iid": {
"type": "string"
},
"no_reactions": {
"type": "boolean"
},
"permalink": {
"type": "string"
},
"team": {
"type": "string"
},
"text": {
"type": "string"
},
"ts": {
"type": "string"
},
"type": {
"type": "string"
},
"username": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,13 @@
{
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"approved": {
"type": "boolean"
}
}
}
}
}
@@ -0,0 +1,156 @@
{
"type": "object",
"properties": {
"channel": {
"type": "string"
},
"message": {
"type": "object",
"properties": {
"app_id": {
"type": "string"
},
"blocks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"block_id": {
"type": "string"
},
"elements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"elements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"style": {
"type": "object",
"properties": {
"bold": {
"type": "boolean"
},
"italic": {
"type": "boolean"
}
}
},
"text": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
},
"type": {
"type": "string"
}
}
}
},
"text": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"type": {
"type": "string"
},
"verbatim": {
"type": "boolean"
}
}
},
"type": {
"type": "string"
}
}
}
},
"bot_id": {
"type": "string"
},
"bot_profile": {
"type": "object",
"properties": {
"app_id": {
"type": "string"
},
"deleted": {
"type": "boolean"
},
"icons": {
"type": "object",
"properties": {
"image_36": {
"type": "string"
},
"image_48": {
"type": "string"
},
"image_72": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"team_id": {
"type": "string"
},
"updated": {
"type": "integer"
}
}
},
"edited": {
"type": "object",
"properties": {
"ts": {
"type": "string"
},
"user": {
"type": "string"
}
}
},
"team": {
"type": "string"
},
"text": {
"type": "string"
},
"type": {
"type": "string"
},
"user": {
"type": "string"
}
}
},
"message_timestamp": {
"type": "string"
},
"ok": {
"type": "boolean"
},
"text": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"ok": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,103 @@
{
"type": "object",
"properties": {
"channel": {
"type": "string"
},
"message": {
"type": "object",
"properties": {
"blocks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"block_id": {
"type": "string"
},
"elements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"elements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
},
"type": {
"type": "string"
}
}
}
},
"type": {
"type": "string"
}
}
}
},
"client_msg_id": {
"type": "string"
},
"permalink": {
"type": "string"
},
"reactions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"count": {
"type": "integer"
},
"name": {
"type": "string"
},
"users": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"team": {
"type": "string"
},
"text": {
"type": "string"
},
"ts": {
"type": "string"
},
"type": {
"type": "string"
},
"user": {
"type": "string"
}
}
},
"ok": {
"type": "boolean"
},
"type": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"ok": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,166 @@
{
"type": "object",
"properties": {
"color": {
"type": "string"
},
"deleted": {
"type": "boolean"
},
"id": {
"type": "string"
},
"is_admin": {
"type": "boolean"
},
"is_app_user": {
"type": "boolean"
},
"is_bot": {
"type": "boolean"
},
"is_email_confirmed": {
"type": "boolean"
},
"is_owner": {
"type": "boolean"
},
"is_primary_owner": {
"type": "boolean"
},
"is_restricted": {
"type": "boolean"
},
"is_ultra_restricted": {
"type": "boolean"
},
"name": {
"type": "string"
},
"profile": {
"type": "object",
"properties": {
"always_active": {
"type": "boolean"
},
"avatar_hash": {
"type": "string"
},
"display_name": {
"type": "string"
},
"display_name_normalized": {
"type": "string"
},
"first_name": {
"type": "string"
},
"huddle_state": {
"type": "string"
},
"huddle_state_expiration_ts": {
"type": "integer"
},
"image_1024": {
"type": "string"
},
"image_192": {
"type": "string"
},
"image_24": {
"type": "string"
},
"image_32": {
"type": "string"
},
"image_48": {
"type": "string"
},
"image_512": {
"type": "string"
},
"image_72": {
"type": "string"
},
"image_original": {
"type": "string"
},
"is_custom_image": {
"type": "boolean"
},
"last_name": {
"type": "string"
},
"phone": {
"type": "string"
},
"real_name": {
"type": "string"
},
"real_name_normalized": {
"type": "string"
},
"skype": {
"type": "string"
},
"status_emoji": {
"type": "string"
},
"status_emoji_display_info": {
"type": "array",
"items": {
"type": "object",
"properties": {
"display_url": {
"type": "string"
},
"emoji_name": {
"type": "string"
},
"unicode": {
"type": "string"
}
}
}
},
"status_expiration": {
"type": "integer"
},
"status_text": {
"type": "string"
},
"status_text_canonical": {
"type": "string"
},
"team": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"real_name": {
"type": "string"
},
"team_id": {
"type": "string"
},
"tz": {
"type": "string"
},
"tz_label": {
"type": "string"
},
"tz_offset": {
"type": "integer"
},
"updated": {
"type": "integer"
},
"who_can_share_contact_card": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"presence": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,95 @@
{
"type": "object",
"properties": {
"avatar_hash": {
"type": "string"
},
"display_name": {
"type": "string"
},
"display_name_normalized": {
"type": "string"
},
"email": {
"type": "string"
},
"first_name": {
"type": "string"
},
"image_1024": {
"type": "string"
},
"image_192": {
"type": "string"
},
"image_24": {
"type": "string"
},
"image_32": {
"type": "string"
},
"image_48": {
"type": "string"
},
"image_512": {
"type": "string"
},
"image_72": {
"type": "string"
},
"image_original": {
"type": "string"
},
"is_custom_image": {
"type": "boolean"
},
"last_name": {
"type": "string"
},
"phone": {
"type": "string"
},
"real_name": {
"type": "string"
},
"real_name_normalized": {
"type": "string"
},
"skype": {
"type": "string"
},
"status_emoji": {
"type": "string"
},
"status_emoji_display_info": {
"type": "array",
"items": {
"type": "object",
"properties": {
"display_url": {
"type": "string"
},
"emoji_name": {
"type": "string"
},
"unicode": {
"type": "string"
}
}
}
},
"status_expiration": {
"type": "integer"
},
"status_text": {
"type": "string"
},
"status_text_canonical": {
"type": "string"
},
"title": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,163 @@
{
"type": "object",
"properties": {
"color": {
"type": "string"
},
"deleted": {
"type": "boolean"
},
"id": {
"type": "string"
},
"is_admin": {
"type": "boolean"
},
"is_app_user": {
"type": "boolean"
},
"is_bot": {
"type": "boolean"
},
"is_email_confirmed": {
"type": "boolean"
},
"is_owner": {
"type": "boolean"
},
"is_primary_owner": {
"type": "boolean"
},
"is_restricted": {
"type": "boolean"
},
"is_ultra_restricted": {
"type": "boolean"
},
"name": {
"type": "string"
},
"profile": {
"type": "object",
"properties": {
"avatar_hash": {
"type": "string"
},
"display_name": {
"type": "string"
},
"display_name_normalized": {
"type": "string"
},
"first_name": {
"type": "string"
},
"huddle_state": {
"type": "string"
},
"huddle_state_expiration_ts": {
"type": "integer"
},
"image_1024": {
"type": "string"
},
"image_192": {
"type": "string"
},
"image_24": {
"type": "string"
},
"image_32": {
"type": "string"
},
"image_48": {
"type": "string"
},
"image_512": {
"type": "string"
},
"image_72": {
"type": "string"
},
"image_original": {
"type": "string"
},
"is_custom_image": {
"type": "boolean"
},
"last_name": {
"type": "string"
},
"phone": {
"type": "string"
},
"real_name": {
"type": "string"
},
"real_name_normalized": {
"type": "string"
},
"skype": {
"type": "string"
},
"status_emoji": {
"type": "string"
},
"status_emoji_display_info": {
"type": "array",
"items": {
"type": "object",
"properties": {
"display_url": {
"type": "string"
},
"emoji_name": {
"type": "string"
},
"unicode": {
"type": "string"
}
}
}
},
"status_expiration": {
"type": "integer"
},
"status_text": {
"type": "string"
},
"status_text_canonical": {
"type": "string"
},
"team": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"real_name": {
"type": "string"
},
"team_id": {
"type": "string"
},
"tz": {
"type": "string"
},
"tz_label": {
"type": "string"
},
"tz_offset": {
"type": "integer"
},
"updated": {
"type": "integer"
},
"who_can_share_contact_card": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,252 @@
{
"type": "object",
"properties": {
"avatar_hash": {
"type": "string"
},
"display_name": {
"type": "string"
},
"display_name_normalized": {
"type": "string"
},
"email": {
"type": "string"
},
"fields": {
"type": "object",
"properties": {
"Xf03UMDX9738": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"Xf03UMDX98DC": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"Xf03UR35L6MT": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"Xf03UR35L7K7": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"Xf03UTUL86LB": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"Xf03UU2E8W4S": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"Xf03UWFR6VJQ": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"Xf03V6LREDCZ": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"Xf0403NW3YCQ": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"Xf05GWG5K0S0": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"Xf08BT55JWBG": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"Xf08BWUCR1LM": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"Xf08FLSF1456": {
"type": "object",
"properties": {
"alt": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
}
},
"first_name": {
"type": "string"
},
"huddle_state": {
"type": "string"
},
"huddle_state_expiration_ts": {
"type": "integer"
},
"image_1024": {
"type": "string"
},
"image_192": {
"type": "string"
},
"image_24": {
"type": "string"
},
"image_32": {
"type": "string"
},
"image_48": {
"type": "string"
},
"image_512": {
"type": "string"
},
"image_72": {
"type": "string"
},
"image_original": {
"type": "string"
},
"is_custom_image": {
"type": "boolean"
},
"last_name": {
"type": "string"
},
"phone": {
"type": "string"
},
"real_name": {
"type": "string"
},
"real_name_normalized": {
"type": "string"
},
"skype": {
"type": "string"
},
"start_date": {
"type": "string"
},
"status_emoji": {
"type": "string"
},
"status_emoji_display_info": {
"type": "array",
"items": {
"type": "object",
"properties": {
"display_url": {
"type": "string"
},
"emoji_name": {
"type": "string"
},
"unicode": {
"type": "string"
}
}
}
},
"status_expiration": {
"type": "integer"
},
"status_text": {
"type": "string"
},
"status_text_canonical": {
"type": "string"
},
"title": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 150.852 150.852"><use xlink:href="#a" x=".926" y=".926"/><symbol id="a" overflow="visible"><g stroke-width="1.852"><path fill="#e01e5a" stroke="#e01e5a" d="M40.741 93.55c0-8.735 6.607-15.772 14.815-15.772s14.815 7.037 14.815 15.772v38.824c0 8.737-6.607 15.774-14.815 15.774s-14.815-7.037-14.815-15.772z"/><path fill="#ecb22d" stroke="#ecb22d" d="M93.55 107.408c-8.735 0-15.772-6.607-15.772-14.815s7.037-14.815 15.772-14.815h38.826c8.735 0 15.772 6.607 15.772 14.815s-7.037 14.815-15.772 14.815z"/><path fill="#2fb67c" stroke="#2fb67c" d="M77.778 15.772C77.778 7.037 84.385 0 92.593 0s14.815 7.037 14.815 15.772v38.826c0 8.735-6.607 15.772-14.815 15.772s-14.815-7.037-14.815-15.772z"/><path fill="#36c5f1" stroke="#36c5f1" d="M15.772 70.371C7.037 70.371 0 63.763 0 55.556s7.037-14.815 15.772-14.815h38.826c8.735 0 15.772 6.607 15.772 14.815s-7.037 14.815-15.772 14.815z"/><g stroke-linejoin="miter"><path fill="#ecb22d" stroke="#ecb22d" d="M77.778 133.333c0 8.208 6.607 14.815 14.815 14.815s14.815-6.607 14.815-14.815-6.607-14.815-14.815-14.815H77.778z"/><path fill="#2fb67c" stroke="#2fb67c" d="M133.334 70.371h-14.815V55.556c0-8.207 6.607-14.815 14.815-14.815s14.815 6.607 14.815 14.815-6.607 14.815-14.815 14.815z"/><path fill="#e01e5a" stroke="#e01e5a" d="M14.815 77.778H29.63v14.815c0 8.207-6.607 14.815-14.815 14.815S0 100.8 0 92.593s6.607-14.815 14.815-14.815z"/><path fill="#36c5f1" stroke="#36c5f1" d="M70.371 14.815V29.63H55.556c-8.207 0-14.815-6.607-14.815-14.815S47.348 0 55.556 0s14.815 6.607 14.815 14.815z"/></g></g></symbol></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,433 @@
import { mock } from 'jest-mock-extended';
import type { IWebhookFunctions, INodeType } from 'n8n-workflow';
import { SlackTrigger } from '../SlackTrigger.node';
// Mock the helper functions
jest.mock('../SlackTriggerHelpers', () => ({
verifySignature: jest.fn().mockResolvedValue(true),
getChannelInfo: jest.fn().mockResolvedValue({ id: 'C123', name: 'test-channel' }),
getUserInfo: jest.fn().mockResolvedValue({ id: 'U123', name: 'test-user' }),
downloadFile: jest.fn().mockResolvedValue(Buffer.from('test file content')),
}));
describe('SlackTrigger Node', () => {
let slackTrigger: INodeType;
let mockWebhookFunctions: ReturnType<typeof mock<IWebhookFunctions>>;
beforeEach(() => {
jest.clearAllMocks();
slackTrigger = new SlackTrigger();
mockWebhookFunctions = mock<IWebhookFunctions>();
// Mock helpers
mockWebhookFunctions.helpers = {
prepareBinaryData: jest.fn().mockResolvedValue({
data: 'binary-data',
mimeType: 'text/plain',
fileName: 'test.txt',
}),
} as any;
// Default mock setup
mockWebhookFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
switch (paramName) {
case 'trigger':
return ['file_share'];
case 'watchWorkspace':
return false;
case 'channelId':
return 'C123';
case 'downloadFiles':
return false;
case 'options':
return {};
default:
return defaultValue;
}
},
);
mockWebhookFunctions.getResponseObject.mockReturnValue({
status: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
end: jest.fn(),
} as any);
});
describe('webhook method - eventChannel extraction', () => {
it('should extract eventChannel from req.body.event.channel when available', async () => {
const mockRequest = {
body: {
type: 'event_callback',
event: {
type: 'message',
channel: 'C123',
user: 'U456',
text: 'Hello world',
},
},
};
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest as any);
const result = await slackTrigger.webhook!.call(mockWebhookFunctions);
expect(result.workflowData).toBeDefined();
expect(result.workflowData![0][0].json).toEqual(mockRequest.body.event);
});
it('should extract eventChannel from req.body.event.item.channel when event.channel is not available', async () => {
const mockRequest = {
body: {
type: 'event_callback',
event: {
type: 'reaction_added',
user: 'U456',
item: {
channel: 'C123',
ts: '1234567890.123456',
},
reaction: 'thumbsup',
},
},
};
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest as any);
mockWebhookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'trigger':
return ['reaction_added'];
case 'watchWorkspace':
return false;
case 'channelId':
return 'C123';
default:
return {};
}
});
const result = await slackTrigger.webhook!.call(mockWebhookFunctions);
expect(result.workflowData).toBeDefined();
expect(result.workflowData![0][0].json).toEqual(mockRequest.body.event);
});
it('should handle when req.body.event.item is undefined/null without throwing error', async () => {
const mockRequest = {
body: {
type: 'event_callback',
event: {
type: 'reaction_added',
user: 'U456',
item: null, // This could cause the original error
reaction: 'thumbsup',
channel_id: 'C123',
},
},
};
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest as any);
mockWebhookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'trigger':
return ['reaction_added'];
case 'watchWorkspace':
return false;
case 'channelId':
return 'C123';
default:
return {};
}
});
// This should not throw an error
const result = await slackTrigger.webhook!.call(mockWebhookFunctions);
expect(result.workflowData).toBeDefined();
expect(result.workflowData![0][0].json).toEqual(mockRequest.body.event);
});
it('should fallback to req.body.event.channel_id when channel and item.channel are not available', async () => {
const mockRequest = {
body: {
type: 'event_callback',
event: {
type: 'message',
user: 'U456',
text: 'Hello world',
channel_id: 'C123', // Fallback value
},
},
};
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest as any);
const result = await slackTrigger.webhook!.call(mockWebhookFunctions);
expect(result.workflowData).toBeDefined();
expect(result.workflowData![0][0].json).toEqual(mockRequest.body.event);
});
it('should handle file_share event with undefined item gracefully', async () => {
const mockRequest = {
body: {
type: 'event_callback',
event: {
type: 'message',
subtype: 'file_share',
user: 'U456',
channel: 'C123',
files: [
{
id: 'F123',
name: 'test.txt',
url_private_download: 'https://files.slack.com/test.txt',
mimetype: 'text/plain',
},
],
},
},
};
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest as any);
const result = await slackTrigger.webhook!.call(mockWebhookFunctions);
expect(result.workflowData).toBeDefined();
expect(result.workflowData![0][0].json).toEqual(mockRequest.body.event);
});
it('should handle complex event structure without throwing errors', async () => {
const mockRequest = {
body: {
type: 'event_callback',
event: {
type: 'app_mention',
user: 'U456',
text: '<@U123> hello there',
// No channel, item, or channel_id - edge case
},
},
};
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest as any);
mockWebhookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'trigger':
return ['app_mention'];
case 'watchWorkspace':
return true; // Watch whole workspace, so channel check should be skipped
default:
return {};
}
});
const result = await slackTrigger.webhook!.call(mockWebhookFunctions);
expect(result.workflowData).toBeDefined();
expect(result.workflowData![0][0].json).toEqual(mockRequest.body.event);
});
});
describe('webhook method - file_share event handling', () => {
it('should handle file_share event when item is undefined', async () => {
const mockRequest = {
body: {
type: 'event_callback',
event: {
type: 'message',
subtype: 'file_share',
user: 'U456',
channel: 'C123',
files: [
{
id: 'F123',
name: 'test.txt',
url_private_download: 'https://files.slack.com/test.txt',
mimetype: 'text/plain',
},
],
item: undefined, // This was causing the original error
},
},
};
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest as any);
mockWebhookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'trigger':
return ['file_share'];
case 'downloadFiles':
return true;
case 'watchWorkspace':
return false;
case 'channelId':
return 'C123';
default:
return {};
}
});
const result = await slackTrigger.webhook!.call(mockWebhookFunctions);
expect(result.workflowData).toBeDefined();
expect(result.workflowData![0][0].json).toEqual(mockRequest.body.event);
expect(result.workflowData![0][0].binary).toBeDefined();
});
it('should handle file_share event when item is null', async () => {
const mockRequest = {
body: {
type: 'event_callback',
event: {
type: 'message',
subtype: 'file_share',
user: 'U456',
channel: 'C123',
files: [
{
id: 'F123',
name: 'test.txt',
url_private_download: 'https://files.slack.com/test.txt',
mimetype: 'text/plain',
},
],
item: null, // Another potential error case
},
},
};
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest as any);
mockWebhookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'trigger':
return ['file_share'];
case 'downloadFiles':
return false;
case 'watchWorkspace':
return false;
case 'channelId':
return 'C123';
default:
return {};
}
});
const result = await slackTrigger.webhook!.call(mockWebhookFunctions);
expect(result.workflowData).toBeDefined();
expect(result.workflowData![0][0].json).toEqual(mockRequest.body.event);
});
it('should handle file_share event with valid item.channel', async () => {
const mockRequest = {
body: {
type: 'event_callback',
event: {
type: 'message',
subtype: 'file_share',
user: 'U456',
files: [
{
id: 'F123',
name: 'test.txt',
url_private_download: 'https://files.slack.com/test.txt',
mimetype: 'text/plain',
},
],
item: {
channel: 'C123',
ts: '1234567890.123456',
},
},
},
};
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest as any);
mockWebhookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'trigger':
return ['file_share'];
case 'downloadFiles':
return false;
case 'watchWorkspace':
return false;
case 'channelId':
return 'C123';
default:
return {};
}
});
const result = await slackTrigger.webhook!.call(mockWebhookFunctions);
expect(result.workflowData).toBeDefined();
expect(result.workflowData![0][0].json).toEqual(mockRequest.body.event);
});
});
describe('webhook method - other event scenarios', () => {
it('should handle team_join event (no channel extraction needed)', async () => {
const mockRequest = {
body: {
type: 'event_callback',
event: {
type: 'team_join',
user: {
id: 'U789',
name: 'newuser',
real_name: 'New User',
},
},
},
};
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest as any);
let resolveIds = false;
mockWebhookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'trigger':
return ['team_join'];
case 'options':
return resolveIds ? { resolveIds: true } : {};
default:
return {};
}
});
resolveIds = false;
const resultNoResolve = await slackTrigger.webhook!.call(mockWebhookFunctions);
expect(resultNoResolve.workflowData).toBeDefined();
expect(resultNoResolve.workflowData![0][0].json).toEqual(mockRequest.body.event);
resolveIds = true;
const resultResolve = await slackTrigger.webhook!.call(mockWebhookFunctions);
expect(resultResolve.workflowData).toBeDefined();
const out = resultResolve.workflowData![0][0].json as { user_resolved?: string };
expect(out.user_resolved).toBe(mockRequest.body.event.user.name);
});
it('should handle url_verification challenge', async () => {
const mockRequest = {
body: {
type: 'url_verification',
challenge: 'test_challenge_123',
},
};
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest as any);
const result = await slackTrigger.webhook!.call(mockWebhookFunctions);
expect(result.noWebhookResponse).toBe(true);
expect(mockWebhookFunctions.getResponseObject().status).toHaveBeenCalledWith(200);
expect(mockWebhookFunctions.getResponseObject().json).toHaveBeenCalledWith({
challenge: 'test_challenge_123',
});
});
});
});
@@ -0,0 +1,196 @@
import { createHmac, timingSafeEqual } from 'crypto';
import { verifySignature } from '../SlackTriggerHelpers';
jest.mock('crypto', () => ({
...jest.requireActual('crypto'),
createHmac: jest.fn().mockReturnValue({
update: jest.fn().mockReturnThis(),
digest: jest
.fn()
.mockReturnValue('a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503'),
}),
timingSafeEqual: jest.fn(),
}));
describe('SlackTriggerHelpers', () => {
let mockWebhookFunctions: any;
const testSignatureSecret = 'xyzz0WbapA4vBCDEFasx0q6G';
const testTimestamp = '1531420618';
const testBody =
'token=xyzz0WbapA4vBCDEFasx0q6G&team_id=T1DC2JH3J&team_domain=testteamnow&channel_id=G8PSS9T3V&channel_name=foobar&user_id=U2CERLKJA&user_name=roadrunner&command=%2Fwebhook-collect&text=&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRskXaIFfN&trigger_id=398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c';
const testSignature = 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503';
beforeEach(() => {
jest.clearAllMocks();
// Mock Date.now() to return a fixed timestamp
const fixedDate = new Date(parseInt(testTimestamp, 10) * 1000);
jest.spyOn(Date, 'now').mockImplementation(() => fixedDate.getTime());
mockWebhookFunctions = {
getCredentials: jest.fn(),
getRequestObject: jest.fn(),
getNode: jest.fn().mockReturnValue({ name: 'Slack Trigger' }),
};
// Default mock return values
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header) => {
if (header === 'x-slack-signature') return testSignature;
if (header === 'x-slack-request-timestamp') return testTimestamp;
return null;
}),
rawBody: testBody,
});
});
describe('verifySignature', () => {
it('should return true when no credentials are provided', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({});
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
expect(mockWebhookFunctions.getCredentials).toHaveBeenCalledWith('slackApi');
});
it('should return true when no signature secret is provided', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
apiToken: 'test-token',
});
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
expect(mockWebhookFunctions.getCredentials).toHaveBeenCalledWith('slackApi');
});
it('should return false when signature header is missing', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
signatureSecret: testSignatureSecret,
});
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header) => {
if (header === 'x-slack-request-timestamp') return testTimestamp;
return null;
}),
rawBody: testBody,
});
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
});
it('should return false when timestamp header is missing', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
signatureSecret: testSignatureSecret,
});
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header) => {
if (header === 'x-slack-signature') return testSignature;
return null;
}),
rawBody: testBody,
});
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
});
it('should return false when timestamp is too old', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
signatureSecret: testSignatureSecret,
});
// Mock Date.now() to return a timestamp that's more than 5 minutes after the request timestamp
const futureDate = new Date((parseInt(testTimestamp, 10) + 301) * 1000);
jest.spyOn(Date, 'now').mockImplementation(() => futureDate.getTime());
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
});
it('should return true when signature is valid', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
signatureSecret: testSignatureSecret,
});
// Mock the timingSafeEqual to return true for valid signature
(timingSafeEqual as jest.Mock).mockReturnValue(true);
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
expect(createHmac).toHaveBeenCalledWith('sha256', testSignatureSecret);
expect(timingSafeEqual).toHaveBeenCalled();
});
it('should return false when signature is invalid', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
signatureSecret: testSignatureSecret,
});
// Mock the timingSafeEqual to return false for invalid signature
(timingSafeEqual as jest.Mock).mockReturnValue(false);
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
expect(createHmac).toHaveBeenCalledWith('sha256', testSignatureSecret);
expect(timingSafeEqual).toHaveBeenCalled();
});
it('should correctly build the signature string with the provided timestamp', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
signatureSecret: testSignatureSecret,
});
await verifySignature.call(mockWebhookFunctions);
expect(createHmac).toHaveBeenCalledWith('sha256', testSignatureSecret);
const mockHmac = createHmac('sha256', testSignatureSecret);
// Verify that update was called with the expected string (using the timestamp from the request)
expect(mockHmac.update).toHaveBeenCalledWith(`v0:${testTimestamp}:${testBody}`);
});
it('should verify timestamp even if signature secret is not set', async () => {
// No signature secret in credentials
mockWebhookFunctions.getCredentials.mockResolvedValue({
apiToken: 'test-token',
});
// Mock Date.now() to return a timestamp that's more than 5 minutes after the request timestamp
const futureDate = new Date((parseInt(testTimestamp, 10) + 301) * 1000);
jest.spyOn(Date, 'now').mockImplementation(() => futureDate.getTime());
const result = await verifySignature.call(mockWebhookFunctions);
// Should return false because timestamp is too old, even though signature secret is not set
expect(result).toBe(false);
expect(mockWebhookFunctions.getCredentials).toHaveBeenCalledWith('slackApi');
});
it('should return true when timestamp is valid even if signature secret is not set', async () => {
// No signature secret in credentials
mockWebhookFunctions.getCredentials.mockResolvedValue({
apiToken: 'test-token',
});
// Keep Date.now() at the same time as the request timestamp (within 5 minute window)
const fixedDate = new Date(parseInt(testTimestamp, 10) * 1000);
jest.spyOn(Date, 'now').mockImplementation(() => fixedDate.getTime());
const result = await verifySignature.call(mockWebhookFunctions);
// Should return true because timestamp is valid and signature secret is not required
expect(result).toBe(true);
expect(mockWebhookFunctions.getCredentials).toHaveBeenCalledWith('slackApi');
});
});
});
@@ -0,0 +1,161 @@
import type { IExecuteFunctions } from 'n8n-workflow';
import { slackApiRequest, slackApiRequestAllItems, validateJSON } from '../../V1/GenericFunctions';
jest.mock('n8n-workflow', () => ({
...jest.requireActual('n8n-workflow'),
NodeApiError: jest.fn(),
}));
describe('Slack V1 > GenericFunctions', () => {
let mockExecuteFunctions: IExecuteFunctions;
beforeEach(() => {
jest.clearAllMocks();
mockExecuteFunctions = {
helpers: {
requestWithAuthentication: jest.fn(),
},
getNode: jest.fn().mockReturnValue({ type: 'n8n-nodes-base.slack', typeVersion: 1 }),
getNodeParameter: jest.fn().mockReturnValue('accessToken'),
} as unknown as IExecuteFunctions;
});
describe('slackApiRequest', () => {
it('should handle successful response', async () => {
const mockResponse = { ok: true, data: 'testData' };
mockExecuteFunctions.helpers.requestWithAuthentication = jest
.fn()
.mockResolvedValue(mockResponse);
const result = await slackApiRequest.call(mockExecuteFunctions, 'GET', '/test');
expect(result).toEqual(mockResponse);
});
it('should use OAuth2 credentials when authentication is oAuth2', async () => {
mockExecuteFunctions.getNodeParameter = jest.fn().mockReturnValue('oAuth2');
const mockResponse = { ok: true };
mockExecuteFunctions.helpers.requestWithAuthentication = jest
.fn()
.mockResolvedValue(mockResponse);
await slackApiRequest.call(mockExecuteFunctions, 'GET', '/test');
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'slackOAuth2Api',
expect.any(Object),
expect.objectContaining({
oauth2: expect.any(Object),
}),
);
});
});
describe('slackApiRequestAllItems', () => {
it('should use default limit of 100 when no limit is provided', async () => {
const mockResponse = {
ok: true,
channels: [{ id: 'ch1' }],
response_metadata: { next_cursor: undefined },
};
mockExecuteFunctions.helpers.requestWithAuthentication = jest
.fn()
.mockResolvedValue(mockResponse);
await slackApiRequestAllItems.bind(mockExecuteFunctions)(
'channels',
'GET',
'conversations.list',
);
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'slackApi',
expect.objectContaining({
headers: expect.any(Object),
qs: {
limit: 100,
page: 2,
cursor: undefined,
},
json: true,
method: 'GET',
uri: expect.any(String),
}),
expect.any(Object),
);
});
it('should use count instead of limit for files.list endpoint', async () => {
const mockResponse = {
ok: true,
files: [{ id: 'file1' }],
response_metadata: { next_cursor: undefined },
};
mockExecuteFunctions.helpers.requestWithAuthentication = jest
.fn()
.mockResolvedValue(mockResponse);
await slackApiRequestAllItems.call(mockExecuteFunctions, 'files', 'GET', 'files.list');
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'slackApi',
expect.objectContaining({
headers: expect.any(Object),
qs: {
count: 100,
page: 2,
cursor: undefined,
},
json: true,
method: 'GET',
uri: expect.any(String),
}),
expect.any(Object),
);
});
it('should handle pagination with response_metadata next_cursor', async () => {
const responses = [
{
ok: true,
channels: [{ id: '1' }],
response_metadata: { next_cursor: 'cursor1' },
},
{
ok: true,
channels: [{ id: '2' }],
response_metadata: { next_cursor: '' },
},
];
mockExecuteFunctions.helpers.requestWithAuthentication = jest
.fn()
.mockImplementationOnce(() => responses[0])
.mockImplementationOnce(() => responses[1]);
const result = await slackApiRequestAllItems.call(
mockExecuteFunctions,
'channels',
'GET',
'conversations.list',
);
expect(result).toEqual([{ id: '1' }, { id: '2' }]);
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledTimes(2);
});
});
describe('validateJSON', () => {
it('should return undefined for invalid JSON', () => {
const result = validateJSON('{invalid:json}');
expect(result).toBeUndefined();
});
it('should return parsed object for valid JSON', () => {
const result = validateJSON('{"key":"value"}');
expect(result).toEqual({ key: 'value' });
});
});
});
@@ -0,0 +1,53 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
channel: {
id: 'C085WNEHP4Y',
name: 'test-channel',
is_channel: true,
is_group: false,
is_im: false,
is_mpim: false,
is_private: false,
created: 1734325731,
is_archived: false,
is_general: false,
unlinked: 0,
name_normalized: 'test-channel',
is_shared: false,
is_org_shared: false,
is_pending_ext_shared: false,
pending_shared: [],
context_team_id: 'T0364MSFHV2',
creator: 'U0362BXQYJW',
is_ext_shared: false,
shared_team_ids: ['T0364MSFHV2'],
pending_connected_team_ids: [],
is_member: true,
topic: {
value: '',
creator: '',
last_set: 0,
},
purpose: {
value: '',
creator: '',
last_set: 0,
},
},
};
describe('Test SlackV1, channel => create', () => {
const slackNock = nock('https://slack.com')
.post('/api/conversations.create', {
name: 'test-channel',
})
.reply(200, API_RESPONSE);
afterAll(() => slackNock.done());
new NodeTestHarness().setupTests({
workflowFiles: ['create.workflow.json'],
});
});
@@ -0,0 +1,111 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking 'Test workflow'",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"authentication": "accessToken",
"resource": "channel",
"operation": "create",
"channelId": "test-channel",
"additionalFields": {
"isPrivate": false
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 1,
"position": [1040, 360],
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "C085WNEHP4Y",
"name": "test-channel",
"is_channel": true,
"is_group": false,
"is_im": false,
"is_mpim": false,
"is_private": false,
"created": 1734325731,
"is_archived": false,
"is_general": false,
"unlinked": 0,
"name_normalized": "test-channel",
"is_shared": false,
"is_org_shared": false,
"is_pending_ext_shared": false,
"pending_shared": [],
"context_team_id": "T0364MSFHV2",
"creator": "U0362BXQYJW",
"is_ext_shared": false,
"shared_team_ids": ["T0364MSFHV2"],
"pending_connected_team_ids": [],
"is_member": true,
"topic": {
"value": "",
"creator": "",
"last_set": 0
},
"purpose": {
"value": "",
"creator": "",
"last_set": 0
}
}
}
]
},
"connections": {
"When clicking 'Test workflow'": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,50 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
channel: 'C08514ZPKB8',
message: {
user: 'U0362BXQYJW',
type: 'message',
ts: '1734322671.726339',
bot_id: 'B0382SHFM46',
app_id: 'A037UTP0Z39',
text: 'test message',
team: 'T0364MSFHV2',
bot_profile: {
id: 'B0382SHFM46',
app_id: 'A037UTP0Z39',
name: 'blocks-test',
icons: {
image_36: 'https://a.slack-edge.com/80588/img/plugins/app/bot_36.png',
image_48: 'https://a.slack-edge.com/80588/img/plugins/app/bot_48.png',
image_72: 'https://a.slack-edge.com/80588/img/plugins/app/service_72.png',
},
deleted: false,
updated: 1648028754,
team_id: 'T0364MSFHV2',
},
},
message_timestamp: '1734322671.726339',
};
describe('Test SlackV1, message => post', () => {
const slackNock = nock('https://slack.com')
.post('/api/chat.postMessage', {
channel: 'C08514ZPKB8',
text: 'test message',
attachments: [],
icon_emoji: '😁',
link_names: true,
mrkdwn: true,
unfurl_links: true,
unfurl_media: true,
})
.reply(200, API_RESPONSE);
afterAll(() => slackNock.done());
new NodeTestHarness().setupTests({
workflowFiles: ['post.workflow.json'],
});
});
@@ -0,0 +1,110 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking 'Test workflow'",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"authentication": "accessToken",
"resource": "message",
"operation": "post",
"channel": "C08514ZPKB8",
"text": "test message",
"otherOptions": {
"icon_emoji": "😁",
"link_names": true,
"mrkdwn": true,
"unfurl_links": true,
"unfurl_media": true
},
"jsonParameters": false
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 1,
"position": [1040, 360],
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"ok": true,
"channel": "C08514ZPKB8",
"message": {
"user": "U0362BXQYJW",
"type": "message",
"ts": "1734322671.726339",
"bot_id": "B0382SHFM46",
"app_id": "A037UTP0Z39",
"text": "test message",
"team": "T0364MSFHV2",
"bot_profile": {
"id": "B0382SHFM46",
"app_id": "A037UTP0Z39",
"name": "blocks-test",
"icons": {
"image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",
"image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",
"image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"
},
"deleted": false,
"updated": 1648028754,
"team_id": "T0364MSFHV2"
}
},
"message_timestamp": "1734322671.726339"
}
}
]
},
"connections": {
"When clicking 'Test workflow'": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
};
describe('Test SlackV2, channel => append', () => {
nock('https://slack.com').post('/api/conversations.archive').reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['archive.workflow.json'],
});
});
@@ -0,0 +1,89 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"resource": "channel",
"operation": "archive",
"channelId": {
"__rl": true,
"value": "C084ZURRQPL",
"mode": "list",
"cachedResultName": "test-001"
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"ok": true
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "13de7d28-7e93-4602-a4d1-5389e8ccc4dc",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,53 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
channel: {
id: 'C085WNEHP4Y',
name: 'test-003',
is_channel: true,
is_group: false,
is_im: false,
is_mpim: false,
is_private: false,
created: 1734325731,
is_archived: false,
is_general: false,
unlinked: 0,
name_normalized: 'test-003',
is_shared: false,
is_org_shared: false,
is_pending_ext_shared: false,
pending_shared: [],
context_team_id: 'T0364MSFHV2',
updated: 1734325731240,
parent_conversation: null,
creator: 'U0362BXQYJW',
is_ext_shared: false,
shared_team_ids: ['T0364MSFHV2'],
pending_connected_team_ids: [],
is_member: true,
last_read: '0000000000.000000',
topic: {
value: '',
creator: '',
last_set: 0,
},
purpose: {
value: '',
creator: '',
last_set: 0,
},
previous_names: [],
priority: 0,
},
};
describe('Test SlackV2, channel => create', () => {
nock('https://slack.com').post('/api/conversations.create').reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['create.workflow.json'],
});
});
@@ -0,0 +1,119 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"resource": "channel",
"channelId": "test-003"
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "C085WNEHP4Y",
"name": "test-003",
"is_channel": true,
"is_group": false,
"is_im": false,
"is_mpim": false,
"is_private": false,
"created": 1734325731,
"is_archived": false,
"is_general": false,
"unlinked": 0,
"name_normalized": "test-003",
"is_shared": false,
"is_org_shared": false,
"is_pending_ext_shared": false,
"pending_shared": [],
"context_team_id": "T0364MSFHV2",
"updated": 1734325731240,
"parent_conversation": null,
"creator": "U0362BXQYJW",
"is_ext_shared": false,
"shared_team_ids": ["T0364MSFHV2"],
"pending_connected_team_ids": [],
"is_member": true,
"last_read": "0000000000.000000",
"topic": {
"value": "",
"creator": "",
"last_set": 0
},
"purpose": {
"value": "",
"creator": "",
"last_set": 0
},
"previous_names": [],
"priority": 0
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "297d0c4b-32e6-4b08-8818-af5e64762521",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,54 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
channel: {
id: 'C085WNEHP4Y',
name: 'test-003',
is_channel: true,
is_group: false,
is_im: false,
is_mpim: false,
is_private: false,
created: 1734325731,
is_archived: false,
is_general: false,
unlinked: 0,
name_normalized: 'test-003',
is_shared: false,
is_org_shared: false,
is_pending_ext_shared: false,
pending_shared: [],
context_team_id: 'T0364MSFHV2',
updated: 1734325731240,
parent_conversation: null,
creator: 'U0362BXQYJW',
is_ext_shared: false,
shared_team_ids: ['T0364MSFHV2'],
pending_connected_team_ids: [],
is_member: true,
last_read: '0000000000.000000',
topic: {
value: '',
creator: '',
last_set: 0,
},
purpose: {
value: '',
creator: '',
last_set: 0,
},
previous_names: [],
},
};
describe('Test SlackV2, channel => get', () => {
nock('https://slack.com')
.post('/api/conversations.info?channel=C085WNEHP4Y')
.reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['get.workflow.json'],
});
});
@@ -0,0 +1,127 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"resource": "channel",
"operation": "get",
"channelId": {
"__rl": true,
"value": "C085WNEHP4Y",
"mode": "list",
"cachedResultName": "test-003"
},
"options": {
"includeNumMembers": true
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "C085WNEHP4Y",
"name": "test-003",
"is_channel": true,
"is_group": false,
"is_im": false,
"is_mpim": false,
"is_private": false,
"created": 1734325731,
"is_archived": false,
"is_general": false,
"unlinked": 0,
"name_normalized": "test-003",
"is_shared": false,
"is_org_shared": false,
"is_pending_ext_shared": false,
"pending_shared": [],
"context_team_id": "T0364MSFHV2",
"updated": 1734325731240,
"parent_conversation": null,
"creator": "U0362BXQYJW",
"is_ext_shared": false,
"shared_team_ids": ["T0364MSFHV2"],
"pending_connected_team_ids": [],
"is_member": true,
"last_read": "0000000000.000000",
"topic": {
"value": "",
"creator": "",
"last_set": 0
},
"purpose": {
"value": "",
"creator": "",
"last_set": 0
},
"previous_names": []
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "753332eb-25d1-443a-acef-92993e89751a",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,119 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
channels: [
{
id: 'C0362BX17TM',
name: 'general',
is_channel: true,
is_group: false,
is_im: false,
is_mpim: false,
is_private: false,
created: 1646724991,
is_archived: false,
is_general: true,
unlinked: 0,
name_normalized: 'general',
is_shared: false,
is_org_shared: false,
is_pending_ext_shared: false,
pending_shared: [],
context_team_id: 'T0364MSFHV2',
updated: 1734075560630,
parent_conversation: null,
creator: 'U0362BXQYJW',
is_ext_shared: false,
shared_team_ids: ['T0364MSFHV2'],
pending_connected_team_ids: [],
is_member: true,
topic: {
value: '',
creator: '',
last_set: 0,
},
purpose: {
value:
'This is the one channel that will always include everyone. Its a great spot for announcements and team-wide conversations.',
creator: 'U0362BXQYJW',
last_set: 1646724991,
},
properties: {
use_case: 'welcome',
},
previous_names: [],
num_members: 1,
},
{
id: 'C0362BXRZQA',
name: 'random',
is_channel: true,
is_group: false,
is_im: false,
is_mpim: false,
is_private: false,
created: 1646724991,
is_archived: false,
is_general: false,
unlinked: 0,
name_normalized: 'random',
is_shared: false,
is_org_shared: false,
is_pending_ext_shared: false,
pending_shared: [],
context_team_id: 'T0364MSFHV2',
updated: 1725415586388,
parent_conversation: null,
creator: 'U0362BXQYJW',
is_ext_shared: false,
shared_team_ids: ['T0364MSFHV2'],
pending_connected_team_ids: [],
is_member: true,
topic: {
value: '',
creator: '',
last_set: 0,
},
purpose: {
value:
'This channel is for... well, everything else. Its a place for team jokes, spur-of-the-moment ideas, and funny GIFs. Go wild!',
creator: 'U0362BXQYJW',
last_set: 1646724991,
},
properties: {
tabs: [
{
id: 'files',
label: '',
type: 'files',
},
],
tabz: [
{
type: 'files',
},
],
use_case: 'random',
},
previous_names: [],
num_members: 2,
},
],
response_metadata: {
next_cursor: 'dGVhbTpDMDM2MkMyM1RSOA==',
},
};
describe('Test SlackV2, channel => getAll', () => {
nock('https://slack.com')
.get(
'/api/conversations.list?types=public_channel%2Cprivate_channel%2Cim%2Cmpim&exclude_archived=true&limit=2',
)
.reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['getAll.workflow.json'],
});
});
@@ -0,0 +1,181 @@
{
"name": "slack new tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"resource": "channel",
"operation": "getAll",
"limit": 2,
"filters": {
"excludeArchived": true,
"types": ["public_channel", "private_channel", "im", "mpim"]
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "C0362BX17TM",
"name": "general",
"is_channel": true,
"is_group": false,
"is_im": false,
"is_mpim": false,
"is_private": false,
"created": 1646724991,
"is_archived": false,
"is_general": true,
"unlinked": 0,
"name_normalized": "general",
"is_shared": false,
"is_org_shared": false,
"is_pending_ext_shared": false,
"pending_shared": [],
"context_team_id": "T0364MSFHV2",
"updated": 1734075560630,
"parent_conversation": null,
"creator": "U0362BXQYJW",
"is_ext_shared": false,
"shared_team_ids": ["T0364MSFHV2"],
"pending_connected_team_ids": [],
"is_member": true,
"topic": {
"value": "",
"creator": "",
"last_set": 0
},
"purpose": {
"value": "This is the one channel that will always include everyone. Its a great spot for announcements and team-wide conversations.",
"creator": "U0362BXQYJW",
"last_set": 1646724991
},
"properties": {
"use_case": "welcome"
},
"previous_names": [],
"num_members": 1
}
},
{
"json": {
"id": "C0362BXRZQA",
"name": "random",
"is_channel": true,
"is_group": false,
"is_im": false,
"is_mpim": false,
"is_private": false,
"created": 1646724991,
"is_archived": false,
"is_general": false,
"unlinked": 0,
"name_normalized": "random",
"is_shared": false,
"is_org_shared": false,
"is_pending_ext_shared": false,
"pending_shared": [],
"context_team_id": "T0364MSFHV2",
"updated": 1725415586388,
"parent_conversation": null,
"creator": "U0362BXQYJW",
"is_ext_shared": false,
"shared_team_ids": ["T0364MSFHV2"],
"pending_connected_team_ids": [],
"is_member": true,
"topic": {
"value": "",
"creator": "",
"last_set": 0
},
"purpose": {
"value": "This channel is for... well, everything else. Its a place for team jokes, spur-of-the-moment ideas, and funny GIFs. Go wild!",
"creator": "U0362BXQYJW",
"last_set": 1646724991
},
"properties": {
"tabs": [
{
"id": "files",
"label": "",
"type": "files"
}
],
"tabz": [
{
"type": "files"
}
],
"use_case": "random"
},
"previous_names": [],
"num_members": 2
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "f74e9c2c-83f4-4ef8-b436-704e1a27a9ef",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,176 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
messages: [
{
user: 'U0362BXQYJW',
type: 'message',
ts: '1734322597.935429',
bot_id: 'B0382SHFM46',
app_id: 'A037UTP0Z39',
text: 'test message\n_Automated with this <http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd|n8n workflow>_',
team: 'T0364MSFHV2',
bot_profile: {
id: 'B0382SHFM46',
deleted: false,
name: 'blocks-test',
updated: 1648028754,
app_id: 'A037UTP0Z39',
icons: {
image_36: 'https://a.slack-edge.com/80588/img/plugins/app/bot_36.png',
image_48: 'https://a.slack-edge.com/80588/img/plugins/app/bot_48.png',
image_72: 'https://a.slack-edge.com/80588/img/plugins/app/service_72.png',
},
team_id: 'T0364MSFHV2',
},
blocks: [
{
type: 'rich_text',
block_id: 'piR',
elements: [
{
type: 'rich_text_section',
elements: [
{
type: 'text',
text: 'test message\n',
},
{
type: 'text',
text: 'Automated with this ',
style: {
italic: true,
},
},
{
type: 'link',
url: 'http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd',
text: 'n8n workflow',
style: {
italic: true,
},
},
],
},
],
},
],
},
{
user: 'U0362BXQYJW',
type: 'message',
ts: '1734322341.161179',
bot_id: 'B0382SHFM46',
app_id: 'A037UTP0Z39',
text: 'test message\n_Automated with this <http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd|n8n workflow>_',
team: 'T0364MSFHV2',
bot_profile: {
id: 'B0382SHFM46',
app_id: 'A037UTP0Z39',
name: 'blocks-test',
icons: {
image_36: 'https://a.slack-edge.com/80588/img/plugins/app/bot_36.png',
image_48: 'https://a.slack-edge.com/80588/img/plugins/app/bot_48.png',
image_72: 'https://a.slack-edge.com/80588/img/plugins/app/service_72.png',
},
deleted: false,
updated: 1648028754,
team_id: 'T0364MSFHV2',
},
blocks: [
{
type: 'rich_text',
block_id: '2BN',
elements: [
{
type: 'rich_text_section',
elements: [
{
type: 'text',
text: 'test message\n',
},
{
type: 'text',
text: 'Automated with this ',
style: {
italic: true,
},
},
{
type: 'link',
url: 'http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd',
text: 'n8n workflow',
style: {
italic: true,
},
},
],
},
],
},
],
},
{
user: 'U0362BXQYJW',
type: 'message',
ts: '1734321960.507649',
edited: {
user: 'B0382SHFM46',
ts: '1734324905.000000',
},
bot_id: 'B0382SHFM46',
app_id: 'A037UTP0Z39',
text: 'updated message',
team: 'T0364MSFHV2',
bot_profile: {
id: 'B0382SHFM46',
app_id: 'A037UTP0Z39',
name: 'blocks-test',
icons: {
image_36: 'https://a.slack-edge.com/80588/img/plugins/app/bot_36.png',
image_48: 'https://a.slack-edge.com/80588/img/plugins/app/bot_48.png',
image_72: 'https://a.slack-edge.com/80588/img/plugins/app/service_72.png',
},
deleted: false,
updated: 1648028754,
team_id: 'T0364MSFHV2',
},
blocks: [
{
type: 'rich_text',
block_id: 'wR+',
elements: [
{
type: 'rich_text_section',
elements: [
{
type: 'text',
text: 'updated message',
},
],
},
],
},
],
},
{
subtype: 'channel_join',
user: 'U0362BXQYJW',
text: '<@U0362BXQYJW> has joined the channel',
type: 'message',
ts: '1734321942.689159',
},
],
};
describe('Test SlackV2, channel => history', () => {
nock('https://slack.com')
.get('/api/conversations.history?channel=C08514ZPKB8&inclusive=true&page=1&limit=100')
.reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['history.workflow.json'],
});
});
@@ -0,0 +1,254 @@
{
"name": "slack new tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"resource": "channel",
"operation": "history",
"channelId": {
"__rl": true,
"value": "C08514ZPKB8",
"mode": "list",
"cachedResultName": "test-002"
},
"returnAll": true,
"filters": {
"inclusive": true
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"user": "U0362BXQYJW",
"type": "message",
"ts": "1734322597.935429",
"bot_id": "B0382SHFM46",
"app_id": "A037UTP0Z39",
"text": "test message\n_Automated with this <http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd|n8n workflow>_",
"team": "T0364MSFHV2",
"bot_profile": {
"id": "B0382SHFM46",
"deleted": false,
"name": "blocks-test",
"updated": 1648028754,
"app_id": "A037UTP0Z39",
"icons": {
"image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",
"image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",
"image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"
},
"team_id": "T0364MSFHV2"
},
"blocks": [
{
"type": "rich_text",
"block_id": "piR",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "test message\n"
},
{
"type": "text",
"text": "Automated with this ",
"style": {
"italic": true
}
},
{
"type": "link",
"url": "http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd",
"text": "n8n workflow",
"style": {
"italic": true
}
}
]
}
]
}
]
}
},
{
"json": {
"user": "U0362BXQYJW",
"type": "message",
"ts": "1734322341.161179",
"bot_id": "B0382SHFM46",
"app_id": "A037UTP0Z39",
"text": "test message\n_Automated with this <http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd|n8n workflow>_",
"team": "T0364MSFHV2",
"bot_profile": {
"id": "B0382SHFM46",
"app_id": "A037UTP0Z39",
"name": "blocks-test",
"icons": {
"image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",
"image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",
"image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"
},
"deleted": false,
"updated": 1648028754,
"team_id": "T0364MSFHV2"
},
"blocks": [
{
"type": "rich_text",
"block_id": "2BN",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "test message\n"
},
{
"type": "text",
"text": "Automated with this ",
"style": {
"italic": true
}
},
{
"type": "link",
"url": "http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd",
"text": "n8n workflow",
"style": {
"italic": true
}
}
]
}
]
}
]
}
},
{
"json": {
"user": "U0362BXQYJW",
"type": "message",
"ts": "1734321960.507649",
"edited": {
"user": "B0382SHFM46",
"ts": "1734324905.000000"
},
"bot_id": "B0382SHFM46",
"app_id": "A037UTP0Z39",
"text": "updated message",
"team": "T0364MSFHV2",
"bot_profile": {
"id": "B0382SHFM46",
"app_id": "A037UTP0Z39",
"name": "blocks-test",
"icons": {
"image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",
"image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",
"image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"
},
"deleted": false,
"updated": 1648028754,
"team_id": "T0364MSFHV2"
},
"blocks": [
{
"type": "rich_text",
"block_id": "wR+",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "updated message"
}
]
}
]
}
]
}
},
{
"json": {
"subtype": "channel_join",
"user": "U0362BXQYJW",
"text": "<@U0362BXQYJW> has joined the channel",
"type": "message",
"ts": "1734321942.689159"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "6b7102c7-d3c5-4b88-b67d-4ee6ae51b581",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,16 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test SlackV2, file => upload', () => {
nock('https://slack.com')
.get('/api/files.getUploadURLExternal?filename=test%20_name.txt&length=25')
.reply(200, { ok: true, upload_url: 'https://slack.com/api/files.upload' })
.post('/api/files.upload', () => true)
.reply(200, { ok: true, file: { id: 'file_id' } })
.post('/api/files.completeUploadExternal')
.reply(200, { ok: true, files: [{ id: 'file_id' }] });
new NodeTestHarness().setupTests({
workflowFiles: ['upload.workflow.json'],
});
});
@@ -0,0 +1,147 @@
{
"name": "slack new tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [440, 360]
},
{
"parameters": {
"resource": "file",
"options": {
"fileName": "test _name.txt",
"initialComment": "test inline",
"threadTs": "1734322671.726339",
"title": "Test Title"
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1100, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1320, 360]
},
{
"parameters": {
"operation": "toJson",
"options": {}
},
"id": "76e317ed-7f2b-46b9-8c9b-f91a0b5d0fdd",
"name": "Convert to File",
"type": "n8n-nodes-base.convertToFile",
"typeVersion": 1.1,
"position": [860, 360]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "a60db675-c351-49fc-bbad-555a70ee5d6d",
"name": "f1",
"value": "123",
"type": "string"
},
{
"id": "b96e7f11-a0ea-4a7f-b803-5ab3238af054",
"name": "f2",
"value": "456",
"type": "string"
}
]
},
"options": {}
},
"id": "773453a7-2bdc-4d50-a3e6-be5e1b86d820",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [660, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "file_id"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
},
"Convert to File": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "Convert to File",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "3a19732a-4416-487c-9f76-1291c610b5d1",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,16 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
channel: 'C08514ZPKB8',
message_timestamp: '1734322671.726339',
};
describe('Test SlackV2, message => delete', () => {
nock('https://slack.com').post('/api/chat.delete').reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['delete.workflow.json'],
});
});
@@ -0,0 +1,92 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"operation": "delete",
"select": "channel",
"channelId": {
"__rl": true,
"value": "C08514ZPKB8",
"mode": "list",
"cachedResultName": "test-002"
},
"timestamp": 1734322671.726339
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"ok": true,
"channel": "C08514ZPKB8",
"message_timestamp": "1734322671.726339"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "cf10cd01-6650-48ca-b4a2-ec07be9d026c",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,18 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
permalink: 'https://myspace-qhg7381.slack.com/archives/C08514ZPKB8/p1734322671726339',
channel: 'C08514ZPKB8',
};
describe('Test SlackV2, message => getPermalink', () => {
nock('https://slack.com')
.get('/api/chat.getPermalink?channel=C08514ZPKB8&message_ts=1734322671.726339')
.reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['getPermalink.workflow.json'],
});
});
@@ -0,0 +1,91 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"operation": "getPermalink",
"channelId": {
"__rl": true,
"value": "C08514ZPKB8",
"mode": "list",
"cachedResultName": "test-002"
},
"timestamp": 1734322671.726339
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"ok": true,
"permalink": "https://myspace-qhg7381.slack.com/archives/C08514ZPKB8/p1734322671726339",
"channel": "C08514ZPKB8"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "64c251c3-afe5-4636-a3fc-de769f8a9ea0",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,66 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
channel: 'C08514ZPKB8',
message: {
user: 'U0362BXQYJW',
type: 'message',
ts: '1734322671.726339',
bot_id: 'B0382SHFM46',
app_id: 'A037UTP0Z39',
text: 'test message',
team: 'T0364MSFHV2',
bot_profile: {
id: 'B0382SHFM46',
app_id: 'A037UTP0Z39',
name: 'blocks-test',
icons: {
image_36: 'https://a.slack-edge.com/80588/img/plugins/app/bot_36.png',
image_48: 'https://a.slack-edge.com/80588/img/plugins/app/bot_48.png',
image_72: 'https://a.slack-edge.com/80588/img/plugins/app/service_72.png',
},
deleted: false,
updated: 1648028754,
team_id: 'T0364MSFHV2',
},
blocks: [
{
type: 'rich_text',
block_id: 't02Ox',
elements: [
{
type: 'rich_text_section',
elements: [
{
type: 'text',
text: 'test message',
},
],
},
],
},
],
},
message_timestamp: '1734322671.726339',
};
describe('Test SlackV2, message => post', () => {
nock('https://slack.com')
.post('/api/chat.postMessage', {
channel: 'C08514ZPKB8',
icon_emoji: '😁',
includeLinkToWorkflow: false,
link_names: true,
mrkdwn: true,
text: 'test message',
unfurl_links: true,
unfurl_media: true,
})
.reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['post.workflow.json'],
});
});
@@ -0,0 +1,143 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"value": "C08514ZPKB8",
"mode": "list",
"cachedResultName": "test-002"
},
"text": "test message",
"otherOptions": {
"includeLinkToWorkflow": false,
"botProfile": {
"imageValues": {
"profilePhotoType": "emoji",
"icon_emoji": "😁"
}
},
"link_names": true,
"mrkdwn": true,
"unfurl_links": true,
"unfurl_media": true
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"ok": true,
"channel": "C08514ZPKB8",
"message": {
"user": "U0362BXQYJW",
"type": "message",
"ts": "1734322671.726339",
"bot_id": "B0382SHFM46",
"app_id": "A037UTP0Z39",
"text": "test message",
"team": "T0364MSFHV2",
"bot_profile": {
"id": "B0382SHFM46",
"app_id": "A037UTP0Z39",
"name": "blocks-test",
"icons": {
"image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",
"image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",
"image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"
},
"deleted": false,
"updated": 1648028754,
"team_id": "T0364MSFHV2"
},
"blocks": [
{
"type": "rich_text",
"block_id": "t02Ox",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "test message"
}
]
}
]
}
]
},
"message_timestamp": "1734322671.726339"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "2bc19572-58f0-4cb9-91c3-78b4b3e0cd95",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,210 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
query: 'test in:test-002',
messages: {
total: 3,
pagination: {
total_count: 3,
page: 1,
per_page: 2,
page_count: 1,
first: 1,
last: 3,
},
paging: {
count: 2,
total: 3,
page: 1,
pages: 1,
},
matches: [
{
iid: 'a60ff37e-5653-4c53-8a44-783ea4315476',
team: 'T0364MSFHV2',
score: 0,
channel: {
id: 'C08514ZPKB8',
is_channel: true,
is_group: false,
is_im: false,
is_mpim: false,
is_shared: false,
is_org_shared: false,
is_ext_shared: false,
is_private: false,
name: 'test-002',
pending_shared: [],
is_pending_ext_shared: false,
},
type: 'message',
user: 'U0362BXQYJW',
username: 'michael.k',
ts: '1734322597.935429',
blocks: [
{
type: 'rich_text',
block_id: '+bc',
elements: [
{
type: 'rich_text_section',
elements: [
{
type: 'text',
text: 'test message\n',
},
{
type: 'text',
text: 'Automated with this ',
style: {
italic: true,
},
},
{
type: 'link',
url: 'http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd',
text: 'n8n workflow',
style: {
italic: true,
},
},
],
},
],
},
],
text: 'test message\n_Automated with this <http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd|n8n workflow>_',
permalink: 'https://myspace-qhg7381.slack.com/archives/C08514ZPKB8/p1734322597935429',
no_reactions: true,
},
{
iid: '96ee0d2e-1a7d-40bf-b4b2-71ee15f004a8',
team: 'T0364MSFHV2',
score: 0,
channel: {
id: 'C08514ZPKB8',
is_channel: true,
is_group: false,
is_im: false,
is_mpim: false,
is_shared: false,
is_org_shared: false,
is_ext_shared: false,
is_private: false,
name: 'test-002',
pending_shared: [],
is_pending_ext_shared: false,
},
type: 'message',
user: 'U0362BXQYJW',
username: 'michael.k',
ts: '1734322341.161179',
blocks: [
{
type: 'rich_text',
block_id: 'FGAKN',
elements: [
{
type: 'rich_text_section',
elements: [
{
type: 'text',
text: 'test message\n',
},
{
type: 'text',
text: 'Automated with this ',
style: {
italic: true,
},
},
{
type: 'link',
url: 'http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd',
text: 'n8n workflow',
style: {
italic: true,
},
},
],
},
],
},
],
text: 'test message\n_Automated with this <http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd|n8n workflow>_',
permalink: 'https://myspace-qhg7381.slack.com/archives/C08514ZPKB8/p1734322341161179',
no_reactions: true,
},
{
iid: '72f79902-65f7-4bf3-b64b-3fd0b0c3746c',
team: 'T0364MSFHV2',
score: 0,
channel: {
id: 'C08514ZPKB8',
is_channel: true,
is_group: false,
is_im: false,
is_mpim: false,
is_shared: false,
is_org_shared: false,
is_ext_shared: false,
is_private: false,
name: 'test-002',
pending_shared: [],
is_pending_ext_shared: false,
},
type: 'message',
user: 'U0362BXQYJW',
username: 'michael.k',
ts: '1734321960.507649',
blocks: [
{
type: 'rich_text',
block_id: 'fnjm',
elements: [
{
type: 'rich_text_section',
elements: [
{
type: 'text',
text: 'test message\n',
},
{
type: 'text',
text: 'Automated with this ',
style: {
italic: true,
},
},
{
type: 'link',
url: 'http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd',
text: 'n8n workflow',
style: {
italic: true,
},
},
],
},
],
},
],
text: 'test message\n_Automated with this <http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd|n8n workflow>_',
permalink: 'https://myspace-qhg7381.slack.com/archives/C08514ZPKB8/p1734321960507649',
no_reactions: true,
},
],
},
};
describe('Test SlackV2, message => search', () => {
nock('https://slack.com')
.post('/api/search.messages?query=test%20in%3Atest-002&sort=timestamp&sort_dir=desc&count=2')
.reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['search.workflow.json'],
});
});
@@ -0,0 +1,262 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"operation": "search",
"query": "test",
"limit": 2,
"options": {
"searchChannel": ["test-002"]
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"iid": "a60ff37e-5653-4c53-8a44-783ea4315476",
"team": "T0364MSFHV2",
"score": 0,
"channel": {
"id": "C08514ZPKB8",
"is_channel": true,
"is_group": false,
"is_im": false,
"is_mpim": false,
"is_shared": false,
"is_org_shared": false,
"is_ext_shared": false,
"is_private": false,
"name": "test-002",
"pending_shared": [],
"is_pending_ext_shared": false
},
"type": "message",
"user": "U0362BXQYJW",
"username": "michael.k",
"ts": "1734322597.935429",
"blocks": [
{
"type": "rich_text",
"block_id": "+bc",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "test message\n"
},
{
"type": "text",
"text": "Automated with this ",
"style": {
"italic": true
}
},
{
"type": "link",
"url": "http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd",
"text": "n8n workflow",
"style": {
"italic": true
}
}
]
}
]
}
],
"text": "test message\n_Automated with this <http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd|n8n workflow>_",
"permalink": "https://myspace-qhg7381.slack.com/archives/C08514ZPKB8/p1734322597935429",
"no_reactions": true
}
},
{
"json": {
"iid": "96ee0d2e-1a7d-40bf-b4b2-71ee15f004a8",
"team": "T0364MSFHV2",
"score": 0,
"channel": {
"id": "C08514ZPKB8",
"is_channel": true,
"is_group": false,
"is_im": false,
"is_mpim": false,
"is_shared": false,
"is_org_shared": false,
"is_ext_shared": false,
"is_private": false,
"name": "test-002",
"pending_shared": [],
"is_pending_ext_shared": false
},
"type": "message",
"user": "U0362BXQYJW",
"username": "michael.k",
"ts": "1734322341.161179",
"blocks": [
{
"type": "rich_text",
"block_id": "FGAKN",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "test message\n"
},
{
"type": "text",
"text": "Automated with this ",
"style": {
"italic": true
}
},
{
"type": "link",
"url": "http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd",
"text": "n8n workflow",
"style": {
"italic": true
}
}
]
}
]
}
],
"text": "test message\n_Automated with this <http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd|n8n workflow>_",
"permalink": "https://myspace-qhg7381.slack.com/archives/C08514ZPKB8/p1734322341161179",
"no_reactions": true
}
},
{
"json": {
"iid": "72f79902-65f7-4bf3-b64b-3fd0b0c3746c",
"team": "T0364MSFHV2",
"score": 0,
"channel": {
"id": "C08514ZPKB8",
"is_channel": true,
"is_group": false,
"is_im": false,
"is_mpim": false,
"is_shared": false,
"is_org_shared": false,
"is_ext_shared": false,
"is_private": false,
"name": "test-002",
"pending_shared": [],
"is_pending_ext_shared": false
},
"type": "message",
"user": "U0362BXQYJW",
"username": "michael.k",
"ts": "1734321960.507649",
"blocks": [
{
"type": "rich_text",
"block_id": "fnjm",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "test message\n"
},
{
"type": "text",
"text": "Automated with this ",
"style": {
"italic": true
}
},
{
"type": "link",
"url": "http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd",
"text": "n8n workflow",
"style": {
"italic": true
}
}
]
}
]
}
],
"text": "test message\n_Automated with this <http://localhost:5678/workflow/qJdEfiBgYLdfYOTs?utm_source=n8n-internal&amp;utm_medium=powered_by&amp;utm_campaign=n8n-nodes-base.slack_be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd|n8n workflow>_",
"permalink": "https://myspace-qhg7381.slack.com/archives/C08514ZPKB8/p1734321960507649",
"no_reactions": true
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "0ea58133-0522-40ae-ab51-ec3c1eced057",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,67 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
channel: 'C08514ZPKB8',
text: 'updated message',
message: {
user: 'U0362BXQYJW',
type: 'message',
edited: {
user: 'B0382SHFM46',
ts: '1734324905.000000',
},
bot_id: 'B0382SHFM46',
app_id: 'A037UTP0Z39',
text: 'updated message',
team: 'T0364MSFHV2',
bot_profile: {
id: 'B0382SHFM46',
app_id: 'A037UTP0Z39',
name: 'blocks-test',
icons: {
image_36: 'https://a.slack-edge.com/80588/img/plugins/app/bot_36.png',
image_48: 'https://a.slack-edge.com/80588/img/plugins/app/bot_48.png',
image_72: 'https://a.slack-edge.com/80588/img/plugins/app/service_72.png',
},
deleted: false,
updated: 1648028754,
team_id: 'T0364MSFHV2',
},
blocks: [
{
type: 'rich_text',
block_id: 'Akc',
elements: [
{
type: 'rich_text_section',
elements: [
{
type: 'text',
text: 'updated message',
},
],
},
],
},
],
},
message_timestamp: '1734321960.507649',
};
describe('Test SlackV2, message => update', () => {
nock('https://slack.com')
.post('/api/chat.update', {
channel: 'C08514ZPKB8',
link_names: true,
parse: 'none',
text: 'updated message',
ts: '1734321960.507649',
})
.reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['update.workflow.json'],
});
});
@@ -0,0 +1,142 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"operation": "update",
"channelId": {
"__rl": true,
"value": "C08514ZPKB8",
"mode": "list",
"cachedResultName": "test-002"
},
"ts": 1734321960.507649,
"text": "updated message",
"updateFields": {
"link_names": true,
"parse": "none"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"ok": true,
"channel": "C08514ZPKB8",
"text": "updated message",
"message": {
"user": "U0362BXQYJW",
"type": "message",
"edited": {
"user": "B0382SHFM46",
"ts": "1734324905.000000"
},
"bot_id": "B0382SHFM46",
"app_id": "A037UTP0Z39",
"text": "updated message",
"team": "T0364MSFHV2",
"bot_profile": {
"id": "B0382SHFM46",
"app_id": "A037UTP0Z39",
"name": "blocks-test",
"icons": {
"image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",
"image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",
"image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"
},
"deleted": false,
"updated": 1648028754,
"team_id": "T0364MSFHV2"
},
"blocks": [
{
"type": "rich_text",
"block_id": "Akc",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "updated message"
}
]
}
]
}
]
},
"message_timestamp": "1734321960.507649"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "27133240-8ff6-4115-b330-9cbc9aa95b25",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,26 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test SlackV2, user => updateProfile', () => {
nock('https://slack.com')
.post('/api/users.profile.set', {
profile: {
customFieldUi: {
customFieldValues: [{ alt: '', id: 'Xf05SGHVUDKM', value: 'TEST title' }],
},
email: 'some@email.com',
fields: { Xf05SGHVUDKM: { alt: '', value: 'TEST title' } },
first_name: 'first',
last_name: 'last',
status_emoji: '👶',
status_expiration: 1734670800,
status_text: 'test status',
},
user: 'id-new',
})
.reply(200, { profile: { test: 'OK' } });
new NodeTestHarness().setupTests({
workflowFiles: ['updateProfile.workflow.json'],
});
});
@@ -0,0 +1,106 @@
{
"name": "slack new tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"resource": "user",
"operation": "updateProfile",
"options": {
"customFieldUi": {
"customFieldValues": [
{
"id": "Xf05SGHVUDKM",
"value": "TEST title"
}
]
},
"email": "some@email.com",
"first_name": "first",
"last_name": "last",
"status": {
"set_status": [
{
"status_emoji": "👶",
"status_expiration": "2024-12-20T00:00:00",
"status_text": "test status"
}
]
},
"user": "id-new"
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"test": "OK"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "297af4ef-eca5-493d-8a1f-c735fcb805f2",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,35 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
usergroup: {
id: 'S0615G0KT',
team_id: 'T060RNRCH',
is_usergroup: true,
name: 'Marketing Team',
description: 'Marketing gurus, PR experts and product advocates.',
handle: 'marketing-team',
is_external: false,
date_create: 1446746793,
date_update: 1446746793,
date_delete: 0,
auto_type: null,
created_by: 'U060RNRCZ',
updated_by: 'U060RNRCZ',
deleted_by: null,
prefs: {
channels: [],
groups: [],
},
user_count: '0',
},
};
describe('Test SlackV2, userGroup => create', () => {
nock('https://slack.com').post('/api/usergroups.create').reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['create.workflow.json'],
});
});
@@ -0,0 +1,108 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking 'Execute workflow'",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"resource": "userGroup",
"operation": "create",
"name": "Marketing Team",
"Options": {
"channelIds": ["C08514ZPKB8"],
"description": "Marketing department user group",
"handle": "marketing",
"include_count": true
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"auto_type": null,
"created_by": "U060RNRCZ",
"date_create": 1446746793,
"date_delete": 0,
"date_update": 1446746793,
"deleted_by": null,
"description": "Marketing gurus, PR experts and product advocates.",
"handle": "marketing-team",
"id": "S0615G0KT",
"is_external": false,
"is_usergroup": true,
"name": "Marketing Team",
"prefs": {
"channels": [],
"groups": []
},
"team_id": "T060RNRCH",
"updated_by": "U060RNRCZ",
"user_count": "0"
}
}
]
},
"connections": {
"When clicking 'Execute workflow'": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "cf10cd01-6650-48ca-b4a2-ec07be9d026c",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,39 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
usergroup: {
id: 'S07XXXXXX',
team_id: 'T1234567890',
is_usergroup: true,
is_subteam: true,
name: 'Marketing Team',
description: 'Marketing department user group',
handle: 'marketing',
is_external: false,
date_create: 1734322671,
date_update: 1734322671,
date_delete: 1734322800,
auto_type: null,
auto_provision: false,
enterprise_subteam_id: '',
created_by: 'U1234567890',
updated_by: 'U1234567890',
deleted_by: 'U1234567890',
prefs: {
channels: [],
groups: [],
},
users: [],
user_count: 0,
},
};
describe('Test SlackV2, userGroup => disable', () => {
nock('https://slack.com').post('/api/usergroups.disable').reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['disable.workflow.json'],
});
});
@@ -0,0 +1,109 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking 'Execute workflow'",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"resource": "userGroup",
"operation": "disable",
"userGroupId": "S07XXXXXX",
"options": {
"include_count": true
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"auto_provision": false,
"auto_type": null,
"created_by": "U1234567890",
"date_create": 1734322671,
"date_delete": 1734322800,
"date_update": 1734322671,
"deleted_by": "U1234567890",
"description": "Marketing department user group",
"enterprise_subteam_id": "",
"handle": "marketing",
"id": "S07XXXXXX",
"is_external": false,
"is_subteam": true,
"is_usergroup": true,
"name": "Marketing Team",
"prefs": {
"channels": [],
"groups": []
},
"team_id": "T1234567890",
"updated_by": "U1234567890",
"user_count": 0,
"users": []
}
}
]
},
"connections": {
"When clicking 'Execute workflow'": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "cf10cd01-6650-48ca-b4a2-ec07be9d026c",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,39 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
usergroup: {
id: 'S07XXXXXX',
team_id: 'T1234567890',
is_usergroup: true,
is_subteam: true,
name: 'Marketing Team',
description: 'Marketing department user group',
handle: 'marketing',
is_external: false,
date_create: 1734322671,
date_update: 1734322671,
date_delete: 0,
auto_type: null,
auto_provision: false,
enterprise_subteam_id: '',
created_by: 'U1234567890',
updated_by: 'U1234567890',
deleted_by: null,
prefs: {
channels: [],
groups: [],
},
users: [],
user_count: 0,
},
};
describe('Test SlackV2, userGroup => enable', () => {
nock('https://slack.com').post('/api/usergroups.enable').reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['enable.workflow.json'],
});
});
@@ -0,0 +1,109 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking 'Execute workflow'",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"resource": "userGroup",
"operation": "enable",
"userGroupId": "S07XXXXXX",
"option": {
"include_count": true
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"auto_provision": false,
"auto_type": null,
"created_by": "U1234567890",
"date_create": 1734322671,
"date_delete": 0,
"date_update": 1734322671,
"deleted_by": null,
"description": "Marketing department user group",
"enterprise_subteam_id": "",
"handle": "marketing",
"id": "S07XXXXXX",
"is_external": false,
"is_subteam": true,
"is_usergroup": true,
"name": "Marketing Team",
"prefs": {
"channels": [],
"groups": []
},
"team_id": "T1234567890",
"updated_by": "U1234567890",
"user_count": 0,
"users": []
}
}
]
},
"connections": {
"When clicking 'Execute workflow'": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "cf10cd01-6650-48ca-b4a2-ec07be9d026c",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,69 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
usergroups: [
{
id: 'S07XXXXXX',
team_id: 'T1234567890',
is_usergroup: true,
is_subteam: true,
name: 'Marketing Team',
description: 'Marketing department user group',
handle: 'marketing',
is_external: false,
date_create: 1734322671,
date_update: 1734322671,
date_delete: 0,
auto_type: null,
auto_provision: false,
enterprise_subteam_id: '',
created_by: 'U1234567890',
updated_by: 'U1234567890',
deleted_by: null,
prefs: {
channels: [],
groups: [],
},
users: ['U1234567890', 'U0987654321'],
user_count: 2,
},
{
id: 'S08YYYYYY',
team_id: 'T1234567890',
is_usergroup: true,
is_subteam: true,
name: 'Engineering Team',
description: 'Engineering department user group',
handle: 'engineering',
is_external: false,
date_create: 1734322600,
date_update: 1734322600,
date_delete: 0,
auto_type: null,
auto_provision: false,
enterprise_subteam_id: '',
created_by: 'U1234567890',
updated_by: 'U1234567890',
deleted_by: null,
prefs: {
channels: [],
groups: [],
},
users: ['U1111111111', 'U2222222222', 'U3333333333'],
user_count: 3,
},
],
};
describe('Test SlackV2, userGroup => getAll', () => {
nock('https://slack.com')
.get('/api/usergroups.list')
.query({ include_count: 'true', include_disabled: 'true', include_users: 'true' })
.reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['getAll.workflow.json'],
});
});
@@ -0,0 +1,139 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking 'Execute workflow'",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"resource": "userGroup",
"operation": "getAll",
"returnAll": false,
"limit": 100,
"options": {
"include_count": true,
"include_disabled": true,
"include_users": true
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "S07XXXXXX",
"team_id": "T1234567890",
"is_usergroup": true,
"is_subteam": true,
"name": "Marketing Team",
"description": "Marketing department user group",
"handle": "marketing",
"is_external": false,
"date_create": 1734322671,
"date_update": 1734322671,
"date_delete": 0,
"auto_type": null,
"auto_provision": false,
"enterprise_subteam_id": "",
"created_by": "U1234567890",
"updated_by": "U1234567890",
"deleted_by": null,
"prefs": {
"channels": [],
"groups": []
},
"users": ["U1234567890", "U0987654321"],
"user_count": 2
}
},
{
"json": {
"id": "S08YYYYYY",
"team_id": "T1234567890",
"is_usergroup": true,
"is_subteam": true,
"name": "Engineering Team",
"description": "Engineering department user group",
"handle": "engineering",
"is_external": false,
"date_create": 1734322600,
"date_update": 1734322600,
"date_delete": 0,
"auto_type": null,
"auto_provision": false,
"enterprise_subteam_id": "",
"created_by": "U1234567890",
"updated_by": "U1234567890",
"deleted_by": null,
"prefs": {
"channels": [],
"groups": []
},
"users": ["U1111111111", "U2222222222", "U3333333333"],
"user_count": 3
}
}
]
},
"connections": {
"When clicking 'Execute workflow'": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "cf10cd01-6650-48ca-b4a2-ec07be9d026c",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,39 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
const API_RESPONSE = {
ok: true,
usergroup: {
id: 'S07XXXXXX',
team_id: 'T1234567890',
is_usergroup: true,
is_subteam: true,
name: 'Marketing Team Updated',
description: 'Updated marketing department user group',
handle: 'marketing-team',
is_external: false,
date_create: 1734322671,
date_update: 1734322900,
date_delete: 0,
auto_type: null,
auto_provision: false,
enterprise_subteam_id: '',
created_by: 'U1234567890',
updated_by: 'U1234567890',
deleted_by: null,
prefs: {
channels: ['C08514ZPKB8', 'C08514ZPKB9'],
groups: [],
},
users: [],
user_count: 0,
},
};
describe('Test SlackV2, userGroup => update', () => {
nock('https://slack.com').post('/api/usergroups.update').reply(200, API_RESPONSE);
new NodeTestHarness().setupTests({
workflowFiles: ['update.workflow.json'],
});
});
@@ -0,0 +1,113 @@
{
"name": "slack tests",
"nodes": [
{
"parameters": {},
"id": "e679c883-1839-47dc-9511-8f7dc370e6b0",
"name": "When clicking 'Execute workflow'",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"resource": "userGroup",
"operation": "update",
"userGroupId": "S07XXXXXX",
"updateFields": {
"name": "Marketing Team Updated",
"description": "Updated marketing department user group",
"handle": "marketing-team",
"channels": ["C08514ZPKB8", "C08514ZPKB9"],
"include_count": true
}
},
"id": "2e1937a6-4c8f-4cd1-ae42-11b2bd12cc4c",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [1040, 360],
"webhookId": "04c5e584-45f4-48d0-bcd2-0ecacecb0f53",
"credentials": {
"slackApi": {
"id": "Bg0bWXf8apAimCqJ",
"name": "Slack account 2"
}
}
},
{
"parameters": {},
"id": "06652908-6b8e-443a-9508-ab229b011b73",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1260, 360]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"auto_provision": false,
"auto_type": null,
"created_by": "U1234567890",
"date_create": 1734322671,
"date_delete": 0,
"date_update": 1734322900,
"deleted_by": null,
"description": "Updated marketing department user group",
"enterprise_subteam_id": "",
"handle": "marketing-team",
"id": "S07XXXXXX",
"is_external": false,
"is_subteam": true,
"is_usergroup": true,
"name": "Marketing Team Updated",
"prefs": {
"channels": ["C08514ZPKB8", "C08514ZPKB9"],
"groups": []
},
"team_id": "T1234567890",
"updated_by": "U1234567890",
"user_count": 0,
"users": []
}
}
]
},
"connections": {
"When clicking 'Execute workflow'": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "cf10cd01-6650-48ca-b4a2-ec07be9d026c",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "qJdEfiBgYLdfYOTs",
"tags": []
}
@@ -0,0 +1,263 @@
import { type MockProxy, mock } from 'jest-mock-extended';
import type { IExecuteFunctions } from 'n8n-workflow';
import { getTarget, createSendAndWaitMessageBody } from '../../V2/GenericFunctions';
describe('Slack Utility Functions', () => {
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
beforeEach(() => {
mockExecuteFunctions = mock<IExecuteFunctions>();
mockExecuteFunctions.getNode.mockReturnValue({ name: 'Slack', typeVersion: 1 } as any);
mockExecuteFunctions.getSignedResumeUrl.mockReturnValueOnce(
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
);
mockExecuteFunctions.getSignedResumeUrl.mockReturnValueOnce(
'http://localhost/waiting-webhook/nodeID?approved=false&signature=abc',
);
jest.clearAllMocks();
});
describe('getTarget', () => {
it('should return correct target id', () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
if (parameterName === 'user') {
return 'testUser';
}
return 'testChannel';
});
expect(getTarget(mockExecuteFunctions, 0, 'channel')).toEqual('testChannel');
expect(getTarget(mockExecuteFunctions, 0, 'user')).toEqual('testUser');
});
});
describe('createSendAndWaitMessageBody', () => {
it('should create message with single button - pre 2.3 plain_text', () => {
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('channel');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('channelID');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('subject');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
expect(createSendAndWaitMessageBody(mockExecuteFunctions)).toEqual({
blocks: [
{
type: 'divider',
},
{
text: {
emoji: true,
text: 'message',
type: 'plain_text',
},
type: 'section',
},
{
text: {
text: ' ',
type: 'plain_text',
},
type: 'section',
},
{
type: 'divider',
},
{
elements: [
{
style: 'primary',
text: {
emoji: true,
text: 'Approve',
type: 'plain_text',
},
type: 'button',
url: 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
},
],
type: 'actions',
},
],
channel: 'channelID',
});
});
it('should create message with double buttons - pre 2.3 plain_text', () => {
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('channel');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('channelID');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('subject');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({ approvalType: 'double' });
expect(createSendAndWaitMessageBody(mockExecuteFunctions)).toEqual({
blocks: [
{
type: 'divider',
},
{
text: {
emoji: true,
text: 'message',
type: 'plain_text',
},
type: 'section',
},
{
text: {
text: ' ',
type: 'plain_text',
},
type: 'section',
},
{
type: 'divider',
},
{
elements: [
{
style: undefined,
text: {
emoji: true,
text: 'Disapprove',
type: 'plain_text',
},
type: 'button',
url: 'http://localhost/waiting-webhook/nodeID?approved=false&signature=abc',
},
{
style: 'primary',
text: {
emoji: true,
text: 'Approve',
type: 'plain_text',
},
type: 'button',
url: 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
},
],
type: 'actions',
},
],
channel: 'channelID',
});
});
it('should create message with single button - 2.3+ mrkdwn', () => {
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('channel');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('channelID');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('subject');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
mockExecuteFunctions.getNode.mockReturnValue({ name: 'Slack', typeVersion: 2.3 } as any);
expect(createSendAndWaitMessageBody(mockExecuteFunctions)).toEqual({
blocks: [
{
type: 'divider',
},
{
text: {
text: 'message',
type: 'mrkdwn',
},
type: 'section',
},
{
text: {
text: ' ',
type: 'plain_text',
},
type: 'section',
},
{
type: 'divider',
},
{
elements: [
{
style: 'primary',
text: {
emoji: true,
text: 'Approve',
type: 'plain_text',
},
type: 'button',
url: 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
},
],
type: 'actions',
},
],
channel: 'channelID',
});
});
it('should create message with double buttons - 2.3+ mrkdwn', () => {
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('channel');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('channelID');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('subject');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({ approvalType: 'double' });
mockExecuteFunctions.getNode.mockReturnValue({ name: 'Slack', typeVersion: 2.3 } as any);
expect(createSendAndWaitMessageBody(mockExecuteFunctions)).toEqual({
blocks: [
{
type: 'divider',
},
{
text: {
text: 'message',
type: 'mrkdwn',
},
type: 'section',
},
{
text: {
text: ' ',
type: 'plain_text',
},
type: 'section',
},
{
type: 'divider',
},
{
elements: [
{
style: undefined,
text: {
emoji: true,
text: 'Disapprove',
type: 'plain_text',
},
type: 'button',
url: 'http://localhost/waiting-webhook/nodeID?approved=false&signature=abc',
},
{
style: 'primary',
text: {
emoji: true,
text: 'Approve',
type: 'plain_text',
},
type: 'button',
url: 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
},
],
type: 'actions',
},
],
channel: 'channelID',
});
});
});
});