first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
jsonParse,
|
||||
NodeApiError,
|
||||
NodeConnectionTypes,
|
||||
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: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.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 },
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Attachments',
|
||||
name: 'attachments',
|
||||
type: 'json',
|
||||
typeOptions: { alwaysOpenEditWindow: true },
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Avatar URL',
|
||||
name: 'avatarUrl',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Components',
|
||||
name: 'components',
|
||||
type: 'json',
|
||||
typeOptions: { alwaysOpenEditWindow: true },
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Embeds',
|
||||
name: 'embeds',
|
||||
type: 'json',
|
||||
typeOptions: { alwaysOpenEditWindow: true },
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Flags',
|
||||
name: 'flags',
|
||||
type: 'number',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'JSON Payload',
|
||||
name: 'payloadJson',
|
||||
type: 'json',
|
||||
typeOptions: { alwaysOpenEditWindow: true },
|
||||
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: IRequestOptions;
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export interface DiscordWebhook {
|
||||
content?: string;
|
||||
username?: string;
|
||||
avatar_url?: string;
|
||||
tts?: boolean;
|
||||
file?: Buffer;
|
||||
embeds?: any[];
|
||||
allowed_mentions?: {
|
||||
parse: Array<'roles' | 'users' | 'everyone'>;
|
||||
roles: string[];
|
||||
users: string[];
|
||||
replied_user: boolean;
|
||||
};
|
||||
flags?: number;
|
||||
attachments?: DiscordAttachment[];
|
||||
components?: any[];
|
||||
payload_json?: any;
|
||||
}
|
||||
|
||||
export interface DiscordAttachment {
|
||||
id?: string;
|
||||
filename?: string;
|
||||
size?: number;
|
||||
description?: string;
|
||||
content_type?: string;
|
||||
url?: string;
|
||||
proxy_url?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
ephemeral?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user