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,328 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, IDataObject } from 'n8n-workflow';
|
||||
import { NodeOperationError, jsonParse } from 'n8n-workflow';
|
||||
import { prepareMultiPartForm } from '../utils';
|
||||
|
||||
describe('Discord V2 Utils', () => {
|
||||
describe('prepareMultiPartForm', () => {
|
||||
let mockExecuteFunctions: IExecuteFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should create multipart form with single file', async () => {
|
||||
const files: IDataObject[] = [{ inputFieldName: 'file1' }];
|
||||
const jsonPayload: IDataObject = { content: 'Test message' };
|
||||
const itemIndex = 0;
|
||||
|
||||
const binaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'image/png',
|
||||
fileName: 'test.png',
|
||||
fileExtension: 'png',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData = jest.fn().mockReturnValue(binaryData);
|
||||
mockExecuteFunctions.helpers.getBinaryDataBuffer = jest
|
||||
.fn()
|
||||
.mockResolvedValue(Buffer.from('test file content'));
|
||||
mockExecuteFunctions.getNode = jest.fn().mockReturnValue({ name: 'Discord' });
|
||||
|
||||
const result = await prepareMultiPartForm.call(
|
||||
mockExecuteFunctions,
|
||||
files,
|
||||
jsonPayload,
|
||||
itemIndex,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'file1');
|
||||
expect(mockExecuteFunctions.helpers.getBinaryDataBuffer).toHaveBeenCalledWith(0, 'file1');
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create multipart form with multiple files', async () => {
|
||||
const files: IDataObject[] = [{ inputFieldName: 'file1' }, { inputFieldName: 'file2' }];
|
||||
const jsonPayload: IDataObject = { content: 'Test message with multiple files' };
|
||||
const itemIndex = 0;
|
||||
|
||||
const binaryData1 = {
|
||||
data: 'base64data1',
|
||||
mimeType: 'image/png',
|
||||
fileName: 'test1.png',
|
||||
fileExtension: 'png',
|
||||
};
|
||||
|
||||
const binaryData2 = {
|
||||
data: 'base64data2',
|
||||
mimeType: 'image/jpeg',
|
||||
fileName: 'test2.jpg',
|
||||
fileExtension: 'jpg',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(binaryData1)
|
||||
.mockReturnValueOnce(binaryData2);
|
||||
mockExecuteFunctions.helpers.getBinaryDataBuffer = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(Buffer.from('test file content 1'))
|
||||
.mockResolvedValueOnce(Buffer.from('test file content 2'));
|
||||
mockExecuteFunctions.getNode = jest.fn().mockReturnValue({ name: 'Discord' });
|
||||
|
||||
const result = await prepareMultiPartForm.call(
|
||||
mockExecuteFunctions,
|
||||
files,
|
||||
jsonPayload,
|
||||
itemIndex,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.helpers.assertBinaryData).toHaveBeenCalledTimes(2);
|
||||
expect(mockExecuteFunctions.helpers.getBinaryDataBuffer).toHaveBeenCalledTimes(2);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should add file extension from fileExtension property when filename has no extension', async () => {
|
||||
const files: IDataObject[] = [{ inputFieldName: 'file1' }];
|
||||
const jsonPayload: IDataObject = { content: 'Test message' };
|
||||
const itemIndex = 0;
|
||||
|
||||
const binaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'image/png',
|
||||
fileName: 'testfile',
|
||||
fileExtension: 'png',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData = jest.fn().mockReturnValue(binaryData);
|
||||
mockExecuteFunctions.helpers.getBinaryDataBuffer = jest
|
||||
.fn()
|
||||
.mockResolvedValue(Buffer.from('test file content'));
|
||||
mockExecuteFunctions.getNode = jest.fn().mockReturnValue({ name: 'Discord' });
|
||||
|
||||
const result = await prepareMultiPartForm.call(
|
||||
mockExecuteFunctions,
|
||||
files,
|
||||
jsonPayload,
|
||||
itemIndex,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should add file extension from mimeType when filename has no extension and fileExtension is missing', async () => {
|
||||
const files: IDataObject[] = [{ inputFieldName: 'file1' }];
|
||||
const jsonPayload: IDataObject = { content: 'Test message' };
|
||||
const itemIndex = 0;
|
||||
|
||||
const binaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'image/jpeg',
|
||||
fileName: 'testfile',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData = jest.fn().mockReturnValue(binaryData);
|
||||
mockExecuteFunctions.helpers.getBinaryDataBuffer = jest
|
||||
.fn()
|
||||
.mockResolvedValue(Buffer.from('test file content'));
|
||||
mockExecuteFunctions.getNode = jest.fn().mockReturnValue({ name: 'Discord' });
|
||||
|
||||
const result = await prepareMultiPartForm.call(
|
||||
mockExecuteFunctions,
|
||||
files,
|
||||
jsonPayload,
|
||||
itemIndex,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw error when binary data is not found', async () => {
|
||||
const files: IDataObject[] = [{ inputFieldName: 'file1' }];
|
||||
const jsonPayload: IDataObject = { content: 'Test message' };
|
||||
const itemIndex = 0;
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData = jest.fn().mockReturnValue(null);
|
||||
mockExecuteFunctions.getNode = jest.fn().mockReturnValue({ name: 'Discord' });
|
||||
|
||||
await expect(
|
||||
prepareMultiPartForm.call(mockExecuteFunctions, files, jsonPayload, itemIndex),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
|
||||
await expect(
|
||||
prepareMultiPartForm.call(mockExecuteFunctions, files, jsonPayload, itemIndex),
|
||||
).rejects.toThrow('Input item [0] does not contain binary data on property file1');
|
||||
});
|
||||
|
||||
it('should handle file with complete filename including extension', async () => {
|
||||
const files: IDataObject[] = [{ inputFieldName: 'file1' }];
|
||||
const jsonPayload: IDataObject = { content: 'Test message' };
|
||||
const itemIndex = 0;
|
||||
|
||||
const binaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/pdf',
|
||||
fileName: 'document.pdf',
|
||||
fileExtension: 'pdf',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData = jest.fn().mockReturnValue(binaryData);
|
||||
mockExecuteFunctions.helpers.getBinaryDataBuffer = jest
|
||||
.fn()
|
||||
.mockResolvedValue(Buffer.from('pdf content'));
|
||||
mockExecuteFunctions.getNode = jest.fn().mockReturnValue({ name: 'Discord' });
|
||||
|
||||
const result = await prepareMultiPartForm.call(
|
||||
mockExecuteFunctions,
|
||||
files,
|
||||
jsonPayload,
|
||||
itemIndex,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should include attachments in payload_json with correct structure', async () => {
|
||||
const files: IDataObject[] = [{ inputFieldName: 'file1' }, { inputFieldName: 'file2' }];
|
||||
const jsonPayload: IDataObject = { content: 'Test with attachments' };
|
||||
const itemIndex = 0;
|
||||
|
||||
const binaryData1 = {
|
||||
data: 'base64data1',
|
||||
mimeType: 'image/png',
|
||||
fileName: 'image1.png',
|
||||
};
|
||||
|
||||
const binaryData2 = {
|
||||
data: 'base64data2',
|
||||
mimeType: 'text/plain',
|
||||
fileName: 'text.txt',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(binaryData1)
|
||||
.mockReturnValueOnce(binaryData2);
|
||||
mockExecuteFunctions.helpers.getBinaryDataBuffer = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(Buffer.from('image content'))
|
||||
.mockResolvedValueOnce(Buffer.from('text content'));
|
||||
mockExecuteFunctions.getNode = jest.fn().mockReturnValue({ name: 'Discord' });
|
||||
|
||||
const result = await prepareMultiPartForm.call(
|
||||
mockExecuteFunctions,
|
||||
files,
|
||||
jsonPayload,
|
||||
itemIndex,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
|
||||
const payloadJsonField = result
|
||||
.getBuffer()
|
||||
.toString()
|
||||
.match(
|
||||
/Content-Disposition: form-data; name="payload_json"[\s\S]*?\r\n\r\n([\s\S]*?)\r\n--/,
|
||||
);
|
||||
expect(payloadJsonField).not.toBeNull();
|
||||
|
||||
const payloadJson = jsonParse<{
|
||||
content: string;
|
||||
attachments: Array<{ id: number; filename: string }>;
|
||||
}>(payloadJsonField![1]);
|
||||
|
||||
expect(payloadJson.content).toBe('Test with attachments');
|
||||
|
||||
expect(payloadJson.attachments).toBeDefined();
|
||||
expect(Array.isArray(payloadJson.attachments)).toBe(true);
|
||||
expect(payloadJson.attachments).toHaveLength(2);
|
||||
|
||||
expect(payloadJson.attachments[0]).toEqual({
|
||||
id: 0,
|
||||
filename: 'image1.png',
|
||||
});
|
||||
|
||||
expect(payloadJson.attachments[1]).toEqual({
|
||||
id: 1,
|
||||
filename: 'text.txt',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty files array', async () => {
|
||||
const files: IDataObject[] = [];
|
||||
const jsonPayload: IDataObject = { content: 'Test message no files' };
|
||||
const itemIndex = 0;
|
||||
|
||||
mockExecuteFunctions.getNode = jest.fn().mockReturnValue({ name: 'Discord' });
|
||||
|
||||
const result = await prepareMultiPartForm.call(
|
||||
mockExecuteFunctions,
|
||||
files,
|
||||
jsonPayload,
|
||||
itemIndex,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should properly format filename when only mimeType is available', async () => {
|
||||
const files: IDataObject[] = [{ inputFieldName: 'file1' }];
|
||||
const jsonPayload: IDataObject = { content: 'Test message' };
|
||||
const itemIndex = 0;
|
||||
|
||||
const binaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'application/json',
|
||||
fileName: 'data',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData = jest.fn().mockReturnValue(binaryData);
|
||||
mockExecuteFunctions.helpers.getBinaryDataBuffer = jest
|
||||
.fn()
|
||||
.mockResolvedValue(Buffer.from('json content'));
|
||||
mockExecuteFunctions.getNode = jest.fn().mockReturnValue({ name: 'Discord' });
|
||||
|
||||
const result = await prepareMultiPartForm.call(
|
||||
mockExecuteFunctions,
|
||||
files,
|
||||
jsonPayload,
|
||||
itemIndex,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle different item indices correctly', async () => {
|
||||
const files: IDataObject[] = [{ inputFieldName: 'file1' }];
|
||||
const jsonPayload: IDataObject = { content: 'Test message' };
|
||||
const itemIndex = 2;
|
||||
|
||||
const binaryData = {
|
||||
data: 'base64data',
|
||||
mimeType: 'image/png',
|
||||
fileName: 'test.png',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData = jest.fn().mockReturnValue(binaryData);
|
||||
mockExecuteFunctions.helpers.getBinaryDataBuffer = jest
|
||||
.fn()
|
||||
.mockResolvedValue(Buffer.from('test file content'));
|
||||
mockExecuteFunctions.getNode = jest.fn().mockReturnValue({ name: 'Discord' });
|
||||
|
||||
const result = await prepareMultiPartForm.call(
|
||||
mockExecuteFunctions,
|
||||
files,
|
||||
jsonPayload,
|
||||
itemIndex,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.helpers.assertBinaryData).toHaveBeenCalledWith(2, 'file1');
|
||||
expect(mockExecuteFunctions.helpers.getBinaryDataBuffer).toHaveBeenCalledWith(2, 'file1');
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,419 @@
|
||||
import FormData from 'form-data';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { extension } from 'mime-types';
|
||||
import type { IDataObject, IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
import { jsonParse, NodeApiError, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { getSendAndWaitConfig } from '../../../../utils/sendAndWait/utils';
|
||||
import { capitalize, createUtmCampaignLink } from '../../../../utils/utilities';
|
||||
import { discordApiMultiPartRequest, 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 };
|
||||
|
||||
error.description =
|
||||
Array.isArray(error.messages) && error.messages.length ? error.messages[0] : error.description;
|
||||
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);
|
||||
}
|
||||
|
||||
if (errorOptions.message === 'Cannot send an empty message') {
|
||||
errorOptions.description =
|
||||
'Something has to be send to the channel whether it is a message, an embed or a file';
|
||||
}
|
||||
}
|
||||
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[]) {
|
||||
return embeds
|
||||
.map((embed) => {
|
||||
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.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,
|
||||
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 = this.helpers.assertBinaryData(i, 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,
|
||||
level: 'warning',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 find 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;
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendDiscordMessage(
|
||||
this: IExecuteFunctions,
|
||||
{
|
||||
guildId,
|
||||
userGuilds,
|
||||
isOAuth2,
|
||||
body,
|
||||
files = [],
|
||||
itemIndex = 0,
|
||||
}: {
|
||||
guildId: string;
|
||||
userGuilds: IDataObject[];
|
||||
isOAuth2: boolean;
|
||||
body: IDataObject;
|
||||
files?: IDataObject[];
|
||||
itemIndex?: number;
|
||||
},
|
||||
) {
|
||||
const sendTo = this.getNodeParameter('sendTo', itemIndex) as string;
|
||||
|
||||
let channelId = '';
|
||||
|
||||
if (sendTo === 'user') {
|
||||
const userId = this.getNodeParameter('userId', itemIndex, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
if (isOAuth2) {
|
||||
try {
|
||||
await discordApiRequest.call(this, 'GET', `/guilds/${guildId}/members/${userId}`);
|
||||
} catch (error) {
|
||||
if (error instanceof NodeApiError && error.httpCode === '404') {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`User with the id ${userId} is not a member of the selected guild`,
|
||||
{
|
||||
itemIndex,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw new NodeOperationError(this.getNode(), error, {
|
||||
itemIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
channelId = (
|
||||
(await discordApiRequest.call(this, 'POST', '/users/@me/channels', {
|
||||
recipient_id: userId,
|
||||
})) as IDataObject
|
||||
).id as string;
|
||||
|
||||
if (!channelId) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Could not create a channel to send direct message to',
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (sendTo === 'channel') {
|
||||
channelId = this.getNodeParameter('channelId', itemIndex, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
}
|
||||
|
||||
if (isOAuth2 && sendTo !== 'user') {
|
||||
await checkAccessToChannel.call(this, channelId, userGuilds, itemIndex);
|
||||
}
|
||||
|
||||
if (!channelId) {
|
||||
throw new NodeOperationError(this.getNode(), 'Channel ID is required', {
|
||||
itemIndex,
|
||||
});
|
||||
}
|
||||
|
||||
let response: IDataObject[] = [];
|
||||
|
||||
if (files?.length) {
|
||||
const multiPartBody = await prepareMultiPartForm.call(this, files, body, itemIndex);
|
||||
|
||||
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: itemIndex } },
|
||||
);
|
||||
|
||||
return executionData;
|
||||
}
|
||||
|
||||
export function createSendAndWaitMessageBody(context: IExecuteFunctions) {
|
||||
const config = getSendAndWaitConfig(context);
|
||||
let description = config.message;
|
||||
if (config.appendAttribution !== false) {
|
||||
const instanceId = context.getInstanceId();
|
||||
const attributionText = 'This message was sent automatically with ';
|
||||
const link = createUtmCampaignLink('n8n-nodes-base.discord', instanceId);
|
||||
description = `${config.message}\n\n_${attributionText}_[n8n](${link})`;
|
||||
}
|
||||
|
||||
const body = {
|
||||
embeds: [
|
||||
{
|
||||
description,
|
||||
color: 5814783,
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
type: 1,
|
||||
components: config.options.map((option) => {
|
||||
return {
|
||||
type: 2,
|
||||
style: 5,
|
||||
label: option.label,
|
||||
url: option.url,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return body;
|
||||
}
|
||||
Reference in New Issue
Block a user