feat(Discord Node): Overhaul (#5351)

Github issue / Community forum post (link here to close automatically):

---------

Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
Co-authored-by: Marcus <marcus@n8n.io>
This commit is contained in:
Michael Kret 2023-11-08 16:11:23 +02:00 committed by GitHub
parent afd637b5ea
commit 6a53c2a375
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 6688 additions and 271 deletions

View file

@ -155,6 +155,7 @@ export const WOOCOMMERCE_TRIGGER_NODE_TYPE = 'n8n-nodes-base.wooCommerceTrigger'
export const XERO_NODE_TYPE = 'n8n-nodes-base.xero';
export const ZENDESK_NODE_TYPE = 'n8n-nodes-base.zendesk';
export const ZENDESK_TRIGGER_NODE_TYPE = 'n8n-nodes-base.zendeskTrigger';
export const DISCORD_NODE_TYPE = 'n8n-nodes-base.discord';
export const EXECUTABLE_TRIGGER_NODE_TYPES = [
START_NODE_TYPE,
@ -576,6 +577,7 @@ export const KEEP_AUTH_IN_NDV_FOR_NODES = [
HTTP_REQUEST_NODE_TYPE,
WEBHOOK_NODE_TYPE,
WAIT_NODE_TYPE,
DISCORD_NODE_TYPE,
];
export const MAIN_AUTH_FIELD_NAME = 'authentication';
export const NODE_RESOURCE_FIELD_NAME = 'resource';

View file

@ -0,0 +1,43 @@
import type {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class DiscordBotApi implements ICredentialType {
name = 'discordBotApi';
displayName = 'Discord Bot API';
documentationUrl = 'discord';
properties: INodeProperties[] = [
{
displayName: 'Bot Token',
name: 'botToken',
type: 'string',
default: '',
required: true,
typeOptions: {
password: true,
},
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=Bot {{$credentials.botToken}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://discord.com/api/v10/',
url: '/users/@me/guilds',
},
};
}

View file

@ -0,0 +1,56 @@
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
export class DiscordOAuth2Api implements ICredentialType {
name = 'discordOAuth2Api';
extends = ['oAuth2Api'];
displayName = 'Discord OAuth2 API';
documentationUrl = 'discord';
properties: INodeProperties[] = [
{
displayName: 'Bot Token',
name: 'botToken',
type: 'string',
default: '',
typeOptions: {
password: true,
},
},
{
displayName: 'Grant Type',
name: 'grantType',
type: 'hidden',
default: 'authorizationCode',
},
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden',
default: 'https://discord.com/api/oauth2/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden',
default: 'https://discord.com/api/oauth2/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
default: 'identify guilds guilds.join bot',
required: true,
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden',
default: 'permissions=1642758929655',
},
];
}

View file

@ -0,0 +1,23 @@
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
export class DiscordWebhookApi implements ICredentialType {
name = 'discordWebhookApi';
displayName = 'Discord Webhook';
documentationUrl = 'discord';
properties: INodeProperties[] = [
{
displayName: 'Webhook URL',
name: 'webhookUri',
type: 'string',
required: true,
default: '',
placeholder: 'https://discord.com/api/webhooks/ID/TOKEN',
typeOptions: {
password: true,
},
},
];
}

View file

@ -1,275 +1,25 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { jsonParse, NodeApiError, NodeOperationError, sleep } from 'n8n-workflow';
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
import { VersionedNodeType } from 'n8n-workflow';
import type { DiscordAttachment, DiscordWebhook } from './Interfaces';
export class Discord implements INodeType {
description: INodeTypeDescription = {
displayName: 'Discord',
name: 'discord',
icon: 'file:discord.svg',
group: ['output'],
version: 1,
description: 'Sends data to Discord',
defaults: {
name: 'Discord',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Webhook URL',
name: 'webhookUri',
type: 'string',
required: true,
default: '',
placeholder: 'https://discord.com/api/webhooks/ID/TOKEN',
},
{
displayName: 'Content',
name: 'text',
type: 'string',
typeOptions: {
maxValue: 2000,
},
default: '',
placeholder: 'Hello World!',
},
{
displayName: 'Additional Fields',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Allowed Mentions',
name: 'allowedMentions',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
default: '',
},
{
displayName: 'Attachments',
name: 'attachments',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
default: '',
},
{
displayName: 'Avatar URL',
name: 'avatarUrl',
type: 'string',
default: '',
},
{
displayName: 'Components',
name: 'components',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
default: '',
},
{
displayName: 'Embeds',
name: 'embeds',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
default: '',
},
{
displayName: 'Flags',
name: 'flags',
type: 'number',
default: '',
},
{
displayName: 'JSON Payload',
name: 'payloadJson',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
default: '',
},
{
displayName: 'Username',
name: 'username',
type: 'string',
default: '',
placeholder: 'User',
},
{
displayName: 'TTS',
name: 'tts',
type: 'boolean',
default: false,
description: 'Whether this message be sent as a Text To Speech message',
},
],
},
],
};
import { DiscordV1 } from './v1/DiscordV1.node';
import { DiscordV2 } from './v2/DiscordV2.node';
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const returnData: INodeExecutionData[] = [];
export class Discord extends VersionedNodeType {
constructor() {
const baseDescription: INodeTypeBaseDescription = {
displayName: 'Discord',
name: 'discord',
icon: 'file:discord.svg',
group: ['output'],
defaultVersion: 2,
description: 'Sends data to Discord',
};
const webhookUri = this.getNodeParameter('webhookUri', 0, '') as string;
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
1: new DiscordV1(baseDescription),
2: new DiscordV2(baseDescription),
};
if (!webhookUri) throw new NodeOperationError(this.getNode(), 'Webhook uri is required.');
const items = this.getInputData();
const length = items.length;
for (let i = 0; i < length; i++) {
const body: DiscordWebhook = {};
const iterationWebhookUri = this.getNodeParameter('webhookUri', i) as string;
body.content = this.getNodeParameter('text', i) as string;
const options = this.getNodeParameter('options', i);
if (!body.content && !options.embeds) {
throw new NodeOperationError(this.getNode(), 'Either content or embeds must be set.', {
itemIndex: i,
});
}
if (options.embeds) {
try {
//@ts-expect-error
body.embeds = JSON.parse(options.embeds);
} catch (e) {
throw new NodeOperationError(this.getNode(), 'Embeds must be valid JSON.', {
itemIndex: i,
});
}
if (!Array.isArray(body.embeds)) {
throw new NodeOperationError(this.getNode(), 'Embeds must be an array of embeds.', {
itemIndex: i,
});
}
}
if (options.username) {
body.username = options.username as string;
}
if (options.components) {
try {
//@ts-expect-error
body.components = JSON.parse(options.components);
} catch (e) {
throw new NodeOperationError(this.getNode(), 'Components must be valid JSON.', {
itemIndex: i,
});
}
}
if (options.allowed_mentions) {
//@ts-expect-error
body.allowed_mentions = jsonParse(options.allowed_mentions);
}
if (options.avatarUrl) {
body.avatar_url = options.avatarUrl as string;
}
if (options.flags) {
body.flags = options.flags as number;
}
if (options.tts) {
body.tts = options.tts as boolean;
}
if (options.payloadJson) {
//@ts-expect-error
body.payload_json = jsonParse(options.payloadJson);
}
if (options.attachments) {
//@ts-expect-error
body.attachments = jsonParse(options.attachments as DiscordAttachment[]);
}
//* Not used props, delete them from the payload as Discord won't need them :^
if (!body.content) delete body.content;
if (!body.username) delete body.username;
if (!body.avatar_url) delete body.avatar_url;
if (!body.embeds) delete body.embeds;
if (!body.allowed_mentions) delete body.allowed_mentions;
if (!body.flags) delete body.flags;
if (!body.components) delete body.components;
if (!body.payload_json) delete body.payload_json;
if (!body.attachments) delete body.attachments;
let requestOptions;
if (!body.payload_json) {
requestOptions = {
resolveWithFullResponse: true,
method: 'POST',
body,
uri: iterationWebhookUri,
headers: {
'content-type': 'application/json; charset=utf-8',
},
json: true,
};
} else {
requestOptions = {
resolveWithFullResponse: true,
method: 'POST',
body,
uri: iterationWebhookUri,
headers: {
'content-type': 'multipart/form-data; charset=utf-8',
},
};
}
let maxTries = 5;
let response;
do {
try {
response = await this.helpers.request(requestOptions);
const resetAfter = response.headers['x-ratelimit-reset-after'] * 1000;
const remainingRatelimit = response.headers['x-ratelimit-remaining'];
// remaining requests 0
// https://discord.com/developers/docs/topics/rate-limits
if (!+remainingRatelimit) {
await sleep(resetAfter ?? 1000);
}
break;
} catch (error) {
// HTTP/1.1 429 TOO MANY REQUESTS
// Await when the current rate limit will reset
// https://discord.com/developers/docs/topics/rate-limits
if (error.statusCode === 429) {
const retryAfter = error.response?.headers['retry-after'] || 1000;
await sleep(+retryAfter);
continue;
}
throw error;
}
} while (--maxTries);
if (maxTries <= 0) {
throw new NodeApiError(this.getNode(), {
error: 'Could not send Webhook message. Max amount of rate-limit retries reached.',
});
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
return [returnData];
super(nodeVersions, baseDescription);
}
}

View file

@ -0,0 +1,66 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'POST') {
return {
id: '1168528323006181417',
type: 0,
last_message_id: null,
flags: 0,
guild_id: '1168516062791340136',
name: 'third',
parent_id: null,
rate_limit_per_user: 0,
topic: null,
position: 3,
permission_overwrites: [],
nsfw: false,
};
}
});
describe('Test DiscordV2, channel => create', () => {
const workflows = ['nodes/Discord/test/v2/node/channel/create.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(1);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/guilds/1168516062791340136/channels',
{ name: 'third', type: '0' },
);
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,106 @@
{
"name": "discord overhaul tests",
"nodes": [
{
"parameters": {},
"id": "254a9d9b-43bf-4f6e-a761-d78146a05838",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"name": "third",
"options": {}
},
"id": "7e638897-0581-42e6-8b89-494908e0ae75",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordBotApi": {
"id": "KaIz8dqE3Vy1E3iL",
"name": "Discord Bot account"
}
}
},
{
"parameters": {},
"id": "10450e91-8642-4b92-af15-9d5ad161b527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "1168528323006181417",
"type": 0,
"last_message_id": null,
"flags": 0,
"guild_id": "1168516062791340136",
"name": "third",
"parent_id": null,
"rate_limit_per_user": 0,
"topic": null,
"position": 3,
"permission_overwrites": [],
"nsfw": false
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "df4fbb47-eb25-4564-b9ad-f16931e35665",
"id": "4DdFKgGmLX07cXvG",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,62 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'DELETE') {
return {
id: '1168528323006181417',
type: 0,
last_message_id: null,
flags: 0,
guild_id: '1168516062791340136',
name: 'third',
parent_id: null,
rate_limit_per_user: 0,
topic: null,
position: 3,
permission_overwrites: [],
nsfw: false,
};
}
});
describe('Test DiscordV2, channel => deleteChannel', () => {
const workflows = ['nodes/Discord/test/v2/node/channel/deleteChannel.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(1);
expect(discordApiRequestSpy).toHaveBeenCalledWith('DELETE', '/channels/1168528323006181417');
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,112 @@
{
"name": "discord overhaul tests",
"nodes": [
{
"parameters": {},
"id": "254a9d9b-43bf-4f6e-a761-d78146a05838",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"operation": "deleteChannel",
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"channelId": {
"__rl": true,
"value": "1168528323006181417",
"mode": "list",
"cachedResultName": "third",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136/1168528323006181417"
}
},
"id": "7e638897-0581-42e6-8b89-494908e0ae75",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordBotApi": {
"id": "KaIz8dqE3Vy1E3iL",
"name": "Discord Bot account"
}
}
},
{
"parameters": {},
"id": "10450e91-8642-4b92-af15-9d5ad161b527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "1168528323006181417",
"type": 0,
"last_message_id": null,
"flags": 0,
"guild_id": "1168516062791340136",
"name": "third",
"parent_id": null,
"rate_limit_per_user": 0,
"topic": null,
"position": 3,
"permission_overwrites": [],
"nsfw": false
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "f0027d0b-87f7-4a39-bc9c-2838078eed60",
"id": "4DdFKgGmLX07cXvG",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,112 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import type { OptionsWithUrl } from 'request-promise-native';
import * as transport from '../../../../v2/transport/helpers';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const requestApiSpy = jest.spyOn(transport, 'requestApi');
requestApiSpy.mockImplementation(
async (options: OptionsWithUrl, credentialType: string, endpoint: string) => {
if (endpoint === '/users/@me/guilds') {
return {
headers: {},
body: [
{
id: '1168516062791340136',
},
],
};
} else {
return {
headers: {},
body: {
id: '1168516240332034067',
type: 0,
last_message_id: null,
flags: 0,
guild_id: '1168516062791340136',
name: 'first',
parent_id: '1168516063340789831',
rate_limit_per_user: 0,
topic: null,
position: 1,
permission_overwrites: [],
nsfw: false,
},
};
}
},
);
describe('Test DiscordV2, channel => get', () => {
const workflows = ['nodes/Discord/test/v2/node/channel/get.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(requestApiSpy).toHaveBeenCalledTimes(3);
expect(requestApiSpy).toHaveBeenCalledWith(
{
body: undefined,
headers: {},
json: true,
method: 'GET',
qs: undefined,
url: 'https://discord.com/api/v10/users/@me/guilds',
},
'discordOAuth2Api',
'/users/@me/guilds',
);
expect(requestApiSpy).toHaveBeenCalledWith(
{
body: undefined,
headers: {},
json: true,
method: 'GET',
qs: undefined,
url: 'https://discord.com/api/v10/channels/1168516240332034067',
},
'discordOAuth2Api',
'/channels/1168516240332034067',
);
expect(requestApiSpy).toHaveBeenCalledWith(
{
body: undefined,
headers: {},
json: true,
method: 'GET',
qs: undefined,
url: 'https://discord.com/api/v10/channels/1168516240332034067',
},
'discordOAuth2Api',
'/channels/1168516240332034067',
);
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,113 @@
{
"name": "discord overhaul copy",
"nodes": [
{
"parameters": {},
"id": "fe1dd916-f466-40c7-9dfa-dfec59219a86",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-560,
780
]
},
{
"parameters": {
"authentication": "oAuth2",
"operation": "get",
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"channelId": {
"__rl": true,
"value": "1168516240332034067",
"mode": "list",
"cachedResultName": "first",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136/1168516240332034067"
}
},
"id": "09cccc50-10d2-49a1-9b9a-9ba1a11a3657",
"name": "OAuth test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-320,
780
],
"credentials": {
"discordOAuth2Api": {
"id": "79",
"name": "Discord account"
}
}
},
{
"parameters": {},
"id": "7f367512-810f-4d5d-9020-0f01a47039f7",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-80,
780
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "1168516240332034067",
"type": 0,
"last_message_id": null,
"flags": 0,
"guild_id": "1168516062791340136",
"name": "first",
"parent_id": "1168516063340789831",
"rate_limit_per_user": 0,
"topic": null,
"position": 1,
"permission_overwrites": [],
"nsfw": false
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "OAuth test",
"type": "main",
"index": 0
}
]
]
},
"OAuth test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "0089557d-57da-45ea-abd1-c7b57691e10a",
"id": "m3OrE6gaFHxa5InI",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,141 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'GET') {
return [
{
id: '1168516063340789831',
type: 4,
flags: 0,
guild_id: '1168516062791340136',
name: 'Text Channels',
parent_id: null,
position: 0,
permission_overwrites: [],
},
{
id: '1168516063340789832',
type: 4,
flags: 0,
guild_id: '1168516062791340136',
name: 'Voice Channels',
parent_id: null,
position: 0,
permission_overwrites: [],
},
{
id: '1168516063340789833',
type: 0,
last_message_id: '1168518371239792720',
flags: 0,
guild_id: '1168516062791340136',
name: 'general',
parent_id: '1168516063340789831',
rate_limit_per_user: 0,
topic: null,
position: 0,
permission_overwrites: [],
nsfw: false,
icon_emoji: {
id: null,
name: '👋',
},
theme_color: null,
},
{
id: '1168516063340789834',
type: 2,
last_message_id: null,
flags: 0,
guild_id: '1168516062791340136',
name: 'General',
parent_id: '1168516063340789832',
rate_limit_per_user: 0,
bitrate: 64000,
user_limit: 0,
rtc_region: null,
position: 0,
permission_overwrites: [],
nsfw: false,
icon_emoji: {
id: null,
name: '🎙️',
},
theme_color: null,
},
{
id: '1168516240332034067',
type: 0,
last_message_id: null,
flags: 0,
guild_id: '1168516062791340136',
name: 'first-channel',
parent_id: '1168516063340789831',
rate_limit_per_user: 30,
topic: 'This is channel topic',
position: 3,
permission_overwrites: [],
nsfw: true,
},
{
id: '1168516269079793766',
type: 0,
last_message_id: null,
flags: 0,
guild_id: '1168516062791340136',
name: 'second',
parent_id: '1168516063340789831',
rate_limit_per_user: 0,
topic: null,
position: 2,
permission_overwrites: [],
nsfw: false,
},
];
}
});
describe('Test DiscordV2, channel => getAll', () => {
const workflows = ['nodes/Discord/test/v2/node/channel/getAll.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(1);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'GET',
'/guilds/1168516062791340136/channels',
);
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,191 @@
{
"name": "discord overhaul tests",
"nodes": [
{
"parameters": {},
"id": "254a9d9b-43bf-4f6e-a761-d78146a05838",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"operation": "getAll",
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"returnAll": true,
"options": {}
},
"id": "7e638897-0581-42e6-8b89-494908e0ae75",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordBotApi": {
"id": "KaIz8dqE3Vy1E3iL",
"name": "Discord Bot account"
}
}
},
{
"parameters": {},
"id": "10450e91-8642-4b92-af15-9d5ad161b527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "1168516063340789831",
"type": 4,
"flags": 0,
"guild_id": "1168516062791340136",
"name": "Text Channels",
"parent_id": null,
"position": 0,
"permission_overwrites": []
}
},
{
"json": {
"id": "1168516063340789832",
"type": 4,
"flags": 0,
"guild_id": "1168516062791340136",
"name": "Voice Channels",
"parent_id": null,
"position": 0,
"permission_overwrites": []
}
},
{
"json": {
"id": "1168516063340789833",
"type": 0,
"last_message_id": "1168518371239792720",
"flags": 0,
"guild_id": "1168516062791340136",
"name": "general",
"parent_id": "1168516063340789831",
"rate_limit_per_user": 0,
"topic": null,
"position": 0,
"permission_overwrites": [],
"nsfw": false,
"icon_emoji": {
"id": null,
"name": "👋"
},
"theme_color": null
}
},
{
"json": {
"id": "1168516063340789834",
"type": 2,
"last_message_id": null,
"flags": 0,
"guild_id": "1168516062791340136",
"name": "General",
"parent_id": "1168516063340789832",
"rate_limit_per_user": 0,
"bitrate": 64000,
"user_limit": 0,
"rtc_region": null,
"position": 0,
"permission_overwrites": [],
"nsfw": false,
"icon_emoji": {
"id": null,
"name": "🎙️"
},
"theme_color": null
}
},
{
"json": {
"id": "1168516240332034067",
"type": 0,
"last_message_id": null,
"flags": 0,
"guild_id": "1168516062791340136",
"name": "first-channel",
"parent_id": "1168516063340789831",
"rate_limit_per_user": 30,
"topic": "This is channel topic",
"position": 3,
"permission_overwrites": [],
"nsfw": true
}
},
{
"json": {
"id": "1168516269079793766",
"type": 0,
"last_message_id": null,
"flags": 0,
"guild_id": "1168516062791340136",
"name": "second",
"parent_id": "1168516063340789831",
"rate_limit_per_user": 0,
"topic": null,
"position": 2,
"permission_overwrites": [],
"nsfw": false
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "905c7383-202e-4391-97a5-e2c579421c17",
"id": "4DdFKgGmLX07cXvG",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,69 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'PATCH') {
return {
id: '1168516240332034067',
type: 0,
last_message_id: null,
flags: 0,
guild_id: '1168516062791340136',
name: 'first-channel',
parent_id: '1168516063340789831',
rate_limit_per_user: 30,
topic: 'This is channel topic',
position: 3,
permission_overwrites: [],
nsfw: true,
};
}
});
describe('Test DiscordV2, channel => update', () => {
const workflows = ['nodes/Discord/test/v2/node/channel/update.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(1);
expect(discordApiRequestSpy).toHaveBeenCalledWith('PATCH', '/channels/1168516240332034067', {
name: 'First Channel',
nsfw: true,
parent_id: '1168516063340789831',
position: 3,
rate_limit_per_user: 30,
topic: 'This is channel topic',
});
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,126 @@
{
"name": "discord overhaul tests",
"nodes": [
{
"parameters": {},
"id": "254a9d9b-43bf-4f6e-a761-d78146a05838",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"operation": "update",
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"channelId": {
"__rl": true,
"value": "1168516240332034067",
"mode": "list",
"cachedResultName": "first",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136/1168516240332034067"
},
"name": "First Channel",
"options": {
"nsfw": true,
"categoryId": {
"__rl": true,
"value": "1168516063340789831",
"mode": "list",
"cachedResultName": "Text Channels",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136/1168516063340789831"
},
"position": 3,
"rate_limit_per_user": 30,
"topic": "This is channel topic"
}
},
"id": "7e638897-0581-42e6-8b89-494908e0ae75",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordBotApi": {
"id": "KaIz8dqE3Vy1E3iL",
"name": "Discord Bot account"
}
}
},
{
"parameters": {},
"id": "10450e91-8642-4b92-af15-9d5ad161b527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "1168516240332034067",
"type": 0,
"last_message_id": null,
"flags": 0,
"guild_id": "1168516062791340136",
"name": "first-channel",
"parent_id": "1168516063340789831",
"rate_limit_per_user": 30,
"topic": "This is channel topic",
"position": 3,
"permission_overwrites": [],
"nsfw": true
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "fc822909-732e-444f-9537-54b7a85a7bd7",
"id": "4DdFKgGmLX07cXvG",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,90 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'GET') {
return [
{
user: {
id: '470936827994570762',
username: 'michael',
avatar: null,
discriminator: '0',
public_flags: 0,
premium_type: 0,
flags: 0,
banner: null,
accent_color: null,
global_name: 'Michael',
avatar_decoration_data: null,
banner_color: null,
},
roles: [],
},
{
user: {
id: '1070667629972430879',
username: 'n8n-node-overhaul',
avatar: null,
discriminator: '1037',
public_flags: 0,
premium_type: 0,
flags: 0,
bot: true,
banner: null,
accent_color: null,
global_name: null,
avatar_decoration_data: null,
banner_color: null,
},
roles: ['1168518368526077992'],
},
];
}
});
describe('Test DiscordV2, member => getAll', () => {
const workflows = ['nodes/Discord/test/v2/node/member/getAll.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(1);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'GET',
'/guilds/1168516062791340136/members',
undefined,
{ limit: 2 },
);
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,134 @@
{
"name": "discord overhaul tests",
"nodes": [
{
"parameters": {},
"id": "254a9d9b-43bf-4f6e-a761-d78146a05838",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"resource": "member",
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"limit": 2,
"options": {
"simplify": true
}
},
"id": "7e638897-0581-42e6-8b89-494908e0ae75",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordBotApi": {
"id": "KaIz8dqE3Vy1E3iL",
"name": "Discord Bot account"
}
}
},
{
"parameters": {},
"id": "10450e91-8642-4b92-af15-9d5ad161b527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"user": {
"id": "470936827994570762",
"username": "michael",
"avatar": null,
"discriminator": "0",
"public_flags": 0,
"premium_type": 0,
"flags": 0,
"banner": null,
"accent_color": null,
"global_name": "Michael",
"avatar_decoration_data": null,
"banner_color": null
},
"roles": []
}
},
{
"json": {
"user": {
"id": "1070667629972430879",
"username": "n8n-node-overhaul",
"avatar": null,
"discriminator": "1037",
"public_flags": 0,
"premium_type": 0,
"flags": 0,
"bot": true,
"banner": null,
"accent_color": null,
"global_name": null,
"avatar_decoration_data": null,
"banner_color": null
},
"roles": [
"1168518368526077992"
]
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "61375773-2f25-4eae-9ef6-e64e69fc9714",
"id": "4DdFKgGmLX07cXvG",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,54 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'PUT') {
return {
success: true,
};
}
});
describe('Test DiscordV2, member => roleAdd', () => {
const workflows = ['nodes/Discord/test/v2/node/member/roleAdd.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(1);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'PUT',
'/guilds/1168516062791340136/members/470936827994570762/roles/1168772374540320890',
);
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,104 @@
{
"name": "discord overhaul tests",
"nodes": [
{
"parameters": {},
"id": "254a9d9b-43bf-4f6e-a761-d78146a05838",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"resource": "member",
"operation": "roleAdd",
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"userId": {
"__rl": true,
"value": "470936827994570762",
"mode": "list",
"cachedResultName": "michael"
},
"role": [
"1168772374540320890"
]
},
"id": "7e638897-0581-42e6-8b89-494908e0ae75",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordBotApi": {
"id": "KaIz8dqE3Vy1E3iL",
"name": "Discord Bot account"
}
}
},
{
"parameters": {},
"id": "10450e91-8642-4b92-af15-9d5ad161b527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"success": true
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "ad26d0d9-faf3-4070-8909-8c2b6f0749f9",
"id": "4DdFKgGmLX07cXvG",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,62 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'DELETE') {
return {
success: true,
};
}
});
describe('Test DiscordV2, member => roleRemove', () => {
const workflows = ['nodes/Discord/test/v2/node/member/roleRemove.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(3);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'DELETE',
'/guilds/1168516062791340136/members/470936827994570762/roles/1168773588963299428',
);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'DELETE',
'/guilds/1168516062791340136/members/470936827994570762/roles/1168773645800308756',
);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'DELETE',
'/guilds/1168516062791340136/members/470936827994570762/roles/1168772374540320890',
);
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,106 @@
{
"name": "discord overhaul tests",
"nodes": [
{
"parameters": {},
"id": "254a9d9b-43bf-4f6e-a761-d78146a05838",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"resource": "member",
"operation": "roleRemove",
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"userId": {
"__rl": true,
"value": "470936827994570762",
"mode": "list",
"cachedResultName": "michael"
},
"role": [
"1168773588963299428",
"1168773645800308756",
"1168772374540320890"
]
},
"id": "7e638897-0581-42e6-8b89-494908e0ae75",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordBotApi": {
"id": "KaIz8dqE3Vy1E3iL",
"name": "Discord Bot account"
}
}
},
{
"parameters": {},
"id": "10450e91-8642-4b92-af15-9d5ad161b527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"success": true
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "e3780e00-5acb-4c2f-8c4f-85a9fb6698c9",
"id": "4DdFKgGmLX07cXvG",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,54 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'DELETE') {
return {
success: true,
};
}
});
describe('Test DiscordV2, message => deleteMessage', () => {
const workflows = ['nodes/Discord/test/v2/node/message/deleteMessage.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(1);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'DELETE',
'/channels/1168516240332034067/messages/1168776343194972210',
);
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,103 @@
{
"name": "discord overhaul tests",
"nodes": [
{
"parameters": {},
"id": "254a9d9b-43bf-4f6e-a761-d78146a05838",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"resource": "message",
"operation": "deleteMessage",
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"channelId": {
"__rl": true,
"value": "1168516240332034067",
"mode": "list",
"cachedResultName": "first-channel",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136/1168516240332034067"
},
"messageId": "1168776343194972210"
},
"id": "7e638897-0581-42e6-8b89-494908e0ae75",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordBotApi": {
"id": "KaIz8dqE3Vy1E3iL",
"name": "Discord Bot account"
}
}
},
{
"parameters": {},
"id": "10450e91-8642-4b92-af15-9d5ad161b527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"success": true
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "15dbf56e-707a-4b5c-814c-ecf78d96d87f",
"id": "4DdFKgGmLX07cXvG",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,73 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'GET') {
return {
id: '1168777380144369718',
channel_id: '1168516240332034067',
author: {
id: '1070667629972430879',
username: 'n8n-node-overhaul',
avatar: null,
discriminator: '1037',
public_flags: 0,
premium_type: 0,
flags: 0,
bot: true,
banner: null,
accent_color: null,
global_name: null,
avatar_decoration_data: null,
banner_color: null,
},
content: 'msg 3',
timestamp: '2023-10-31T05:04:02.260000+00:00',
type: 0,
};
}
});
describe('Test DiscordV2, message => get', () => {
const workflows = ['nodes/Discord/test/v2/node/message/get.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(1);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'GET',
'/channels/1168516240332034067/messages/1168777380144369718',
);
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,125 @@
{
"name": "discord overhaul tests",
"nodes": [
{
"parameters": {},
"id": "254a9d9b-43bf-4f6e-a761-d78146a05838",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"resource": "message",
"operation": "get",
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"channelId": {
"__rl": true,
"value": "1168516240332034067",
"mode": "list",
"cachedResultName": "first-channel",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136/1168516240332034067"
},
"messageId": "1168777380144369718",
"options": {
"simplify": true
}
},
"id": "7e638897-0581-42e6-8b89-494908e0ae75",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordBotApi": {
"id": "KaIz8dqE3Vy1E3iL",
"name": "Discord Bot account"
}
}
},
{
"parameters": {},
"id": "10450e91-8642-4b92-af15-9d5ad161b527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "1168777380144369718",
"channel_id": "1168516240332034067",
"author": {
"id": "1070667629972430879",
"username": "n8n-node-overhaul",
"avatar": null,
"discriminator": "1037",
"public_flags": 0,
"premium_type": 0,
"flags": 0,
"bot": true,
"banner": null,
"accent_color": null,
"global_name": null,
"avatar_decoration_data": null,
"banner_color": null
},
"content": "msg 3",
"timestamp": "2023-10-31T05:04:02.260000+00:00",
"type": 0
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "876e52a8-2fb3-4efc-9ef0-123807be3806",
"id": "4DdFKgGmLX07cXvG",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,98 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'GET') {
return [
{
id: '1168784010269433998',
type: 0,
content: 'msg 4',
channel_id: '1168516240332034067',
author: {
id: '1070667629972430879',
username: 'n8n-node-overhaul',
avatar: null,
discriminator: '1037',
public_flags: 0,
premium_type: 0,
flags: 0,
bot: true,
banner: null,
accent_color: null,
global_name: null,
avatar_decoration_data: null,
banner_color: null,
},
attachments: [],
embeds: [
{
type: 'rich',
title: 'Some Title',
description: 'description',
color: 2112935,
timestamp: '2023-10-30T22:00:00+00:00',
author: {
name: 'Me',
},
},
],
mentions: [],
mention_roles: [],
pinned: false,
mention_everyone: false,
tts: false,
timestamp: '2023-10-31T05:30:23.005000+00:00',
edited_timestamp: null,
flags: 0,
components: [],
},
];
}
});
describe('Test DiscordV2, message => getAll', () => {
const workflows = ['nodes/Discord/test/v2/node/message/getAll.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(1);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'GET',
'/channels/1168516240332034067/messages',
undefined,
{ limit: 1 },
);
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,144 @@
{
"name": "discord overhaul tests",
"nodes": [
{
"parameters": {},
"id": "254a9d9b-43bf-4f6e-a761-d78146a05838",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"resource": "message",
"operation": "getAll",
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"channelId": {
"__rl": true,
"value": "1168516240332034067",
"mode": "list",
"cachedResultName": "first-channel",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136/1168516240332034067"
},
"limit": 1,
"options": {}
},
"id": "7e638897-0581-42e6-8b89-494908e0ae75",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordBotApi": {
"id": "KaIz8dqE3Vy1E3iL",
"name": "Discord Bot account"
}
}
},
{
"parameters": {},
"id": "10450e91-8642-4b92-af15-9d5ad161b527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "1168784010269433998",
"type": 0,
"content": "msg 4",
"channel_id": "1168516240332034067",
"author": {
"id": "1070667629972430879",
"username": "n8n-node-overhaul",
"avatar": null,
"discriminator": "1037",
"public_flags": 0,
"premium_type": 0,
"flags": 0,
"bot": true,
"banner": null,
"accent_color": null,
"global_name": null,
"avatar_decoration_data": null,
"banner_color": null
},
"attachments": [],
"embeds": [
{
"type": "rich",
"title": "Some Title",
"description": "description",
"color": 2112935,
"timestamp": "2023-10-30T22:00:00+00:00",
"author": {
"name": "Me"
}
}
],
"mentions": [],
"mention_roles": [],
"pinned": false,
"mention_everyone": false,
"tts": false,
"timestamp": "2023-10-31T05:30:23.005000+00:00",
"edited_timestamp": null,
"flags": 0,
"components": []
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "fd45b651-cad2-4985-bcee-9c87efeb9af5",
"id": "4DdFKgGmLX07cXvG",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,54 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'PUT') {
return {
success: true,
};
}
});
describe('Test DiscordV2, message => react', () => {
const workflows = ['nodes/Discord/test/v2/node/message/react.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(1);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'PUT',
'/channels/1168516240332034067/messages/1168777380144369718/reactions/%F0%9F%98%80/@me',
);
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,104 @@
{
"name": "discord overhaul tests",
"nodes": [
{
"parameters": {},
"id": "254a9d9b-43bf-4f6e-a761-d78146a05838",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"resource": "message",
"operation": "react",
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"channelId": {
"__rl": true,
"value": "1168516240332034067",
"mode": "list",
"cachedResultName": "first-channel",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136/1168516240332034067"
},
"messageId": "1168777380144369718",
"emoji": "😀"
},
"id": "7e638897-0581-42e6-8b89-494908e0ae75",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordBotApi": {
"id": "KaIz8dqE3Vy1E3iL",
"name": "Discord Bot account"
}
}
},
{
"parameters": {},
"id": "10450e91-8642-4b92-af15-9d5ad161b527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"success": true
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "cf2de495-fe80-4a98-9d06-c251ea1661ad",
"id": "4DdFKgGmLX07cXvG",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,107 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'POST') {
return {
id: '1168784010269433998',
type: 0,
content: 'msg 4',
channel_id: '1168516240332034067',
author: {
id: '1070667629972430879',
username: 'n8n-node-overhaul',
avatar: null,
discriminator: '1037',
public_flags: 0,
premium_type: 0,
flags: 0,
bot: true,
banner: null,
accent_color: null,
global_name: null,
avatar_decoration_data: null,
banner_color: null,
},
attachments: [],
embeds: [
{
type: 'rich',
title: 'Some Title',
description: 'description',
color: 2112935,
timestamp: '2023-10-30T22:00:00+00:00',
author: {
name: 'Me',
},
},
],
mentions: [],
mention_roles: [],
pinned: false,
mention_everyone: false,
tts: false,
timestamp: '2023-10-31T05:30:23.005000+00:00',
edited_timestamp: null,
flags: 0,
components: [],
referenced_message: null,
};
}
});
describe('Test DiscordV2, message => send', () => {
const workflows = ['nodes/Discord/test/v2/node/message/send.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(1);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/channels/1168516240332034067/messages',
{
content: 'msg 4',
embeds: [
{
author: { name: 'Me' },
color: 2112935,
description: 'description',
timestamp: '2023-10-30T22:00:00.000Z',
title: 'Some Title',
},
],
},
);
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,155 @@
{
"name": "discord overhaul tests",
"nodes": [
{
"parameters": {},
"id": "254a9d9b-43bf-4f6e-a761-d78146a05838",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"resource": "message",
"guildId": {
"__rl": true,
"value": "1168516062791340136",
"mode": "list",
"cachedResultName": "TEST server",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136"
},
"channelId": {
"__rl": true,
"value": "1168516240332034067",
"mode": "list",
"cachedResultName": "first-channel",
"cachedResultUrl": "https://discord.com/channels/1168516062791340136/1168516240332034067"
},
"content": "msg 4",
"options": {},
"embeds": {
"values": [
{
"description": "description",
"author": "Me",
"color": "#203DA7",
"timestamp": "2023-10-30T22:00:00.000Z",
"title": "Some Title"
}
]
}
},
"id": "7e638897-0581-42e6-8b89-494908e0ae75",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordBotApi": {
"id": "KaIz8dqE3Vy1E3iL",
"name": "Discord Bot account"
}
}
},
{
"parameters": {},
"id": "10450e91-8642-4b92-af15-9d5ad161b527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "1168784010269433998",
"type": 0,
"content": "msg 4",
"channel_id": "1168516240332034067",
"author": {
"id": "1070667629972430879",
"username": "n8n-node-overhaul",
"avatar": null,
"discriminator": "1037",
"public_flags": 0,
"premium_type": 0,
"flags": 0,
"bot": true,
"banner": null,
"accent_color": null,
"global_name": null,
"avatar_decoration_data": null,
"banner_color": null
},
"attachments": [],
"embeds": [
{
"type": "rich",
"title": "Some Title",
"description": "description",
"color": 2112935,
"timestamp": "2023-10-30T22:00:00+00:00",
"author": {
"name": "Me"
}
}
],
"mentions": [],
"mention_roles": [],
"pinned": false,
"mention_everyone": false,
"tts": false,
"timestamp": "2023-10-31T05:30:23.005000+00:00",
"edited_timestamp": null,
"flags": 0,
"components": [],
"referenced_message": null
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "5b208250-62db-4b00-9e02-53392eb838a9",
"id": "4DdFKgGmLX07cXvG",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,104 @@
import type { INodeTypes } from 'n8n-workflow';
import nock from 'nock';
import * as transport from '../../../../v2/transport/discord.api';
import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string, endpoint) => {
if (method === 'POST') {
return {
id: '1168768986385747999',
type: 0,
content: 'TEST Message',
channel_id: '1074646335082479626',
author: {
id: '1153265494955135077',
username: 'TEST_USER',
avatar: null,
discriminator: '0000',
public_flags: 0,
flags: 0,
bot: true,
global_name: null,
},
attachments: [],
embeds: [
{
type: 'rich',
description: 'some description',
color: 10930459,
timestamp: '2023-10-17T21:00:00+00:00',
author: {
name: 'Michael',
},
},
],
mentions: [],
mention_roles: [],
pinned: false,
mention_everyone: false,
tts: true,
timestamp: '2023-10-31T04:30:41.032000+00:00',
edited_timestamp: null,
flags: 4096,
components: [],
webhook_id: '1153265494955135077',
};
}
});
describe('Test DiscordV2, webhook => sendLegacy', () => {
const workflows = ['nodes/Discord/test/v2/node/webhook/sendLegacy.workflow.json'];
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
const nodeTypes = setup(tests);
const testNode = async (testData: WorkflowTestData, types: INodeTypes) => {
const { result } = await executeWorkflow(testData, types);
const resultNodeData = getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) => {
return expect(resultData).toEqual(testData.output.nodeData[nodeName]);
});
expect(discordApiRequestSpy).toHaveBeenCalledTimes(1);
expect(discordApiRequestSpy).toHaveBeenCalledWith(
'POST',
'',
{
content: 'TEST Message',
embeds: [
{
author: { name: 'Michael' },
color: 10930459,
description: 'some description',
timestamp: '2023-10-17T21:00:00.000Z',
},
],
flags: 4096,
tts: true,
username: 'TEST_USER',
},
{ wait: true },
);
expect(result.finished).toEqual(true);
};
for (const testData of tests) {
test(testData.description, async () => testNode(testData, nodeTypes));
}
});

View file

@ -0,0 +1,141 @@
{
"name": "discord overhaul tests copy",
"nodes": [
{
"parameters": {},
"id": "8fb04834-2c97-4f21-9300-0f38b0e82f08",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-660,
560
]
},
{
"parameters": {
"authentication": "webhook",
"content": "TEST Message",
"options": {
"flags": [
"SUPPRESS_NOTIFICATIONS"
],
"tts": true,
"username": "TEST_USER",
"wait": true
},
"embeds": {
"values": [
{
"description": "some description",
"author": "Michael",
"color": "#A6C91B",
"timestamp": "2023-10-17T21:00:00.000Z"
}
]
}
},
"id": "61f96217-f6b3-4989-be70-77b723e8e169",
"name": "Bot test",
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
-420,
560
],
"credentials": {
"discordWebhookApi": {
"id": "86",
"name": "Discord account 3"
}
}
},
{
"parameters": {},
"id": "c9c936f7-7dee-40d2-bcf6-255cc9d6d5e8",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-200,
560
]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "1168768986385747999",
"type": 0,
"content": "TEST Message",
"channel_id": "1074646335082479626",
"author": {
"id": "1153265494955135077",
"username": "TEST_USER",
"avatar": null,
"discriminator": "0000",
"public_flags": 0,
"flags": 0,
"bot": true,
"global_name": null
},
"attachments": [],
"embeds": [
{
"type": "rich",
"description": "some description",
"color": 10930459,
"timestamp": "2023-10-17T21:00:00+00:00",
"author": {
"name": "Michael"
}
}
],
"mentions": [],
"mention_roles": [],
"pinned": false,
"mention_everyone": false,
"tts": true,
"timestamp": "2023-10-31T04:30:41.032000+00:00",
"edited_timestamp": null,
"flags": 4096,
"components": [],
"webhook_id": "1153265494955135077"
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Bot test",
"type": "main",
"index": 0
}
]
]
},
"Bot test": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "a4deab55-3791-4e57-b879-d804cd839348",
"id": "Hpl0rsKs6xAbHVO4",
"meta": {
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
},
"tags": []
}

View file

@ -0,0 +1,160 @@
import type { IExecuteFunctions, INode } from 'n8n-workflow';
import {
createSimplifyFunction,
prepareOptions,
prepareEmbeds,
checkAccessToGuild,
setupChannelGetter,
} from '../../v2/helpers/utils';
import * as transport from '../../v2//transport/discord.api';
const node: INode = {
id: '1',
name: 'Discord node',
typeVersion: 2,
type: 'n8n-nodes-base.discord',
position: [60, 760],
parameters: {
resource: 'channel',
operation: 'get',
},
};
describe('Test Discord > createSimplifyFunction', () => {
it('should create function', () => {
const result = createSimplifyFunction(['message_reference']);
expect(result).toBeDefined();
expect(typeof result).toBe('function');
});
it('should return object containing only specified fields', () => {
const simplify = createSimplifyFunction(['id', 'name']);
const data = {
id: '123',
name: 'test',
type: 'test',
randomField: 'test',
};
const result = simplify(data);
expect(result).toEqual({
id: '123',
name: 'test',
});
});
});
describe('Test Discord > prepareOptions', () => {
it('should return correct flag value', () => {
const result = prepareOptions({
flags: ['SUPPRESS_EMBEDS', 'SUPPRESS_NOTIFICATIONS'],
});
expect(result.flags).toBe((1 << 2) + (1 << 12));
});
it('should convert message_reference', () => {
const result = prepareOptions(
{
message_reference: '123456',
},
'789000',
);
expect(result.message_reference).toEqual({
message_id: '123456',
guild_id: '789000',
});
});
});
describe('Test Discord > prepareEmbeds', () => {
it('should return return empty object removing empty strings', () => {
const embeds = [
{
test1: 'test',
test2: 'test',
description: 'test',
},
];
const executeFunction = {};
const result = prepareEmbeds.call(executeFunction as unknown as IExecuteFunctions, embeds);
expect(result).toEqual(embeds);
});
});
describe('Test Discord > checkAccessToGuild', () => {
it('should throw error', () => {
const guildId = '123456';
const guilds = [
{
id: '789000',
},
];
expect(() => {
checkAccessToGuild(node, guildId, guilds);
}).toThrow('You do not have access to the guild with the id 123456');
});
it('should pass', () => {
const guildId = '123456';
const guilds = [
{
id: '123456',
},
{
id: '789000',
},
];
expect(() => {
checkAccessToGuild(node, guildId, guilds);
}).not.toThrow();
});
});
describe('Test Discord > setupChannelGetter & checkAccessToChannel', () => {
const discordApiRequestSpy = jest.spyOn(transport, 'discordApiRequest');
discordApiRequestSpy.mockImplementation(async (method: string) => {
if (method === 'GET') {
return {
guild_id: '123456',
};
}
});
afterAll(() => {
jest.restoreAllMocks();
});
it('should setup channel getter and get channel id', async () => {
const fakeExecuteFunction = (auth: string) => {
return {
getNodeParameter: (parameter: string) => {
if (parameter === 'authentication') return auth;
if (parameter === 'channelId') return '42';
},
getNode: () => node,
} as unknown as IExecuteFunctions;
};
const userGuilds = [
{
id: '789000',
},
];
try {
const getChannel = await setupChannelGetter.call(fakeExecuteFunction('oAuth2'), userGuilds);
await getChannel(0);
} catch (error) {
expect(error.message).toBe('You do not have access to the guild with the id 123456');
}
const getChannel = await setupChannelGetter.call(fakeExecuteFunction('botToken'), userGuilds);
const channelId = await getChannel(0);
expect(channelId).toBe('42');
});
});

View file

@ -0,0 +1,291 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import type {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
} from 'n8n-workflow';
import { jsonParse, NodeApiError, NodeOperationError, sleep } from 'n8n-workflow';
import type { DiscordAttachment, DiscordWebhook } from './Interfaces';
import { oldVersionNotice } from '../../../utils/descriptions';
const versionDescription: INodeTypeDescription = {
displayName: 'Discord',
name: 'discord',
icon: 'file:discord.svg',
group: ['output'],
version: 1,
description: 'Sends data to Discord',
defaults: {
name: 'Discord',
},
inputs: ['main'],
outputs: ['main'],
properties: [
oldVersionNotice,
{
displayName: 'Webhook URL',
name: 'webhookUri',
type: 'string',
required: true,
default: '',
placeholder: 'https://discord.com/api/webhooks/ID/TOKEN',
},
{
displayName: 'Content',
name: 'text',
type: 'string',
typeOptions: {
maxValue: 2000,
},
default: '',
placeholder: 'Hello World!',
},
{
displayName: 'Additional Fields',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Allowed Mentions',
name: 'allowedMentions',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
default: '',
},
{
displayName: 'Attachments',
name: 'attachments',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
default: '',
},
{
displayName: 'Avatar URL',
name: 'avatarUrl',
type: 'string',
default: '',
},
{
displayName: 'Components',
name: 'components',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
default: '',
},
{
displayName: 'Embeds',
name: 'embeds',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
default: '',
},
{
displayName: 'Flags',
name: 'flags',
type: 'number',
default: '',
},
{
displayName: 'JSON Payload',
name: 'payloadJson',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
default: '',
},
{
displayName: 'Username',
name: 'username',
type: 'string',
default: '',
placeholder: 'User',
},
{
displayName: 'TTS',
name: 'tts',
type: 'boolean',
default: false,
description: 'Whether this message be sent as a Text To Speech message',
},
],
},
],
};
export class DiscordV1 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...versionDescription,
};
}
async execute(this: IExecuteFunctions) {
const returnData: INodeExecutionData[] = [];
const webhookUri = this.getNodeParameter('webhookUri', 0, '') as string;
if (!webhookUri) throw new NodeOperationError(this.getNode(), 'Webhook uri is required.');
const items = this.getInputData();
const length = items.length;
for (let i = 0; i < length; i++) {
const body: DiscordWebhook = {};
const iterationWebhookUri = this.getNodeParameter('webhookUri', i) as string;
body.content = this.getNodeParameter('text', i) as string;
const options = this.getNodeParameter('options', i);
if (!body.content && !options.embeds) {
throw new NodeOperationError(this.getNode(), 'Either content or embeds must be set.', {
itemIndex: i,
});
}
if (options.embeds) {
try {
//@ts-expect-error
body.embeds = JSON.parse(options.embeds);
if (!Array.isArray(body.embeds)) {
throw new NodeOperationError(this.getNode(), 'Embeds must be an array of embeds.', {
itemIndex: i,
});
}
} catch (e) {
throw new NodeOperationError(this.getNode(), 'Embeds must be valid JSON.', {
itemIndex: i,
});
}
}
if (options.username) {
body.username = options.username as string;
}
if (options.components) {
try {
//@ts-expect-error
body.components = JSON.parse(options.components);
} catch (e) {
throw new NodeOperationError(this.getNode(), 'Components must be valid JSON.', {
itemIndex: i,
});
}
}
if (options.allowed_mentions) {
//@ts-expect-error
body.allowed_mentions = jsonParse(options.allowed_mentions);
}
if (options.avatarUrl) {
body.avatar_url = options.avatarUrl as string;
}
if (options.flags) {
body.flags = options.flags as number;
}
if (options.tts) {
body.tts = options.tts as boolean;
}
if (options.payloadJson) {
//@ts-expect-error
body.payload_json = jsonParse(options.payloadJson);
}
if (options.attachments) {
//@ts-expect-error
body.attachments = jsonParse(options.attachments as DiscordAttachment[]);
}
//* Not used props, delete them from the payload as Discord won't need them :^
if (!body.content) delete body.content;
if (!body.username) delete body.username;
if (!body.avatar_url) delete body.avatar_url;
if (!body.embeds) delete body.embeds;
if (!body.allowed_mentions) delete body.allowed_mentions;
if (!body.flags) delete body.flags;
if (!body.components) delete body.components;
if (!body.payload_json) delete body.payload_json;
if (!body.attachments) delete body.attachments;
let requestOptions;
if (!body.payload_json) {
requestOptions = {
resolveWithFullResponse: true,
method: 'POST',
body,
uri: iterationWebhookUri,
headers: {
'content-type': 'application/json; charset=utf-8',
},
json: true,
};
} else {
requestOptions = {
resolveWithFullResponse: true,
method: 'POST',
body,
uri: iterationWebhookUri,
headers: {
'content-type': 'multipart/form-data; charset=utf-8',
},
};
}
let maxTries = 5;
let response;
do {
try {
response = await this.helpers.request(requestOptions);
const resetAfter = response.headers['x-ratelimit-reset-after'] * 1000;
const remainingRatelimit = response.headers['x-ratelimit-remaining'];
// remaining requests 0
// https://discord.com/developers/docs/topics/rate-limits
if (!+remainingRatelimit) {
await sleep(resetAfter ?? 1000);
}
break;
} catch (error) {
// HTTP/1.1 429 TOO MANY REQUESTS
// Await when the current rate limit will reset
// https://discord.com/developers/docs/topics/rate-limits
if (error.statusCode === 429) {
const retryAfter = error.response?.headers['retry-after'] || 1000;
await sleep(+retryAfter);
continue;
}
throw error;
}
} while (--maxTries);
if (maxTries <= 0) {
throw new NodeApiError(this.getNode(), {
error: 'Could not send Webhook message. Max amount of rate-limit retries reached.',
});
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
return [returnData];
}
}

View file

@ -0,0 +1,35 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import type {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
} from 'n8n-workflow';
import { listSearch, loadOptions } from './methods';
import { router } from './actions/router';
import { versionDescription } from './actions/versionDescription';
export class DiscordV2 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...versionDescription,
};
}
methods = {
listSearch,
loadOptions,
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
return router.call(this);
}
}

View file

@ -0,0 +1,206 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import { parseDiscordError, prepareErrorData } from '../../helpers/utils';
import { discordApiRequest } from '../../transport';
import { categoryRLC } from '../common.description';
const properties: INodeProperties[] = [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
required: true,
description: 'The name of the channel',
placeholder: 'e.g. new-channel',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
default: '0',
required: true,
description: 'The type of channel to create',
options: [
{
name: 'Guild Text',
value: '0',
},
{
name: 'Guild Voice',
value: '2',
},
{
name: 'Guild Category',
value: '4',
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Age-Restricted (NSFW)',
name: 'nsfw',
type: 'boolean',
default: false,
description: 'Whether the content of the channel might be nsfw (not safe for work)',
displayOptions: {
hide: {
'/type': ['4'],
},
},
},
{
displayName: 'Bitrate',
name: 'bitrate',
type: 'number',
default: 8000,
placeholder: 'e.g. 8000',
typeOptions: {
minValue: 8000,
maxValue: 96000,
},
description: 'The bitrate (in bits) of the voice channel',
displayOptions: {
show: {
'/type': ['2'],
},
},
},
{
...categoryRLC,
displayOptions: {
hide: {
'/type': ['4'],
},
},
},
{
displayName: 'Position',
name: 'position',
type: 'number',
default: 1,
},
{
displayName: 'Rate Limit Per User',
name: 'rate_limit_per_user',
type: 'number',
default: 0,
description: 'Amount of seconds a user has to wait before sending another message',
displayOptions: {
hide: {
'/type': ['4'],
},
},
},
{
displayName: 'Topic',
name: 'topic',
type: 'string',
default: '',
typeOptions: {
rows: 2,
},
description: 'The channel topic description (0-1024 characters)',
placeholder: 'e.g. This channel is about…',
displayOptions: {
hide: {
'/type': ['4'],
},
},
},
{
displayName: 'User Limit',
name: 'user_limit',
type: 'number',
default: 0,
typeOptions: {
minValue: 0,
maxValue: 99,
},
placeholder: 'e.g. 20',
description:
'The limit for the number of members that can be in the channel (0 refers to no limit)',
displayOptions: {
show: {
'/type': ['2'],
},
},
},
],
},
];
const displayOptions = {
show: {
resource: ['channel'],
operation: ['create'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
for (let i = 0; i < items.length; i++) {
try {
const name = this.getNodeParameter('name', i) as string;
const type = this.getNodeParameter('type', i) as string;
const options = this.getNodeParameter('options', i);
if (options.categoryId) {
options.parent_id = (options.categoryId as IDataObject).value;
delete options.categoryId;
}
const body: IDataObject = {
name,
type,
...options,
};
const response = await discordApiRequest.call(
this,
'POST',
`/guilds/${guildId}/channels`,
body,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error, i);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, i));
continue;
}
throw err;
}
}
return returnData;
}

View file

@ -0,0 +1,61 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import { parseDiscordError, prepareErrorData, setupChannelGetter } from '../../helpers/utils';
import { discordApiRequest } from '../../transport';
import { channelRLC } from '../common.description';
const properties: INodeProperties[] = [channelRLC];
const displayOptions = {
show: {
resource: ['channel'],
operation: ['deleteChannel'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
userGuilds: IDataObject[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
const getChannelId = await setupChannelGetter.call(this, userGuilds);
for (let i = 0; i < items.length; i++) {
try {
const channelId = await getChannelId(i);
const response = await discordApiRequest.call(this, 'DELETE', `/channels/${channelId}`);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error, i);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, i));
continue;
}
throw err;
}
}
return returnData;
}

View file

@ -0,0 +1,61 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import { parseDiscordError, prepareErrorData, setupChannelGetter } from '../../helpers/utils';
import { discordApiRequest } from '../../transport';
import { channelRLC } from '../common.description';
const properties: INodeProperties[] = [channelRLC];
const displayOptions = {
show: {
resource: ['channel'],
operation: ['get'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
userGuilds: IDataObject[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
const getChannelId = await setupChannelGetter.call(this, userGuilds);
for (let i = 0; i < items.length; i++) {
try {
const channelId = await getChannelId(i);
const response = await discordApiRequest.call(this, 'GET', `/channels/${channelId}`);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error, i);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, i));
continue;
}
throw err;
}
}
return returnData;
}

View file

@ -0,0 +1,96 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import { parseDiscordError, prepareErrorData } from '../../helpers/utils';
import { discordApiRequest } from '../../transport';
import { returnAllOrLimit } from '../../../../../utils/descriptions';
const properties: INodeProperties[] = [
...returnAllOrLimit,
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Filter by Type',
name: 'filter',
type: 'multiOptions',
default: [],
options: [
{
name: 'Guild Text',
value: 0,
},
{
name: 'Guild Voice',
value: 2,
},
{
name: 'Guild Category',
value: 4,
},
],
},
],
},
];
const displayOptions = {
show: {
resource: ['channel'],
operation: ['getAll'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
try {
const returnAll = this.getNodeParameter('returnAll', 0, false);
let response = await discordApiRequest.call(this, 'GET', `/guilds/${guildId}/channels`);
if (!returnAll) {
const limit = this.getNodeParameter('limit', 0);
response = (response as IDataObject[]).slice(0, limit);
}
const options = this.getNodeParameter('options', 0, {});
if (options.filter) {
const filter = options.filter as number[];
response = (response as IDataObject[]).filter((item) => filter.includes(item.type as number));
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: 0 } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, 0));
}
throw err;
}
return returnData;
}

View file

@ -0,0 +1,72 @@
import type { INodeProperties } from 'n8n-workflow';
import * as create from './create.operation';
import * as get from './get.operation';
import * as getAll from './getAll.operation';
import * as update from './update.operation';
import * as deleteChannel from './deleteChannel.operation';
import { guildRLC } from '../common.description';
export { create, get, getAll, update, deleteChannel };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['channel'],
authentication: ['botToken', 'oAuth2'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new channel',
action: 'Create a channel',
},
{
name: 'Delete',
value: 'deleteChannel',
description: 'Delete a channel',
action: 'Delete a channel',
},
{
name: 'Get',
value: 'get',
description: 'Get a channel',
action: 'Get a channel',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve the channels of a server',
action: 'Get many channels',
},
{
name: 'Update',
value: 'update',
description: 'Update a channel',
action: 'Update a channel',
},
],
default: 'create',
},
{
...guildRLC,
displayOptions: {
show: {
resource: ['channel'],
authentication: ['botToken', 'oAuth2'],
},
},
},
...create.description,
...deleteChannel.description,
...get.description,
...getAll.description,
...update.description,
];

View file

@ -0,0 +1,153 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import { parseDiscordError, prepareErrorData, setupChannelGetter } from '../../helpers/utils';
import { discordApiRequest } from '../../transport';
import { categoryRLC, channelRLC } from '../common.description';
const properties: INodeProperties[] = [
channelRLC,
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description:
"The new name of the channel. Fill this field only if you want to change the channel's name.",
placeholder: 'e.g. new-channel-name',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Age-Restricted (NSFW)',
name: 'nsfw',
type: 'boolean',
default: false,
description: 'Whether the content of the channel might be nsfw (not safe for work)',
},
{
displayName: 'Bitrate',
name: 'bitrate',
type: 'number',
default: 8000,
typeOptions: {
minValue: 8000,
maxValue: 96000,
},
description: 'The bitrate (in bits) of the voice channel',
hint: 'Only applicable to voice channels',
},
categoryRLC,
{
displayName: 'Position',
name: 'position',
type: 'number',
default: 1,
},
{
displayName: 'Rate Limit Per User',
name: 'rate_limit_per_user',
type: 'number',
default: 0,
description: 'Amount of seconds a user has to wait before sending another message',
},
{
displayName: 'Topic',
name: 'topic',
type: 'string',
default: '',
typeOptions: {
rows: 2,
},
description: 'The channel topic description (0-1024 characters)',
placeholder: 'e.g. This channel is about…',
},
{
displayName: 'User Limit',
name: 'user_limit',
type: 'number',
default: 0,
typeOptions: {
minValue: 0,
maxValue: 99,
},
placeholder: 'e.g. 20',
hint: 'Only applicable to voice channels',
description:
'The limit for the number of members that can be in the channel (0 refers to no limit)',
},
],
},
];
const displayOptions = {
show: {
resource: ['channel'],
operation: ['update'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
userGuilds: IDataObject[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
const getChannelId = await setupChannelGetter.call(this, userGuilds);
for (let i = 0; i < items.length; i++) {
try {
const channelId = await getChannelId(i);
const name = this.getNodeParameter('name', i) as string;
const options = this.getNodeParameter('options', i);
if (options.categoryId) {
options.parent_id = (options.categoryId as IDataObject).value;
delete options.categoryId;
}
const body: IDataObject = {
name,
...options,
};
const response = await discordApiRequest.call(this, 'PATCH', `/channels/${channelId}`, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error, i);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, i));
continue;
}
throw err;
}
}
return returnData;
}

View file

@ -0,0 +1,466 @@
import type { INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../utils/utilities';
export const guildRLC: INodeProperties = {
displayName: 'Server',
name: 'guildId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
description: 'Select the server (guild) that your bot is connected to',
modes: [
{
displayName: 'By Name',
name: 'list',
type: 'list',
placeholder: 'e.g. my-server',
typeOptions: {
searchListMethod: 'guildSearch',
},
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
placeholder: 'e.g. https://discord.com/channels/[guild-id]',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/discord.com\\/channels\\/([0-9]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/discord.com\\/channels\\/([0-9]+)',
errorMessage: 'Not a valid Discord Server URL',
},
},
],
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 896347036838936576',
validation: [
{
type: 'regex',
properties: {
regex: '[0-9]+',
errorMessage: 'Not a valid Discord Server ID',
},
},
],
},
],
};
export const channelRLC: INodeProperties = {
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
description: 'Select the channel by name, URL, or ID',
modes: [
{
displayName: 'By Name',
name: 'list',
type: 'list',
placeholder: 'e.g. my-channel',
typeOptions: {
searchListMethod: 'channelSearch',
},
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
placeholder: 'e.g. https://discord.com/channels/[guild-id]/[channel-id]',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/discord.com\\/channels\\/[0-9]+\\/([0-9]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/discord.com\\/channels\\/[0-9]+\\/([0-9]+)',
errorMessage: 'Not a valid Discord Channel URL',
},
},
],
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 896347036838936576',
validation: [
{
type: 'regex',
properties: {
regex: '[0-9]+',
errorMessage: 'Not a valid Discord Channel ID',
},
},
],
},
],
};
export const textChannelRLC: INodeProperties = {
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
description: 'Select the channel by name, URL, or ID',
modes: [
{
displayName: 'By Name',
name: 'list',
type: 'list',
placeholder: 'e.g. my-channel',
typeOptions: {
searchListMethod: 'textChannelSearch',
},
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
placeholder: 'e.g. https://discord.com/channels/[guild-id]/[channel-id]',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/discord.com\\/channels\\/[0-9]+\\/([0-9]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/discord.com\\/channels\\/[0-9]+\\/([0-9]+)',
errorMessage: 'Not a valid Discord Channel URL',
},
},
],
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 896347036838936576',
validation: [
{
type: 'regex',
properties: {
regex: '[0-9]+',
errorMessage: 'Not a valid Discord Channel ID',
},
},
],
},
],
};
export const categoryRLC: INodeProperties = {
displayName: 'Parent Category',
name: 'categoryId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
description: 'The parent category where you want the channel to appear',
modes: [
{
displayName: 'By Name',
name: 'list',
type: 'list',
placeholder: 'e.g. my-channel',
typeOptions: {
searchListMethod: 'categorySearch',
},
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
placeholder: 'e.g. https://discord.com/channels/[guild-id]/[channel-id]',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/discord.com\\/channels\\/[0-9]+\\/([0-9]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/discord.com\\/channels\\/[0-9]+\\/([0-9]+)',
errorMessage: 'Not a valid Discord Category URL',
},
},
],
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 896347036838936576',
validation: [
{
type: 'regex',
properties: {
regex: '[0-9]+',
errorMessage: 'Not a valid Discord Category ID',
},
},
],
},
],
};
export const userRLC: INodeProperties = {
displayName: 'User',
name: 'userId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
description: 'Select the user you want to assign a role to',
modes: [
{
displayName: 'By Name',
name: 'list',
type: 'list',
placeholder: 'e.g. DiscordUser',
typeOptions: {
searchListMethod: 'userSearch',
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 786953432728469534',
validation: [
{
type: 'regex',
properties: {
regex: '[0-9]+',
errorMessage: 'Not a valid User ID',
},
},
],
},
],
};
export const roleMultiOptions: INodeProperties = {
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
displayName: 'Role',
name: 'role',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getRoles',
loadOptionsDependsOn: ['userId.value', 'guildId.value', 'operation'],
},
required: true,
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options
description: 'Select the roles you want to add to the user',
default: [],
};
export const maxResultsNumber: INodeProperties = {
displayName: 'Max Results',
name: 'maxResults',
type: 'number',
typeOptions: {
minValue: 1,
},
default: 50,
description: 'Maximum number of results. Too many results may slow down the query.',
};
export const messageIdString: INodeProperties = {
displayName: 'Message ID',
name: 'messageId',
type: 'string',
default: '',
required: true,
description: 'The ID of the message',
placeholder: 'e.g. 1057576506244726804',
};
export const simplifyBoolean: INodeProperties = {
displayName: 'Simplify',
name: 'simplify',
type: 'boolean',
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
};
// embeds -----------------------------------------------------------------------------------------
const embedFields: INodeProperties[] = [
{
displayName: 'Description (Required)',
name: 'description',
type: 'string',
default: '',
description: 'The description of embed',
placeholder: 'e.g. My description',
typeOptions: {
rows: 2,
},
},
{
displayName: 'Author',
name: 'author',
type: 'string',
default: '',
description: 'The name of the author',
placeholder: 'e.g. John Doe',
},
{
displayName: 'Color',
name: 'color',
// eslint-disable-next-line n8n-nodes-base/node-param-color-type-unused
type: 'color',
default: '',
description: 'Color code of the embed',
placeholder: 'e.g. 12123432',
},
{
displayName: 'Timestamp',
name: 'timestamp',
type: 'dateTime',
default: '',
description: 'The time displayed at the bottom of the embed. Provide in ISO8601 format.',
placeholder: 'e.g. 2023-02-08 09:30:26',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'The title of embed',
placeholder: "e.g. Embed's title",
},
{
displayName: 'URL',
name: 'url',
type: 'string',
default: '',
description: 'The URL where you want to link the embed to',
placeholder: 'e.g. https://discord.com/',
},
{
displayName: 'URL Image',
name: 'image',
type: 'string',
default: '',
description: 'Source URL of image (only supports http(s) and attachments)',
placeholder: 'e.g. https://example.com/image.png',
},
{
displayName: 'URL Thumbnail',
name: 'thumbnail',
type: 'string',
default: '',
description: 'Source URL of thumbnail (only supports http(s) and attachments)',
placeholder: 'e.g. https://example.com/image.png',
},
{
displayName: 'URL Video',
name: 'video',
type: 'string',
default: '',
description: 'Source URL of video',
placeholder: 'e.g. https://example.com/video.mp4',
},
];
const embedFieldsDescription = updateDisplayOptions(
{
show: {
inputMethod: ['fields'],
},
},
embedFields,
);
export const embedsFixedCollection: INodeProperties = {
displayName: 'Embeds',
name: 'embeds',
type: 'fixedCollection',
placeholder: 'Add Embeds',
typeOptions: {
multipleValues: true,
},
default: [],
options: [
{
displayName: 'Values',
name: 'values',
values: [
{
displayName: 'Input Method',
name: 'inputMethod',
type: 'options',
options: [
{
name: 'Enter Fields',
value: 'fields',
},
{
name: 'Raw JSON',
value: 'json',
},
],
default: 'fields',
},
{
displayName: 'Value',
name: 'json',
type: 'string',
default: '={}',
typeOptions: {
editor: 'json',
editorLanguage: 'json',
rows: 2,
},
displayOptions: {
show: {
inputMethod: ['json'],
},
},
},
...embedFieldsDescription,
],
},
],
};
// -------------------------------------------------------------------------------------------
export const filesFixedCollection: INodeProperties = {
displayName: 'Files',
name: 'files',
type: 'fixedCollection',
placeholder: 'Add Files',
typeOptions: {
multipleValues: true,
},
default: [],
options: [
{
displayName: 'Values',
name: 'values',
values: [
{
displayName: 'Input Data Field Name',
name: 'inputFieldName',
type: 'string',
default: 'data',
description: 'The contents of the file being sent with the message',
placeholder: 'e.g. data',
hint: 'The name of the input field containing the binary file data to be sent',
},
],
},
],
};

View file

@ -0,0 +1,121 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import { createSimplifyFunction, parseDiscordError, prepareErrorData } from '../../helpers/utils';
import { discordApiRequest } from '../../transport';
import { simplifyBoolean } from '../common.description';
import { returnAllOrLimit } from '../../../../../utils/descriptions';
const properties: INodeProperties[] = [
...returnAllOrLimit,
{
displayName: 'After',
name: 'after',
type: 'string',
default: '',
placeholder: 'e.g. 786953432728469534',
description: 'The ID of the user after which to return the members',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [simplifyBoolean],
},
];
const displayOptions = {
show: {
resource: ['member'],
operation: ['getAll'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const returnAll = this.getNodeParameter('returnAll', 0, false);
const after = this.getNodeParameter('after', 0);
const qs: IDataObject = {};
if (!returnAll) {
const limit = this.getNodeParameter('limit', 0);
qs.limit = limit;
}
if (after) {
qs.after = after;
}
let response: IDataObject[] = [];
try {
if (!returnAll) {
const limit = this.getNodeParameter('limit', 0);
qs.limit = limit;
response = await discordApiRequest.call(
this,
'GET',
`/guilds/${guildId}/members`,
undefined,
qs,
);
} else {
let responseData;
qs.limit = 100;
do {
responseData = await discordApiRequest.call(
this,
'GET',
`/guilds/${guildId}/members`,
undefined,
qs,
);
if (!responseData?.length) break;
qs.after = responseData[responseData.length - 1].user.id;
response.push(...responseData);
} while (responseData.length);
}
const simplify = this.getNodeParameter('options.simplify', 0, false) as boolean;
if (simplify) {
const simplifyResponse = createSimplifyFunction(['user', 'roles', 'permissions']);
response = response.map(simplifyResponse);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: 0 } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, 0));
}
throw err;
}
return returnData;
}

View file

@ -0,0 +1,56 @@
import type { INodeProperties } from 'n8n-workflow';
import * as getAll from './getAll.operation';
import * as roleAdd from './roleAdd.operation';
import * as roleRemove from './roleRemove.operation';
import { guildRLC } from '../common.description';
export { getAll, roleAdd, roleRemove };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['member'],
authentication: ['botToken', 'oAuth2'],
},
},
options: [
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve the members of a server',
action: 'Get many members',
},
{
name: 'Role Add',
value: 'roleAdd',
description: 'Add a role to a member',
action: 'Add a role to a member',
},
{
name: 'Role Remove',
value: 'roleRemove',
description: 'Remove a role from a member',
action: 'Remove a role from a member',
},
],
default: 'getAll',
},
{
...guildRLC,
displayOptions: {
show: {
resource: ['member'],
authentication: ['botToken', 'oAuth2'],
},
},
},
...getAll.description,
...roleAdd.description,
...roleRemove.description,
];

View file

@ -0,0 +1,63 @@
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import { parseDiscordError, prepareErrorData } from '../../helpers/utils';
import { discordApiRequest } from '../../transport';
import { roleMultiOptions, userRLC } from '../common.description';
const properties: INodeProperties[] = [userRLC, roleMultiOptions];
const displayOptions = {
show: {
resource: ['member'],
operation: ['roleAdd'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
for (let i = 0; i < items.length; i++) {
try {
const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
const roles = this.getNodeParameter('role', i, []) as string[];
for (const roleId of roles) {
await discordApiRequest.call(
this,
'PUT',
`/guilds/${guildId}/members/${userId}/roles/${roleId}`,
);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error, i);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, i));
continue;
}
throw err;
}
}
return returnData;
}

View file

@ -0,0 +1,63 @@
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import { parseDiscordError, prepareErrorData } from '../../helpers/utils';
import { discordApiRequest } from '../../transport';
import { roleMultiOptions, userRLC } from '../common.description';
const properties: INodeProperties[] = [userRLC, roleMultiOptions];
const displayOptions = {
show: {
resource: ['member'],
operation: ['roleRemove'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
for (let i = 0; i < items.length; i++) {
try {
const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
const roles = this.getNodeParameter('role', i, []) as string[];
for (const roleId of roles) {
await discordApiRequest.call(
this,
'DELETE',
`/guilds/${guildId}/members/${userId}/roles/${roleId}`,
);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error, i);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, i));
continue;
}
throw err;
}
}
return returnData;
}

View file

@ -0,0 +1,63 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import { parseDiscordError, prepareErrorData, setupChannelGetter } from '../../helpers/utils';
import { discordApiRequest } from '../../transport';
import { channelRLC, messageIdString } from '../common.description';
const properties: INodeProperties[] = [channelRLC, messageIdString];
const displayOptions = {
show: {
resource: ['message'],
operation: ['deleteMessage'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
userGuilds: IDataObject[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
const getChannelId = await setupChannelGetter.call(this, userGuilds);
for (let i = 0; i < items.length; i++) {
try {
const channelId = await getChannelId(i);
const messageId = this.getNodeParameter('messageId', i) as string;
await discordApiRequest.call(this, 'DELETE', `/channels/${channelId}/messages/${messageId}`);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error, i);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, i));
continue;
}
throw err;
}
}
return returnData;
}

View file

@ -0,0 +1,97 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import {
createSimplifyFunction,
parseDiscordError,
prepareErrorData,
setupChannelGetter,
} from '../../helpers/utils';
import { discordApiRequest } from '../../transport';
import { channelRLC, messageIdString, simplifyBoolean } from '../common.description';
const properties: INodeProperties[] = [
channelRLC,
messageIdString,
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [simplifyBoolean],
},
];
const displayOptions = {
show: {
resource: ['message'],
operation: ['get'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
userGuilds: IDataObject[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
const simplifyResponse = createSimplifyFunction([
'id',
'channel_id',
'author',
'content',
'timestamp',
'type',
]);
const getChannelId = await setupChannelGetter.call(this, userGuilds);
for (let i = 0; i < items.length; i++) {
try {
const channelId = await getChannelId(i);
const messageId = this.getNodeParameter('messageId', i) as string;
let response = await discordApiRequest.call(
this,
'GET',
`/channels/${channelId}/messages/${messageId}`,
);
const simplify = this.getNodeParameter('options.simplify', i, false) as boolean;
if (simplify) {
response = simplifyResponse(response);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error, i);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, i));
continue;
}
throw err;
}
}
return returnData;
}

View file

@ -0,0 +1,124 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import {
createSimplifyFunction,
parseDiscordError,
prepareErrorData,
setupChannelGetter,
} from '../../helpers/utils';
import { discordApiRequest } from '../../transport';
import { channelRLC, simplifyBoolean } from '../common.description';
import { returnAllOrLimit } from '../../../../../utils/descriptions';
const properties: INodeProperties[] = [
channelRLC,
...returnAllOrLimit,
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [simplifyBoolean],
},
];
const displayOptions = {
show: {
resource: ['message'],
operation: ['getAll'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
userGuilds: IDataObject[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
const simplifyResponse = createSimplifyFunction([
'id',
'channel_id',
'author',
'content',
'timestamp',
'type',
]);
const getChannelId = await setupChannelGetter.call(this, userGuilds);
for (let i = 0; i < items.length; i++) {
try {
const channelId = await getChannelId(i);
const returnAll = this.getNodeParameter('returnAll', i, false);
const qs: IDataObject = {};
let response: IDataObject[] = [];
if (!returnAll) {
const limit = this.getNodeParameter('limit', 0);
qs.limit = limit;
response = await discordApiRequest.call(
this,
'GET',
`/channels/${channelId}/messages`,
undefined,
qs,
);
} else {
let responseData;
qs.limit = 100;
do {
responseData = await discordApiRequest.call(
this,
'GET',
`/channels/${channelId}/messages`,
undefined,
qs,
);
if (!responseData?.length) break;
qs.before = responseData[responseData.length - 1].id;
response.push(...responseData);
} while (responseData.length);
}
const simplify = this.getNodeParameter('options.simplify', i, false) as boolean;
if (simplify) {
response = response.map(simplifyResponse);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error, i);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, i));
continue;
}
throw err;
}
}
return returnData;
}

View file

@ -0,0 +1,72 @@
import type { INodeProperties } from 'n8n-workflow';
import * as getAll from './getAll.operation';
import * as react from './react.operation';
import * as send from './send.operation';
import * as deleteMessage from './deleteMessage.operation';
import * as get from './get.operation';
import { guildRLC } from '../common.description';
export { getAll, react, send, deleteMessage, get };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['message'],
authentication: ['botToken', 'oAuth2'],
},
},
options: [
{
name: 'Delete',
value: 'deleteMessage',
description: 'Delete a message in a channel',
action: 'Delete a message',
},
{
name: 'Get',
value: 'get',
description: 'Get a message in a channel',
action: 'Get a message',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve the latest messages in a channel',
action: 'Get many messages',
},
{
name: 'React with Emoji',
value: 'react',
description: 'React to a message with an emoji',
action: 'React with an emoji to a message',
},
{
name: 'Send',
value: 'send',
description: 'Send a message to a channel, thread, or member',
action: 'Send a message',
},
],
default: 'send',
},
{
...guildRLC,
displayOptions: {
show: {
resource: ['message'],
authentication: ['botToken', 'oAuth2'],
},
},
},
...getAll.description,
...react.description,
...send.description,
...deleteMessage.description,
...get.description,
];

View file

@ -0,0 +1,79 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import { parseDiscordError, prepareErrorData, setupChannelGetter } from '../../helpers/utils';
import { discordApiRequest } from '../../transport';
import { channelRLC, messageIdString } from '../common.description';
const properties: INodeProperties[] = [
channelRLC,
messageIdString,
{
displayName: 'Emoji',
name: 'emoji',
type: 'string',
default: '',
required: true,
description: 'The emoji you want to react with',
},
];
const displayOptions = {
show: {
resource: ['message'],
operation: ['react'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
userGuilds: IDataObject[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
const getChannelId = await setupChannelGetter.call(this, userGuilds);
for (let i = 0; i < items.length; i++) {
try {
const channelId = await getChannelId(i);
const messageId = this.getNodeParameter('messageId', i) as string;
const emoji = this.getNodeParameter('emoji', i) as string;
await discordApiRequest.call(
this,
'PUT',
`/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error, i);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, i));
continue;
}
throw err;
}
}
return returnData;
}

View file

@ -0,0 +1,228 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import { discordApiMultiPartRequest, discordApiRequest } from '../../transport';
import {
embedsFixedCollection,
filesFixedCollection,
textChannelRLC,
userRLC,
} from '../common.description';
import {
checkAccessToChannel,
parseDiscordError,
prepareEmbeds,
prepareErrorData,
prepareMultiPartForm,
prepareOptions,
} from '../../helpers/utils';
const properties: INodeProperties[] = [
{
displayName: 'Send To',
name: 'sendTo',
type: 'options',
options: [
{
name: 'User',
value: 'user',
},
{
name: 'Channel',
value: 'channel',
},
],
default: 'channel',
description: 'Send message to a channel or DM to a user',
},
{
...userRLC,
displayOptions: {
show: {
sendTo: ['user'],
},
},
},
{
...textChannelRLC,
displayOptions: {
show: {
sendTo: ['channel'],
},
},
},
{
displayName: 'Message',
name: 'content',
type: 'string',
default: '',
required: true,
description: 'The content of the message (up to 2000 characters)',
placeholder: 'e.g. My message',
typeOptions: {
rows: 2,
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Flags',
name: 'flags',
type: 'multiOptions',
default: [],
description:
'Message flags. <a href="https://discord.com/developers/docs/resources/channel#message-object-message-flags" target="_blank">More info</a>.”.',
options: [
{
name: 'Suppress Embeds',
value: 'SUPPRESS_EMBEDS',
},
{
name: 'Suppress Notifications',
value: 'SUPPRESS_NOTIFICATIONS',
},
],
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
displayName: 'Message to Reply to',
name: 'message_reference',
type: 'string',
default: '',
description: 'Fill this to make your message a reply. Add the message ID.',
placeholder: 'e.g. 1059467601836773386',
},
{
displayName: 'Text-to-Speech (TTS)',
name: 'tts',
type: 'boolean',
default: false,
description: 'Whether to have a bot reading the message directly in the channel',
},
],
},
embedsFixedCollection,
filesFixedCollection,
];
const displayOptions = {
show: {
resource: ['message'],
operation: ['send'],
},
hide: {
authentication: ['webhook'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
guildId: string,
userGuilds: IDataObject[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
const isOAuth2 = this.getNodeParameter('authentication', 0) === 'oAuth2';
for (let i = 0; i < items.length; i++) {
const content = this.getNodeParameter('content', i) as string;
const options = prepareOptions(this.getNodeParameter('options', i, {}), guildId);
const embeds = (this.getNodeParameter('embeds', i, undefined) as IDataObject)
?.values as IDataObject[];
const files = (this.getNodeParameter('files', i, undefined) as IDataObject)
?.values as IDataObject[];
const body: IDataObject = {
content,
...options,
};
if (embeds) {
body.embeds = prepareEmbeds.call(this, embeds, i);
}
try {
const sendTo = this.getNodeParameter('sendTo', i) as string;
let channelId = '';
if (sendTo === 'user') {
const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
channelId = (
(await discordApiRequest.call(this, 'POST', '/users/@me/channels', {
recipient_id: userId,
})) as IDataObject
).id as string;
}
if (sendTo === 'channel') {
channelId = this.getNodeParameter('channelId', i, undefined, {
extractValue: true,
}) as string;
}
if (!channelId) {
throw new NodeOperationError(this.getNode(), 'Channel ID is required');
}
if (isOAuth2) await checkAccessToChannel.call(this, channelId, userGuilds, i);
let response: IDataObject[] = [];
if (files?.length) {
const multiPartBody = await prepareMultiPartForm.call(this, items, files, body, i);
response = await discordApiMultiPartRequest.call(
this,
'POST',
`/channels/${channelId}/messages`,
multiPartBody,
);
} else {
response = await discordApiRequest.call(
this,
'POST',
`/channels/${channelId}/messages`,
body,
);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error, i);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, i));
continue;
}
throw err;
}
}
return returnData;
}

View file

@ -0,0 +1,10 @@
import type { AllEntities } from 'n8n-workflow';
type NodeMap = {
channel: 'get' | 'getAll' | 'create' | 'update' | 'deleteChannel';
message: 'deleteMessage' | 'getAll' | 'get' | 'react' | 'send';
member: 'getAll' | 'roleAdd' | 'roleRemove';
webhook: 'sendLegacy';
};
export type Discord = AllEntities<NodeMap>;

View file

@ -0,0 +1,68 @@
import type { IDataObject, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { discordApiRequest } from '../transport';
import { checkAccessToGuild } from '../helpers/utils';
import * as message from './message';
import * as channel from './channel';
import * as member from './member';
import * as webhook from './webhook';
import type { Discord } from './node.type';
export async function router(this: IExecuteFunctions) {
let returnData: INodeExecutionData[] = [];
let resource = 'webhook';
//resource parameter is hidden when authentication is set to webhook
//prevent error when getting resource parameter
try {
resource = this.getNodeParameter<Discord>('resource', 0);
} catch (error) {}
const operation = this.getNodeParameter('operation', 0);
let guildId = '';
let userGuilds: IDataObject[] = [];
if (resource !== 'webhook') {
guildId = this.getNodeParameter('guildId', 0, '', {
extractValue: true,
}) as string;
const isOAuth2 = this.getNodeParameter('authentication', 0, '') === 'oAuth2';
if (isOAuth2) {
userGuilds = (await discordApiRequest.call(
this,
'GET',
'/users/@me/guilds',
)) as IDataObject[];
checkAccessToGuild(this.getNode(), guildId, userGuilds);
}
}
const discord = {
resource,
operation,
} as Discord;
switch (discord.resource) {
case 'channel':
returnData = await channel[discord.operation].execute.call(this, guildId, userGuilds);
break;
case 'message':
returnData = await message[discord.operation].execute.call(this, guildId, userGuilds);
break;
case 'member':
returnData = await member[discord.operation].execute.call(this, guildId);
break;
case 'webhook':
returnData = await webhook[discord.operation].execute.call(this);
break;
default:
throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known`);
}
return [returnData];
}

View file

@ -0,0 +1,106 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import type { INodeTypeDescription } from 'n8n-workflow';
import * as message from './message';
import * as channel from './channel';
import * as member from './member';
import * as webhook from './webhook';
export const versionDescription: INodeTypeDescription = {
displayName: 'Discord',
name: 'discord',
icon: 'file:discord.svg',
group: ['output'],
version: 2,
subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}',
description: 'Sends data to Discord',
defaults: {
name: 'Discord',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'discordBotApi',
required: true,
displayOptions: {
show: {
authentication: ['botToken'],
},
},
},
{
name: 'discordOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
{
name: 'discordWebhookApi',
displayOptions: {
show: {
authentication: ['webhook'],
},
},
},
],
properties: [
{
displayName: 'Connection Type',
name: 'authentication',
type: 'options',
options: [
{
name: 'Bot Token',
value: 'botToken',
description: 'Manage messages, channels, and members on a server',
},
{
name: 'OAuth2',
value: 'oAuth2',
description: 'Manage messages, channels, and members on a server',
},
{
name: 'Webhook',
value: 'webhook',
description: 'Send messages to a specific channel',
},
],
default: 'botToken',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Channel',
value: 'channel',
},
{
name: 'Message',
value: 'message',
},
{
name: 'Member',
value: 'member',
},
],
default: 'channel',
displayOptions: {
hide: {
authentication: ['webhook'],
},
},
},
...message.description,
...channel.description,
...member.description,
...webhook.description,
],
};

View file

@ -0,0 +1,29 @@
import type { INodeProperties } from 'n8n-workflow';
import * as sendLegacy from './sendLegacy.operation';
export { sendLegacy };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
authentication: ['webhook'],
},
},
options: [
{
name: 'Send a Message',
value: 'sendLegacy',
description: 'Send a message to a channel using the webhook',
action: 'Send a message',
},
],
default: 'sendLegacy',
},
...sendLegacy.description,
];

View file

@ -0,0 +1,167 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../utils/utilities';
import { discordApiMultiPartRequest, discordApiRequest } from '../../transport';
import {
parseDiscordError,
prepareEmbeds,
prepareErrorData,
prepareMultiPartForm,
prepareOptions,
} from '../../helpers/utils';
import { embedsFixedCollection, filesFixedCollection } from '../common.description';
const properties: INodeProperties[] = [
{
displayName: 'Message',
name: 'content',
type: 'string',
default: '',
required: true,
description: 'The content of the message (up to 2000 characters)',
placeholder: 'e.g. My message',
typeOptions: {
rows: 2,
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Avatar URL',
name: 'avatar_url',
type: 'string',
default: '',
description: 'Override the default avatar of the webhook',
placeholder: 'e.g. https://example.com/image.png',
},
{
displayName: 'Flags',
name: 'flags',
type: 'multiOptions',
default: [],
description:
'Message flags. <a href="https://discord.com/developers/docs/resources/channel#message-object-message-flags" target="_blank">More info</a>.”.',
options: [
{
name: 'Suppress Embeds',
value: 'SUPPRESS_EMBEDS',
},
{
name: 'Suppress Notifications',
value: 'SUPPRESS_NOTIFICATIONS',
},
],
},
{
displayName: 'Text-to-Speech (TTS)',
name: 'tts',
type: 'boolean',
default: false,
description: 'Whether to have a bot reading the message directly in the channel',
},
{
displayName: 'Username',
name: 'username',
type: 'string',
default: '',
description: 'Override the default username of the webhook',
placeholder: 'e.g. My Username',
},
{
displayName: 'Wait',
name: 'wait',
type: 'boolean',
default: false,
description: 'Whether wait for the message to be created before returning its response',
},
],
},
embedsFixedCollection,
filesFixedCollection,
];
const displayOptions = {
show: {
operation: ['sendLegacy'],
},
hide: {
authentication: ['botToken', 'oAuth2'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
for (let i = 0; i < items.length; i++) {
const content = this.getNodeParameter('content', i) as string;
const options = prepareOptions(this.getNodeParameter('options', i, {}));
const embeds = (this.getNodeParameter('embeds', i, undefined) as IDataObject)
?.values as IDataObject[];
const files = (this.getNodeParameter('files', i, undefined) as IDataObject)
?.values as IDataObject[];
let qs: IDataObject | undefined = undefined;
if (options.wait) {
qs = {
wait: options.wait,
};
delete options.wait;
}
const body: IDataObject = {
content,
...options,
};
if (embeds) {
body.embeds = prepareEmbeds.call(this, embeds);
}
try {
let response: IDataObject[] = [];
if (files?.length) {
const multiPartBody = await prepareMultiPartForm.call(this, items, files, body, i);
response = await discordApiMultiPartRequest.call(this, 'POST', '', multiPartBody);
} else {
response = await discordApiRequest.call(this, 'POST', '', body, qs);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
const err = parseDiscordError.call(this, error, i);
if (this.continueOnFail()) {
returnData.push(...prepareErrorData.call(this, err, i));
continue;
}
throw err;
}
}
return returnData;
}

View file

@ -0,0 +1,287 @@
import type {
IBinaryKeyData,
IDataObject,
IExecuteFunctions,
INode,
INodeExecutionData,
} from 'n8n-workflow';
import { jsonParse, NodeOperationError } from 'n8n-workflow';
import { isEmpty } from 'lodash';
import FormData from 'form-data';
import { capitalize } from '../../../../utils/utilities';
import { extension } from 'mime-types';
import { discordApiRequest } from '../transport';
export const createSimplifyFunction =
(includedFields: string[]) =>
(item: IDataObject): IDataObject => {
const result: IDataObject = {};
for (const field of includedFields) {
if (item[field] === undefined) continue;
result[field] = item[field];
}
return result;
};
export function parseDiscordError(this: IExecuteFunctions, error: any, itemIndex = 0) {
let errorData = error.cause.error;
const errorOptions: IDataObject = { itemIndex };
if (!errorData && error.description) {
try {
const errorString = (error.description as string).split(' - ')[1];
if (errorString) {
errorData = jsonParse(errorString);
}
} catch (err) {}
}
if (errorData?.message) {
errorOptions.message = errorData.message;
}
if ((error?.message as string)?.toLowerCase()?.includes('bad request') && errorData) {
if (errorData?.message) {
errorOptions.message = errorData.message;
}
if (errorData?.errors?.embeds) {
const embedErrors = errorData.errors.embeds?.[0];
const embedErrorsKeys = Object.keys(embedErrors).map((key) => capitalize(key));
if (embedErrorsKeys.length) {
const message =
embedErrorsKeys.length === 1
? `The parameter ${embedErrorsKeys[0]} is not properly formatted`
: `The parameters ${embedErrorsKeys.join(', ')} are not properly formatted`;
errorOptions.message = message;
errorOptions.description = 'Review the formatting or clear it';
}
return new NodeOperationError(this.getNode(), errorData.errors, errorOptions);
}
if (errorData?.errors?.message_reference) {
errorOptions.message = "The message to reply to ID can't be found";
errorOptions.description =
'Check the "Message to Reply to" parameter and remove it if you don\'t want to reply to an existing message';
return new NodeOperationError(this.getNode(), errorData.errors, errorOptions);
}
}
return new NodeOperationError(this.getNode(), errorData || error, errorOptions);
}
export function prepareErrorData(this: IExecuteFunctions, error: any, i: number) {
let description = error.description;
try {
description = JSON.parse(error.description as string);
} catch (err) {}
return this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message, description }),
{ itemData: { item: i } },
);
}
export function prepareOptions(options: IDataObject, guildId?: string) {
if (options.flags) {
if ((options.flags as string[]).length === 2) {
options.flags = (1 << 2) + (1 << 12);
} else if ((options.flags as string[]).includes('SUPPRESS_EMBEDS')) {
options.flags = 1 << 2;
} else if ((options.flags as string[]).includes('SUPPRESS_NOTIFICATIONS')) {
options.flags = 1 << 12;
}
}
if (options.message_reference) {
options.message_reference = {
message_id: options.message_reference,
guild_id: guildId,
};
}
return options;
}
export function prepareEmbeds(this: IExecuteFunctions, embeds: IDataObject[], i = 0) {
return embeds
.map((embed, index) => {
let embedReturnData: IDataObject = {};
if (embed.inputMethod === 'json') {
if (typeof embed.json === 'object') {
embedReturnData = embed.json as IDataObject;
}
try {
embedReturnData = jsonParse(embed.json as string);
} catch (error) {
throw new NodeOperationError(this.getNode(), 'Not a valid JSON', error);
}
} else {
delete embed.inputMethod;
for (const key of Object.keys(embed)) {
if (embed[key] !== '') {
embedReturnData[key] = embed[key];
}
}
}
if (!embedReturnData.description) {
throw new NodeOperationError(
this.getNode(),
`Description is required, embed ${index} in item ${i} is missing it`,
);
}
if (embedReturnData.author) {
embedReturnData.author = {
name: embedReturnData.author,
};
}
if (embedReturnData.color && typeof embedReturnData.color === 'string') {
embedReturnData.color = parseInt(embedReturnData.color.replace('#', ''), 16);
}
if (embedReturnData.video) {
embedReturnData.video = {
url: embedReturnData.video,
width: 1270,
height: 720,
};
}
if (embedReturnData.thumbnail) {
embedReturnData.thumbnail = {
url: embedReturnData.thumbnail,
};
}
if (embedReturnData.image) {
embedReturnData.image = {
url: embedReturnData.image,
};
}
return embedReturnData;
})
.filter((embed) => !isEmpty(embed));
}
export async function prepareMultiPartForm(
this: IExecuteFunctions,
items: INodeExecutionData[],
files: IDataObject[],
jsonPayload: IDataObject,
i: number,
) {
const multiPartBody = new FormData();
const attachments: IDataObject[] = [];
const filesData: IDataObject[] = [];
for (const [index, file] of files.entries()) {
const binaryData = (items[i].binary as IBinaryKeyData)?.[file.inputFieldName as string];
if (!binaryData) {
throw new NodeOperationError(
this.getNode(),
`Input item [${i}] does not contain binary data on property ${file.inputFieldName}`,
);
}
let filename = binaryData.fileName as string;
if (!filename.includes('.')) {
if (binaryData.fileExtension) {
filename += `.${binaryData.fileExtension}`;
}
if (binaryData.mimeType) {
filename += `.${extension(binaryData.mimeType)}`;
}
}
attachments.push({
id: index,
filename,
});
filesData.push({
data: await this.helpers.getBinaryDataBuffer(i, file.inputFieldName as string),
name: filename,
mime: binaryData.mimeType,
});
}
multiPartBody.append('payload_json', JSON.stringify({ ...jsonPayload, attachments }), {
contentType: 'application/json',
});
for (const [index, binaryData] of filesData.entries()) {
multiPartBody.append(`files[${index}]`, binaryData.data, {
contentType: binaryData.name as string,
filename: binaryData.mime as string,
});
}
return multiPartBody;
}
export function checkAccessToGuild(
node: INode,
guildId: string,
userGuilds: IDataObject[],
itemIndex = 0,
) {
if (!userGuilds.some((guild) => guild.id === guildId)) {
throw new NodeOperationError(
node,
`You do not have access to the guild with the id ${guildId}`,
{
itemIndex,
},
);
}
}
export async function checkAccessToChannel(
this: IExecuteFunctions,
channelId: string,
userGuilds: IDataObject[],
itemIndex = 0,
) {
let guildId = '';
try {
const channel = await discordApiRequest.call(this, 'GET', `/channels/${channelId}`);
guildId = channel.guild_id;
} catch (error) {}
if (!guildId) {
throw new NodeOperationError(
this.getNode(),
`Could not fing server for channel with the id ${channelId}`,
{
itemIndex,
},
);
}
checkAccessToGuild(this.getNode(), guildId, userGuilds, itemIndex);
}
export async function setupChannelGetter(this: IExecuteFunctions, userGuilds: IDataObject[]) {
const isOAuth2 = this.getNodeParameter('authentication', 0) === 'oAuth2';
return async (i: number) => {
const channelId = this.getNodeParameter('channelId', i, undefined, {
extractValue: true,
}) as string;
if (isOAuth2) await checkAccessToChannel.call(this, channelId, userGuilds, i);
return channelId;
};
}

View file

@ -0,0 +1,2 @@
export * as listSearch from './listSearch';
export * as loadOptions from './loadOptions';

View file

@ -0,0 +1,164 @@
import {
type IDataObject,
type ILoadOptionsFunctions,
type INodeListSearchResult,
} from 'n8n-workflow';
import { discordApiRequest } from '../transport';
import { checkAccessToGuild } from '../helpers/utils';
async function getGuildId(this: ILoadOptionsFunctions) {
const guildId = this.getNodeParameter('guildId', undefined, {
extractValue: true,
}) as string;
const isOAuth2 = this.getNodeParameter('authentication', '') === 'oAuth2';
if (isOAuth2) {
const userGuilds = (await discordApiRequest.call(
this,
'GET',
'/users/@me/guilds',
)) as IDataObject[];
checkAccessToGuild(this.getNode(), guildId, userGuilds);
}
return guildId;
}
async function checkBotAccessToGuild(this: ILoadOptionsFunctions, guildId: string, botId: string) {
try {
const members: Array<{ user: { id: string } }> = await discordApiRequest.call(
this,
'GET',
`/guilds/${guildId}/members`,
undefined,
{ limit: 1000 },
);
return members.some((member) => member.user.id === botId);
} catch (error) {}
return false;
}
export async function guildSearch(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
const response = (await discordApiRequest.call(
this,
'GET',
'/users/@me/guilds',
)) as IDataObject[];
let guilds: IDataObject[] = [];
const isOAuth2 = this.getNodeParameter('authentication', 0) === 'oAuth2';
if (isOAuth2) {
const botId = (await discordApiRequest.call(this, 'GET', '/users/@me')).id as string;
for (const guild of response) {
if (!(await checkBotAccessToGuild.call(this, guild.id as string, botId))) continue;
guilds.push(guild);
}
} else {
guilds = response;
}
return {
results: guilds.map((guild) => ({
name: guild.name as string,
value: guild.id as string,
url: `https://discord.com/channels/${guild.id}`,
})),
};
}
export async function channelSearch(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
const guildId = await getGuildId.call(this);
const response = await discordApiRequest.call(this, 'GET', `/guilds/${guildId}/channels`);
return {
results: (response as IDataObject[])
.filter((cannel) => cannel.type !== 4) // Filter out categories
.map((channel) => ({
name: channel.name as string,
value: channel.id as string,
url: `https://discord.com/channels/${guildId}/${channel.id}`,
})),
};
}
export async function textChannelSearch(
this: ILoadOptionsFunctions,
): Promise<INodeListSearchResult> {
const guildId = await getGuildId.call(this);
const response = await discordApiRequest.call(this, 'GET', `/guilds/${guildId}/channels`);
return {
results: (response as IDataObject[])
.filter((cannel) => ![2, 4].includes(cannel.type as number)) // Only text channels
.map((channel) => ({
name: channel.name as string,
value: channel.id as string,
url: `https://discord.com/channels/${guildId}/${channel.id}`,
})),
};
}
export async function categorySearch(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
const guildId = await getGuildId.call(this);
const response = await discordApiRequest.call(this, 'GET', `/guilds/${guildId}/channels`);
return {
results: (response as IDataObject[])
.filter((cannel) => cannel.type === 4) // Return only categories
.map((channel) => ({
name: channel.name as string,
value: channel.id as string,
url: `https://discord.com/channels/${guildId}/${channel.id}`,
})),
};
}
export async function userSearch(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const guildId = await getGuildId.call(this);
const limit = 100;
const qs = { limit, after: paginationToken };
const response = await discordApiRequest.call(
this,
'GET',
`/guilds/${guildId}/members`,
undefined,
qs,
);
if (response.length === 0) {
return {
results: [],
paginationToken: undefined,
};
}
let lastUserId;
//less then limit means that there are no more users to return, so leave lastUserId undefined
if (!(response.length < limit)) {
lastUserId = response[response.length - 1].user.id as string;
}
return {
results: (response as Array<{ user: IDataObject }>).map(({ user }) => ({
name: user.username as string,
value: user.id as string,
})),
paginationToken: lastUserId,
};
}

View file

@ -0,0 +1,46 @@
import type { IDataObject, ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
import { discordApiRequest } from '../transport';
import { checkAccessToGuild } from '../helpers/utils';
export async function getRoles(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const guildId = this.getNodeParameter('guildId', undefined, {
extractValue: true,
}) as string;
const isOAuth2 = this.getNodeParameter('authentication', '') === 'oAuth2';
if (isOAuth2) {
const userGuilds = (await discordApiRequest.call(
this,
'GET',
'/users/@me/guilds',
)) as IDataObject[];
checkAccessToGuild(this.getNode(), guildId, userGuilds);
}
let response = await discordApiRequest.call(this, 'GET', `/guilds/${guildId}/roles`);
const operations = this.getNodeParameter('operation') as string;
if (operations === 'roleRemove') {
const userId = this.getNodeParameter('userId', undefined, {
extractValue: true,
}) as string;
const userRoles = ((
await discordApiRequest.call(this, 'GET', `/guilds/${guildId}/members/${userId}`)
).roles || []) as string[];
response = response.filter((role: IDataObject) => {
return userRoles.includes(role.id as string);
});
}
return response
.filter((role: IDataObject) => role.name !== '@everyone' && !role.managed)
.map((role: IDataObject) => ({
name: role.name as string,
value: role.id as string,
}));
}

View file

@ -0,0 +1,101 @@
import type { OptionsWithUrl } from 'request';
import type {
IDataObject,
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-workflow';
import { sleep, NodeApiError, jsonParse } from 'n8n-workflow';
import type FormData from 'form-data';
import { getCredentialsType, requestApi } from './helpers';
export async function discordApiRequest(
this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
method: string,
endpoint: string,
body?: IDataObject,
qs?: IDataObject,
) {
const authentication = this.getNodeParameter('authentication', 0, 'webhook') as string;
const headers: IDataObject = {};
const credentialType = getCredentialsType(authentication);
const options: OptionsWithUrl = {
headers,
method,
qs,
body,
url: `https://discord.com/api/v10${endpoint}`,
json: true,
};
if (credentialType === 'discordWebhookApi') {
const credentials = await this.getCredentials('discordWebhookApi');
options.url = credentials.webhookUri as string;
}
try {
const response = await requestApi.call(this, options, credentialType, endpoint);
const resetAfter = Number(response.headers['x-ratelimit-reset-after']);
const remaining = Number(response.headers['x-ratelimit-remaining']);
if (remaining === 0) {
await sleep(resetAfter);
} else {
await sleep(20); //prevent excing global rate limit of 50 requests per second
}
return response.body || { success: true };
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
export async function discordApiMultiPartRequest(
this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
method: string,
endpoint: string,
formData: FormData,
) {
const headers: IDataObject = {
'content-type': 'multipart/form-data; charset=utf-8',
};
const authentication = this.getNodeParameter('authentication', 0, 'webhook') as string;
const credentialType = getCredentialsType(authentication);
const options: OptionsWithUrl = {
headers,
method,
formData,
url: `https://discord.com/api/v10${endpoint}`,
};
if (credentialType === 'discordWebhookApi') {
const credentials = await this.getCredentials('discordWebhookApi');
options.url = credentials.webhookUri as string;
}
try {
const response = await requestApi.call(this, options, credentialType, endpoint);
const resetAfter = Number(response.headers['x-ratelimit-reset-after']);
const remaining = Number(response.headers['x-ratelimit-remaining']);
if (remaining === 0) {
await sleep(resetAfter);
} else {
await sleep(20); //prevent excing global rate limit of 50 requests per second
}
return jsonParse<IDataObject[]>(response.body);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}

View file

@ -0,0 +1,47 @@
import type { OptionsWithUrl } from 'request';
import type {
IDataObject,
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-workflow';
export const getCredentialsType = (authentication: string) => {
let credentialType = '';
switch (authentication) {
case 'botToken':
credentialType = 'discordBotApi';
break;
case 'oAuth2':
credentialType = 'discordOAuth2Api';
break;
case 'webhook':
credentialType = 'discordWebhookApi';
break;
default:
credentialType = 'discordBotApi';
}
return credentialType;
};
export async function requestApi(
this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
options: OptionsWithUrl,
credentialType: string,
endpoint: string,
) {
let response;
if (credentialType === 'discordOAuth2Api' && endpoint !== '/users/@me/guilds') {
const credentials = await this.getCredentials('discordOAuth2Api');
(options.headers as IDataObject)!.Authorization = `Bot ${credentials.botToken}`;
response = await this.helpers.request({ ...options, resolveWithFullResponse: true });
} else {
response = await this.helpers.requestWithAuthentication.call(this, credentialType, {
...options,
resolveWithFullResponse: true,
});
}
return response;
}

View file

@ -0,0 +1,2 @@
export * from './discord.api';
export * from './helpers';

View file

@ -89,6 +89,9 @@
"dist/credentials/DeepLApi.credentials.js",
"dist/credentials/DemioApi.credentials.js",
"dist/credentials/DhlApi.credentials.js",
"dist/credentials/DiscordBotApi.credentials.js",
"dist/credentials/DiscordOAuth2Api.credentials.js",
"dist/credentials/DiscordWebhookApi.credentials.js",
"dist/credentials/DiscourseApi.credentials.js",
"dist/credentials/DisqusApi.credentials.js",
"dist/credentials/DriftApi.credentials.js",

View file

@ -7,3 +7,28 @@ export const oldVersionNotice: INodeProperties = {
type: 'notice',
default: '',
};
export const returnAllOrLimit: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
},
default: 100,
description: 'Max number of results to return',
},
];

View file

@ -294,10 +294,19 @@ export function flattenObject(data: IDataObject) {
}
/**
* Generate Paired Item Data by length of input array
* Capitalizes the first letter of a string
*
* @param {number} length
* @param {string} string The string to capitalize
*/
export function capitalize(str: string): string {
if (!str) return str;
const chars = str.split('');
chars[0] = chars[0].toUpperCase();
return chars.join('');
}
export function generatePairedItemData(length: number): IPairedItemData[] {
return Array.from({ length }, (_, item) => ({
item,