mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(Discord Node): New sendAndWait operation (#12894)
Co-authored-by: Dana <152518854+dana-gill@users.noreply.github.com>
This commit is contained in:
parent
066908060f
commit
d47bfddd65
|
@ -2,7 +2,10 @@
|
||||||
"node": "n8n-nodes-base.discord",
|
"node": "n8n-nodes-base.discord",
|
||||||
"nodeVersion": "1.0",
|
"nodeVersion": "1.0",
|
||||||
"codexVersion": "1.0",
|
"codexVersion": "1.0",
|
||||||
"categories": ["Communication"],
|
"categories": ["Communication", "HITL"],
|
||||||
|
"subcategories": {
|
||||||
|
"HITL": ["Human in the Loop"]
|
||||||
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"credentialDocumentation": [
|
"credentialDocumentation": [
|
||||||
{
|
{
|
||||||
|
@ -14,5 +17,6 @@
|
||||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.discord/"
|
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.discord/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"alias": ["human", "form", "wait", "hitl", "approval"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
import type { MockProxy } from 'jest-mock-extended';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import { SEND_AND_WAIT_OPERATION, type IExecuteFunctions } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { versionDescription } from '../../../../v2/actions/versionDescription';
|
||||||
|
import { DiscordV2 } from '../../../../v2/DiscordV2.node';
|
||||||
|
import * as transport from '../../../../v2/transport/discord.api';
|
||||||
|
|
||||||
|
jest.mock('../../../../v2/transport/discord.api', () => {
|
||||||
|
const originalModule = jest.requireActual('../../../../v2/transport/discord.api');
|
||||||
|
return {
|
||||||
|
...originalModule,
|
||||||
|
discordApiRequest: jest.fn(async function (method: string) {
|
||||||
|
if (method === 'POST') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test DiscordV2, message => sendAndWait', () => {
|
||||||
|
let discord: DiscordV2;
|
||||||
|
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
discord = new DiscordV2(versionDescription);
|
||||||
|
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||||
|
mockExecuteFunctions.helpers = {
|
||||||
|
constructExecutionMetaData: jest.fn(() => []),
|
||||||
|
returnJsonArray: jest.fn(() => []),
|
||||||
|
} as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send message and put execution to wait', async () => {
|
||||||
|
const items = [{ json: { data: 'test' } }];
|
||||||
|
mockExecuteFunctions.getNodeParameter.mockImplementation((key: string) => {
|
||||||
|
if (key === 'operation') return SEND_AND_WAIT_OPERATION;
|
||||||
|
if (key === 'resource') return 'message';
|
||||||
|
if (key === 'authentication') return 'botToken';
|
||||||
|
if (key === 'message') return 'my message';
|
||||||
|
if (key === 'subject') return '';
|
||||||
|
if (key === 'approvalOptions.values') return {};
|
||||||
|
if (key === 'responseType') return 'approval';
|
||||||
|
if (key === 'sendTo') return 'channel';
|
||||||
|
if (key === 'channelId') return 'channelID';
|
||||||
|
if (key === 'options.limitWaitTime.values') return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
mockExecuteFunctions.putExecutionToWait.mockImplementation();
|
||||||
|
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||||
|
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||||
|
|
||||||
|
mockExecuteFunctions.evaluateExpression.mockReturnValueOnce('http://localhost/waiting-webhook');
|
||||||
|
mockExecuteFunctions.evaluateExpression.mockReturnValueOnce('nodeID');
|
||||||
|
|
||||||
|
const result = await discord.execute.call(mockExecuteFunctions);
|
||||||
|
|
||||||
|
expect(result).toEqual([items]);
|
||||||
|
expect(transport.discordApiRequest).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockExecuteFunctions.putExecutionToWait).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(transport.discordApiRequest).toHaveBeenCalledWith(
|
||||||
|
'POST',
|
||||||
|
'/channels/channelID/messages',
|
||||||
|
{
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
label: 'Approve',
|
||||||
|
style: 5,
|
||||||
|
type: 2,
|
||||||
|
url: 'http://localhost/waiting-webhook/nodeID?approved=true',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
color: 5814783,
|
||||||
|
description:
|
||||||
|
'my message\n\n_This message was sent automatically with _[n8n](https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_instanceId)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
import { router } from './actions/router';
|
import { router } from './actions/router';
|
||||||
import { versionDescription } from './actions/versionDescription';
|
import { versionDescription } from './actions/versionDescription';
|
||||||
import { listSearch, loadOptions } from './methods';
|
import { listSearch, loadOptions } from './methods';
|
||||||
|
import { sendAndWaitWebhook } from '../../../utils/sendAndWait/utils';
|
||||||
|
|
||||||
export class DiscordV2 implements INodeType {
|
export class DiscordV2 implements INodeType {
|
||||||
description: INodeTypeDescription;
|
description: INodeTypeDescription;
|
||||||
|
@ -25,6 +26,8 @@ export class DiscordV2 implements INodeType {
|
||||||
loadOptions,
|
loadOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
webhook = sendAndWaitWebhook;
|
||||||
|
|
||||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
return await router.call(this);
|
return await router.call(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -463,3 +463,40 @@ export const filesFixedCollection: INodeProperties = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendToProperties: 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'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import type { INodeProperties } from 'n8n-workflow';
|
import { SEND_AND_WAIT_OPERATION, type INodeProperties } from 'n8n-workflow';
|
||||||
|
|
||||||
import * as deleteMessage from './deleteMessage.operation';
|
import * as deleteMessage from './deleteMessage.operation';
|
||||||
import * as get from './get.operation';
|
import * as get from './get.operation';
|
||||||
import * as getAll from './getAll.operation';
|
import * as getAll from './getAll.operation';
|
||||||
import * as react from './react.operation';
|
import * as react from './react.operation';
|
||||||
import * as send from './send.operation';
|
import * as send from './send.operation';
|
||||||
|
import * as sendAndWait from './sendAndWait.operation';
|
||||||
import { guildRLC } from '../common.description';
|
import { guildRLC } from '../common.description';
|
||||||
|
|
||||||
export { getAll, react, send, deleteMessage, get };
|
export { getAll, react, send, deleteMessage, get, sendAndWait };
|
||||||
|
|
||||||
export const description: INodeProperties[] = [
|
export const description: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
|
@ -52,6 +53,12 @@ export const description: INodeProperties[] = [
|
||||||
description: 'Send a message to a channel, thread, or member',
|
description: 'Send a message to a channel, thread, or member',
|
||||||
action: 'Send a message',
|
action: 'Send a message',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Send and Wait for Response',
|
||||||
|
value: SEND_AND_WAIT_OPERATION,
|
||||||
|
description: 'Send a message and wait for response',
|
||||||
|
action: 'Send message and wait for response',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
default: 'send',
|
default: 'send',
|
||||||
},
|
},
|
||||||
|
@ -69,4 +76,5 @@ export const description: INodeProperties[] = [
|
||||||
...send.description,
|
...send.description,
|
||||||
...deleteMessage.description,
|
...deleteMessage.description,
|
||||||
...get.description,
|
...get.description,
|
||||||
|
...sendAndWait.description,
|
||||||
];
|
];
|
||||||
|
|
|
@ -4,60 +4,23 @@ import type {
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||||
import {
|
import {
|
||||||
checkAccessToChannel,
|
|
||||||
parseDiscordError,
|
parseDiscordError,
|
||||||
prepareEmbeds,
|
prepareEmbeds,
|
||||||
prepareErrorData,
|
prepareErrorData,
|
||||||
prepareMultiPartForm,
|
|
||||||
prepareOptions,
|
prepareOptions,
|
||||||
|
sendDiscordMessage,
|
||||||
} from '../../helpers/utils';
|
} from '../../helpers/utils';
|
||||||
import { discordApiMultiPartRequest, discordApiRequest } from '../../transport';
|
|
||||||
import {
|
import {
|
||||||
embedsFixedCollection,
|
embedsFixedCollection,
|
||||||
filesFixedCollection,
|
filesFixedCollection,
|
||||||
textChannelRLC,
|
sendToProperties,
|
||||||
userRLC,
|
|
||||||
} from '../common.description';
|
} from '../common.description';
|
||||||
|
|
||||||
const properties: INodeProperties[] = [
|
const properties: INodeProperties[] = [
|
||||||
{
|
...sendToProperties,
|
||||||
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',
|
displayName: 'Message',
|
||||||
name: 'content',
|
name: 'content',
|
||||||
|
@ -157,90 +120,17 @@ export async function execute(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sendTo = this.getNodeParameter('sendTo', i) as string;
|
returnData.push(
|
||||||
|
...(await sendDiscordMessage.call(this, {
|
||||||
let channelId = '';
|
guildId,
|
||||||
|
userGuilds,
|
||||||
if (sendTo === 'user') {
|
isOAuth2,
|
||||||
const userId = this.getNodeParameter('userId', i, 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: i,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NodeOperationError(this.getNode(), error, {
|
|
||||||
itemIndex: i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: i },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sendTo === 'channel') {
|
|
||||||
channelId = this.getNodeParameter('channelId', i, undefined, {
|
|
||||||
extractValue: true,
|
|
||||||
}) as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOAuth2 && sendTo !== 'user') {
|
|
||||||
await checkAccessToChannel.call(this, channelId, userGuilds, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!channelId) {
|
|
||||||
throw new NodeOperationError(this.getNode(), 'Channel ID is required', { itemIndex: 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,
|
body,
|
||||||
);
|
items,
|
||||||
}
|
files,
|
||||||
|
itemIndex: i,
|
||||||
const executionData = this.helpers.constructExecutionMetaData(
|
})),
|
||||||
this.helpers.returnJsonArray(response),
|
|
||||||
{ itemData: { item: i } },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
returnData.push(...executionData);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = parseDiscordError.call(this, error, i);
|
const err = parseDiscordError.call(this, error, i);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import type {
|
||||||
|
IDataObject,
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { getSendAndWaitProperties } from '../../../../../utils/sendAndWait/utils';
|
||||||
|
import {
|
||||||
|
createSendAndWaitMessageBody,
|
||||||
|
parseDiscordError,
|
||||||
|
prepareErrorData,
|
||||||
|
sendDiscordMessage,
|
||||||
|
} from '../../helpers/utils';
|
||||||
|
import { sendToProperties } from '../common.description';
|
||||||
|
|
||||||
|
export const description: INodeProperties[] = getSendAndWaitProperties(
|
||||||
|
sendToProperties,
|
||||||
|
'message',
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
noButtonStyle: true,
|
||||||
|
defaultApproveLabel: '✓ Approve',
|
||||||
|
defaultDisapproveLabel: '✗ Decline',
|
||||||
|
},
|
||||||
|
).filter((p) => p.name !== 'subject');
|
||||||
|
|
||||||
|
export async function execute(
|
||||||
|
this: IExecuteFunctions,
|
||||||
|
guildId: string,
|
||||||
|
userGuilds: IDataObject[],
|
||||||
|
): Promise<INodeExecutionData[]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
const isOAuth2 = this.getNodeParameter('authentication', 0) === 'oAuth2';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendDiscordMessage.call(this, {
|
||||||
|
guildId,
|
||||||
|
userGuilds,
|
||||||
|
isOAuth2,
|
||||||
|
body: createSendAndWaitMessageBody(this),
|
||||||
|
items,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const err = parseDiscordError.call(this, error, 0);
|
||||||
|
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
return prepareErrorData.call(this, err, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import type { AllEntities } from 'n8n-workflow';
|
||||||
|
|
||||||
type NodeMap = {
|
type NodeMap = {
|
||||||
channel: 'get' | 'getAll' | 'create' | 'update' | 'deleteChannel';
|
channel: 'get' | 'getAll' | 'create' | 'update' | 'deleteChannel';
|
||||||
message: 'deleteMessage' | 'getAll' | 'get' | 'react' | 'send';
|
message: 'deleteMessage' | 'getAll' | 'get' | 'react' | 'send' | 'sendAndWait';
|
||||||
member: 'getAll' | 'roleAdd' | 'roleRemove';
|
member: 'getAll' | 'roleAdd' | 'roleRemove';
|
||||||
webhook: 'sendLegacy';
|
webhook: 'sendLegacy';
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import type { IDataObject, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
import type { IDataObject, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||||
import { NodeOperationError } from 'n8n-workflow';
|
import { NodeOperationError, SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
|
||||||
|
|
||||||
import * as channel from './channel';
|
import * as channel from './channel';
|
||||||
import * as member from './member';
|
import * as member from './member';
|
||||||
import * as message from './message';
|
import * as message from './message';
|
||||||
import type { Discord } from './node.type';
|
import type { Discord } from './node.type';
|
||||||
import * as webhook from './webhook';
|
import * as webhook from './webhook';
|
||||||
|
import { configureWaitTillDate } from '../../../../utils/sendAndWait/utils';
|
||||||
import { checkAccessToGuild } from '../helpers/utils';
|
import { checkAccessToGuild } from '../helpers/utils';
|
||||||
import { discordApiRequest } from '../transport';
|
import { discordApiRequest } from '../transport';
|
||||||
|
|
||||||
|
@ -46,6 +47,15 @@ export async function router(this: IExecuteFunctions) {
|
||||||
operation,
|
operation,
|
||||||
} as Discord;
|
} as Discord;
|
||||||
|
|
||||||
|
if (discord.resource === 'message' && discord.operation === SEND_AND_WAIT_OPERATION) {
|
||||||
|
returnData = await message[discord.operation].execute.call(this, guildId, userGuilds);
|
||||||
|
|
||||||
|
const waitTill = configureWaitTillDate(this);
|
||||||
|
|
||||||
|
await this.putExecutionToWait(waitTill);
|
||||||
|
return [returnData];
|
||||||
|
}
|
||||||
|
|
||||||
switch (discord.resource) {
|
switch (discord.resource) {
|
||||||
case 'channel':
|
case 'channel':
|
||||||
returnData = await channel[discord.operation].execute.call(this, guildId, userGuilds);
|
returnData = await channel[discord.operation].execute.call(this, guildId, userGuilds);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import * as channel from './channel';
|
||||||
import * as member from './member';
|
import * as member from './member';
|
||||||
import * as message from './message';
|
import * as message from './message';
|
||||||
import * as webhook from './webhook';
|
import * as webhook from './webhook';
|
||||||
|
import { sendAndWaitWebhooksDescription } from '../../../../utils/sendAndWait/descriptions';
|
||||||
|
|
||||||
export const versionDescription: INodeTypeDescription = {
|
export const versionDescription: INodeTypeDescription = {
|
||||||
displayName: 'Discord',
|
displayName: 'Discord',
|
||||||
|
@ -19,6 +20,7 @@ export const versionDescription: INodeTypeDescription = {
|
||||||
},
|
},
|
||||||
inputs: [NodeConnectionType.Main],
|
inputs: [NodeConnectionType.Main],
|
||||||
outputs: [NodeConnectionType.Main],
|
outputs: [NodeConnectionType.Main],
|
||||||
|
webhooks: sendAndWaitWebhooksDescription,
|
||||||
credentials: [
|
credentials: [
|
||||||
{
|
{
|
||||||
name: 'discordBotApi',
|
name: 'discordBotApi',
|
||||||
|
|
|
@ -8,10 +8,11 @@ import type {
|
||||||
INode,
|
INode,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { jsonParse, NodeOperationError } from 'n8n-workflow';
|
import { jsonParse, NodeApiError, NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { getSendAndWaitConfig } from '../../../../utils/sendAndWait/utils';
|
||||||
import { capitalize } from '../../../../utils/utilities';
|
import { capitalize } from '../../../../utils/utilities';
|
||||||
import { discordApiRequest } from '../transport';
|
import { discordApiMultiPartRequest, discordApiRequest } from '../transport';
|
||||||
|
|
||||||
export const createSimplifyFunction =
|
export const createSimplifyFunction =
|
||||||
(includedFields: string[]) =>
|
(includedFields: string[]) =>
|
||||||
|
@ -285,3 +286,141 @@ export async function setupChannelGetter(this: IExecuteFunctions, userGuilds: ID
|
||||||
return channelId;
|
return channelId;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendDiscordMessage(
|
||||||
|
this: IExecuteFunctions,
|
||||||
|
{
|
||||||
|
guildId,
|
||||||
|
userGuilds,
|
||||||
|
isOAuth2,
|
||||||
|
body,
|
||||||
|
items,
|
||||||
|
files = [],
|
||||||
|
itemIndex = 0,
|
||||||
|
}: {
|
||||||
|
guildId: string;
|
||||||
|
userGuilds: IDataObject[];
|
||||||
|
isOAuth2: boolean;
|
||||||
|
body: IDataObject;
|
||||||
|
items: INodeExecutionData[];
|
||||||
|
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, items, 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);
|
||||||
|
|
||||||
|
const instanceId = context.getInstanceId();
|
||||||
|
const attributionText = 'This message was sent automatically with ';
|
||||||
|
const link = `https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=${encodeURIComponent(
|
||||||
|
'n8n-nodes-base.telegram',
|
||||||
|
)}${instanceId ? '_' + instanceId : ''}`;
|
||||||
|
const 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: `${config.url}?approved=${option.value}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
|
@ -30,5 +30,5 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"alias": ["SMTP"]
|
"alias": ["SMTP", "email", "human", "form", "wait", "hitl", "approval"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,5 +17,6 @@
|
||||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlechat/"
|
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlechat/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"alias": ["human", "form", "wait", "hitl", "approval"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,5 +55,5 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"alias": ["email", "human", "form", "wait"]
|
"alias": ["email", "human", "form", "wait", "hitl", "approval"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,5 +18,5 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"alias": ["email"]
|
"alias": ["email", "human", "form", "wait", "hitl", "approval"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"subcategories": {
|
"subcategories": {
|
||||||
"HITL": ["Human in the Loop"]
|
"HITL": ["Human in the Loop"]
|
||||||
},
|
},
|
||||||
"alias": ["human", "form", "wait"],
|
"alias": ["human", "form", "wait", "hitl", "approval"],
|
||||||
"resources": {
|
"resources": {
|
||||||
"credentialDocumentation": [
|
"credentialDocumentation": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
"subcategories": {
|
"subcategories": {
|
||||||
"HITL": ["Human in the Loop"]
|
"HITL": ["Human in the Loop"]
|
||||||
},
|
},
|
||||||
|
"alias": ["human", "form", "wait", "hitl", "approval"],
|
||||||
"resources": {
|
"resources": {
|
||||||
"credentialDocumentation": [
|
"credentialDocumentation": [
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue