feat(Mailhook Node): New node

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-10-30 14:55:06 +01:00
parent 497d637fc5
commit c28ccb3e5c
No known key found for this signature in database
22 changed files with 170 additions and 53 deletions

View file

@ -48,6 +48,7 @@ export interface FrontendSettings {
};
timezone: string;
urlBaseWebhook: string;
domain: string;
urlBaseEditor: string;
versionCli: string;
nodeJsVersion: string;

View file

@ -100,6 +100,7 @@ export class FrontendService {
workflowCallerPolicyDefaultOption: this.globalConfig.workflows.callerPolicyDefaultOption,
timezone: this.globalConfig.generic.timezone,
urlBaseWebhook: this.urlService.getWebhookBaseUrl(),
domain: this.urlService.getDomain(),
urlBaseEditor: instanceBaseUrl,
binaryDataMode: config.getEnv('binaryDataManager.mode'),
nodeJsVersion: process.version.replace(/^v/, ''),
@ -250,6 +251,7 @@ export class FrontendService {
// Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel`
const instanceBaseUrl = this.urlService.getInstanceBaseUrl();
this.settings.urlBaseWebhook = this.urlService.getWebhookBaseUrl();
this.settings.domain = this.urlService.getDomain();
this.settings.urlBaseEditor = instanceBaseUrl;
this.settings.oauthCallbackUrls = {
oauth1: `${instanceBaseUrl}/${restEndpoint}/oauth1-credential/callback`,

View file

@ -21,6 +21,10 @@ export class UrlService {
return urlBaseWebhook;
}
getDomain() {
return process.env.DOMAIN ?? new URL(this.getInstanceBaseUrl()).hostname;
}
/** Return the n8n instance base URL without trailing slash */
getInstanceBaseUrl(): string {
const n8nBaseUrl = config.getEnv('editorBaseUrl') || this.getWebhookBaseUrl();

View file

@ -202,7 +202,7 @@ export class UserManagementMailer {
private get basePayload() {
const baseUrl = this.urlService.getInstanceBaseUrl();
const domain = new URL(baseUrl).hostname;
const domain = this.urlService.getDomain();
return { baseUrl, domain };
}
}

View file

@ -971,7 +971,8 @@ export async function getBase(
currentNodeParameters?: INodeParameters,
executionTimeoutTimestamp?: number,
): Promise<IWorkflowExecuteAdditionalData> {
const urlBaseWebhook = Container.get(UrlService).getWebhookBaseUrl();
const urlService = Container.get(UrlService);
const urlBaseWebhook = urlService.getWebhookBaseUrl();
const globalConfig = Container.get(GlobalConfig);
@ -988,6 +989,7 @@ export async function getBase(
webhookBaseUrl: urlBaseWebhook + globalConfig.endpoints.webhook,
webhookWaitingBaseUrl: urlBaseWebhook + globalConfig.endpoints.webhookWaiting,
webhookTestBaseUrl: urlBaseWebhook + globalConfig.endpoints.webhookTest,
domain: urlService.getDomain(),
currentNodeParameters,
executionTimeoutTimestamp,
userId,

View file

@ -4572,6 +4572,9 @@ export function getExecuteHookFunctions(
webhookData?.isTest,
);
},
getDomain(): string {
return additionalData.domain;
},
getWebhookName(): string {
if (webhookData === undefined) {
throw new ApplicationError('Only supported in webhook functions');

View file

@ -64,6 +64,7 @@
"luxon": "catalog:",
"n8n-design-system": "workspace:*",
"n8n-workflow": "workspace:*",
"nanoid": "catalog:",
"pinia": "^2.2.4",
"prettier": "^3.3.3",
"qrcode.vue": "^3.3.4",

View file

@ -8,7 +8,7 @@ import type {
IVersionNotificationSettings,
} from '@n8n/api-types';
import type { Scope } from '@n8n/permissions';
import type { IMenuItem, NodeCreatorTag } from 'n8n-design-system';
import type { NodeCreatorTag } from 'n8n-design-system';
import type {
GenericValue,
IConnections,
@ -883,6 +883,7 @@ export interface RootState {
pushRef: string;
urlBaseWebhook: string;
urlBaseEditor: string;
domain: string;
instanceId: string;
binaryDataMode: 'default' | 'filesystem' | 's3';
}
@ -890,51 +891,6 @@ export interface RootState {
export interface NodeMetadataMap {
[nodeName: string]: INodeMetadata;
}
export interface IRootState {
activeExecutions: IExecutionsCurrentSummaryExtended[];
activeWorkflows: string[];
activeActions: string[];
activeCredentialType: string | null;
baseUrl: string;
defaultLocale: string;
endpointForm: string;
endpointFormTest: string;
endpointFormWaiting: string;
endpointWebhook: string;
endpointWebhookTest: string;
endpointWebhookWaiting: string;
executionId: string | null;
executingNode: string[];
executionWaitingForWebhook: boolean;
pushConnectionActive: boolean;
saveDataErrorExecution: string;
saveDataSuccessExecution: string;
saveManualExecutions: boolean;
timezone: string;
stateIsDirty: boolean;
executionTimeout: number;
maxExecutionTimeout: number;
versionCli: string;
oauthCallbackUrls: object;
n8nMetadata: object;
workflowExecutionData: IExecutionResponse | null;
workflowExecutionPairedItemMappings: { [itemId: string]: Set<string> };
lastSelectedNode: string | null;
lastSelectedNodeOutputIndex: number | null;
nodeViewOffsetPosition: XYPosition;
nodeViewMoveInProgress: boolean;
selectedNodes: INodeUi[];
pushRef: string;
urlBaseEditor: string;
urlBaseWebhook: string;
workflow: IWorkflowDb;
workflowsById: IWorkflowsMap;
sidebarMenuItems: IMenuItem[];
instanceId: string;
nodeMetadata: NodeMetadataMap;
subworkflowExecutionError: Error | null;
binaryDataMode: string;
}
export interface CommunityPackageMap {
[name: string]: PublicInstalledPackage;

View file

@ -4,6 +4,7 @@ import { useToast } from '@/composables/useToast';
import {
CHAT_TRIGGER_NODE_TYPE,
FORM_TRIGGER_NODE_TYPE,
MAILHOOK_TRIGGER_NODE_TYPE,
OPEN_URL_PANEL_TRIGGER_NODE_TYPES,
PRODUCTION_ONLY_TRIGGER_NODE_TYPES,
} from '@/constants';
@ -95,6 +96,18 @@ const baseText = computed(() => {
copyMessage: i18n.baseText('nodeWebhooks.showMessage.message.formTrigger'),
};
case MAILHOOK_TRIGGER_NODE_TYPE:
return {
toggleTitle: i18n.baseText('nodeWebhooks.webhookUrls.mailhookTrigger'),
clickToDisplay: i18n.baseText('nodeWebhooks.clickToDisplayWebhookUrls.mailhookTrigger'),
clickToHide: i18n.baseText('nodeWebhooks.clickToHideWebhookUrls.mailhookTrigger'),
clickToCopy: i18n.baseText('nodeWebhooks.clickToCopyWebhookUrls.mailhookTrigger'),
testUrl: i18n.baseText('nodeWebhooks.mailhook.testEmailId'),
productionUrl: i18n.baseText('nodeWebhooks.mailhook.productionEmailId'),
copyTitle: i18n.baseText('nodeWebhooks.showMessage.title.mailhookTrigger'),
copyMessage: i18n.baseText('nodeWebhooks.showMessage.message.mailhookTrigger'),
};
default:
return {
toggleTitle: i18n.baseText('nodeWebhooks.webhookUrls'),
@ -127,6 +140,9 @@ function copyWebhookUrl(webhookData: IWebhookDescription): void {
function getWebhookUrlDisplay(webhookData: IWebhookDescription): string {
if (props.node) {
if (props.node.type === MAILHOOK_TRIGGER_NODE_TYPE) {
return workflowHelpers.getMailhookEmailId(props.node);
}
return workflowHelpers.getWebhookUrl(
webhookData,
props.node,

View file

@ -1085,7 +1085,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
function resolveNodeWebhook(node: INodeUi, nodeTypeDescription: INodeTypeDescription) {
if (nodeTypeDescription.webhooks?.length) {
node.webhookId = uuid();
nodeHelpers.assignWebhookId(node);
}
// if it's a webhook and the path is empty set the UUID as the default path
@ -1657,7 +1657,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
(n) => n.webhookId === node.webhookId,
);
if (isDuplicate) {
node.webhookId = uuid();
nodeHelpers.assignWebhookId(node);
if (node.parameters.path) {
node.parameters.path = node.webhookId as string;

View file

@ -1,11 +1,13 @@
import { ref, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { nanoid } from 'nanoid';
import { v4 as uuid } from 'uuid';
import type { Connection, ConnectionDetachedParams } from '@jsplumb/core';
import { useHistoryStore } from '@/stores/history.store';
import {
CUSTOM_API_CALL_KEY,
FORM_TRIGGER_NODE_TYPE,
MAILHOOK_TRIGGER_NODE_TYPE,
NODE_OUTPUT_DEFAULT_KEY,
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
SPLIT_IN_BATCHES_NODE_TYPE,
@ -1247,6 +1249,10 @@ export function useNodeHelpers() {
canvasStore.jsPlumbInstance?.setSuspendDrawing(false, true);
}
function assignWebhookId(node: INodeUi) {
node.webhookId = node.type === MAILHOOK_TRIGGER_NODE_TYPE ? nanoid(16).toLowerCase() : uuid();
}
return {
hasProxyAuth,
isCustomApiCallSelected,
@ -1281,5 +1287,6 @@ export function useNodeHelpers() {
removeConnectionByConnectionInfo,
addPinDataConnections,
removePinDataConnections,
assignWebhookId,
};
}

View file

@ -706,6 +706,10 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
return NodeHelpers.getNodeWebhookUrl(baseUrl, workflowId, node, path, isFullPath);
}
function getMailhookEmailId(node: INodeUi) {
return `${node.webhookId}@${rootStore.domain}`;
}
/**
* Returns a copy of provided node parameters with added resolvedExpressionValue
* @param nodeParameters
@ -1200,6 +1204,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
getNodeDataToSave,
getWebhookExpressionValue,
getWebhookUrl,
getMailhookEmailId,
resolveExpression,
updateWorkflow,
saveCurrentWorkflow,

View file

@ -196,6 +196,7 @@ export const SIMULATE_NODE_TYPE = 'n8n-nodes-base.simulate';
export const SIMULATE_TRIGGER_NODE_TYPE = 'n8n-nodes-base.simulateTrigger';
export const AI_TRANSFORM_NODE_TYPE = 'n8n-nodes-base.aiTransform';
export const FORM_NODE_TYPE = 'n8n-nodes-base.form';
export const MAILHOOK_TRIGGER_NODE_TYPE = 'n8n-nodes-base.mailhook';
export const CREDENTIAL_ONLY_NODE_PREFIX = 'n8n-creds-base';
export const CREDENTIAL_ONLY_HTTP_NODE_VERSION = 4.1;
@ -227,11 +228,15 @@ export const OPEN_URL_PANEL_TRIGGER_NODE_TYPES = [
WEBHOOK_NODE_TYPE,
FORM_TRIGGER_NODE_TYPE,
CHAT_TRIGGER_NODE_TYPE,
MAILHOOK_TRIGGER_NODE_TYPE,
];
export const LIST_LIKE_NODE_OPERATIONS = ['getAll', 'getMany', 'read', 'search'];
export const PRODUCTION_ONLY_TRIGGER_NODE_TYPES = [CHAT_TRIGGER_NODE_TYPE];
export const PRODUCTION_ONLY_TRIGGER_NODE_TYPES = [
CHAT_TRIGGER_NODE_TYPE,
MAILHOOK_TRIGGER_NODE_TYPE,
];
// Node creator
export const NODE_CREATOR_OPEN_SOURCES: Record<

View file

@ -1343,23 +1343,31 @@
"nodeWebhooks.clickToCopyWebhookUrls": "Click to copy webhook URLs",
"nodeWebhooks.clickToCopyWebhookUrls.formTrigger": "Click to copy Form URL",
"nodeWebhooks.clickToCopyWebhookUrls.chatTrigger": "Click to copy Chat URL",
"nodeWebhooks.clickToCopyWebhookUrls.mailhookTrigger": "Click to copy Email ID",
"nodeWebhooks.clickToDisplayWebhookUrls": "Click to display webhook URLs",
"nodeWebhooks.clickToDisplayWebhookUrls.formTrigger": "Click to display Form URL",
"nodeWebhooks.clickToDisplayWebhookUrls.chatTrigger": "Click to display Chat URL",
"nodeWebhooks.clickToDisplayWebhookUrls.mailhookTrigger": "Click to display Email IDs",
"nodeWebhooks.clickToHideWebhookUrls": "Click to hide webhook URLs",
"nodeWebhooks.clickToHideWebhookUrls.formTrigger": "Click to hide Form URL",
"nodeWebhooks.clickToHideWebhookUrls.chatTrigger": "Click to hide Chat URL",
"nodeWebhooks.clickToHideWebhookUrls.mailhookTrigger": "Click to hide Mailhook Email IDs",
"nodeWebhooks.invalidExpression": "[INVALID EXPRESSION]",
"nodeWebhooks.mailhook.testEmailId": "Test Email ID",
"nodeWebhooks.mailhook.productionEmailId": "Production Email ID",
"nodeWebhooks.productionUrl": "Production URL",
"nodeWebhooks.showMessage.title": "URL copied",
"nodeWebhooks.showMessage.title.formTrigger": "Form URL copied",
"nodeWebhooks.showMessage.title.chatTrigger": "Chat URL copied",
"nodeWebhooks.showMessage.title.mailhookTrigger": "Mailhook Email ID copied",
"nodeWebhooks.showMessage.message.formTrigger": "Form submissions made via this URL will trigger the workflow when it's activated",
"nodeWebhooks.showMessage.message.chatTrigger": "Chat submissions made via this URL will trigger the workflow when it's activated",
"nodeWebhooks.showMessage.message.mailhookTrigger": "Incoming emails on this Email ID will trigger the workflow when it's activated",
"nodeWebhooks.testUrl": "Test URL",
"nodeWebhooks.webhookUrls": "Webhook URLs",
"nodeWebhooks.webhookUrls.formTrigger": "Form URLs",
"nodeWebhooks.webhookUrls.chatTrigger": "Chat URL",
"nodeWebhooks.webhookUrls.mailhookTrigger": "Mailhook Email IDs",
"onboardingWorkflow.stickyContent": "## 👇 Get started faster \nLightning tour of the key concepts [4 min] \n\n[![n8n quickstart video](/static/quickstart_thumbnail.png#full-width)](https://www.youtube.com/watch?v=1MwSoB0gnM4)",
"openWorkflow.workflowImportError": "Could not import workflow",
"openWorkflow.workflowNotFoundError": "Could not find workflow",

View file

@ -30,6 +30,7 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
pushRef: randomString(10).toLowerCase(),
urlBaseWebhook: 'http://localhost:5678/',
urlBaseEditor: 'http://localhost:5678',
domain: 'localhost',
instanceId: '',
binaryDataMode: 'default',
});
@ -54,6 +55,8 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
() => `${state.value.urlBaseEditor}${state.value.endpointWebhookWaiting}`,
);
const domain = computed(() => state.value.domain);
const pushRef = computed(() => state.value.pushRef);
const binaryDataMode = computed(() => state.value.binaryDataMode);
@ -142,6 +145,10 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
state.value.endpointWebhookWaiting = endpointWebhookWaiting;
};
const setDomain = (domain: string) => {
state.value.domain = domain;
};
const setTimezone = (timezone: string) => {
state.value.timezone = timezone;
setGlobalState({ defaultTimezone: timezone });
@ -189,6 +196,7 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
webhookUrl,
webhookTestUrl,
webhookWaitingUrl,
domain,
restUrl,
restCloudApiContext,
restApiContext,
@ -213,6 +221,7 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
setEndpointWebhook,
setEndpointWebhookTest,
setEndpointWebhookWaiting,
setDomain,
setTimezone,
setExecutionTimeout,
setMaxExecutionTimeout,

View file

@ -243,6 +243,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
rootStore.setEndpointWebhook(fetchedSettings.endpointWebhook);
rootStore.setEndpointWebhookTest(fetchedSettings.endpointWebhookTest);
rootStore.setEndpointWebhookWaiting(fetchedSettings.endpointWebhookWaiting);
rootStore.setDomain(fetchedSettings.domain);
rootStore.setTimezone(fetchedSettings.timezone);
rootStore.setExecutionTimeout(fetchedSettings.executionTimeout);
rootStore.setMaxExecutionTimeout(fetchedSettings.maxExecutionTimeout);

View file

@ -1923,7 +1923,7 @@ export default defineComponent({
this.workflowHelpers.getCurrentWorkflow().nodes,
).some((n) => n.webhookId === node.webhookId);
if (isDuplicate) {
node.webhookId = uuid();
this.nodeHelpers.assignWebhookId(node);
if (node.parameters.path) {
node.parameters.path = node.webhookId as string;
@ -2389,7 +2389,7 @@ export default defineComponent({
newNodeData.name = this.uniqueNodeName(localizedName);
if (nodeTypeData.webhooks?.length) {
newNodeData.webhookId = uuid();
this.nodeHelpers.assignWebhookId(newNodeData);
}
await this.nodeHelpers.addNodes([newNodeData], undefined, trackHistory);

View file

@ -0,0 +1,87 @@
import {
IDataObject,
IWebhookFunctions,
INodeTypeDescription,
INodeType,
IWebhookResponseData,
NodeConnectionType,
IHookFunctions,
} from 'n8n-workflow';
// TODO: this should be picked up from a config object
const baseUrl = 'http://localhost:8080/rest/mailhooks';
const apiRequest = async (
context: IHookFunctions,
operation: 'checkExists' | 'create' | 'delete',
) => {
const { webhookId } = context.getNode();
const domain = context.getDomain();
const method = operation === 'checkExists' ? 'GET' : operation === 'create' ? 'POST' : 'DELETE';
let url = baseUrl;
if (operation !== 'create') {
url += `/${webhookId}@${domain}`;
}
const response = await context.helpers.httpRequest({
url,
method,
returnFullResponse: true,
ignoreHttpStatusErrors: true,
});
return response.statusCode === 204;
};
export class Mailhook implements INodeType {
description: INodeTypeDescription = {
displayName: 'Mailhook',
name: 'mailhook',
icon: 'file:mailhook.svg',
group: ['trigger'],
version: 1,
description: 'Start a workflow on a Mailhook trigger',
defaults: {
name: 'Mailhook',
color: '#4363AE',
},
inputs: [],
outputs: [NodeConnectionType.Main],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
ndvHideMethod: true,
},
],
properties: [],
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
return await apiRequest(this, 'checkExists');
},
async create(this: IHookFunctions): Promise<boolean> {
return await apiRequest(this, 'create');
},
async delete(this: IHookFunctions): Promise<boolean> {
return await apiRequest(this, 'delete');
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
// The data to return and so start the workflow with
const returnData: IDataObject[] = [];
returnData.push({
headers: this.getHeaderData(),
params: this.getParamsData(),
query: this.getQueryData(),
body: this.getBodyData(),
});
return {
workflowData: [this.helpers.returnJsonArray(returnData)],
};
}
}

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
<path d="M 7.885 9.683 L 20.885 19.683 L 33.885 9.683" stroke="#727db5" stroke-width="2" fill="transparent"/>
<path d="M 8.011 21.45 L 21.011 31.45 L 34.011 21.45" stroke="#727db5" stroke-width="2" fill="transparent"/>
</svg>

After

Width:  |  Height:  |  Size: 314 B

View file

@ -604,6 +604,7 @@
"dist/nodes/Mailcheck/Mailcheck.node.js",
"dist/nodes/Mailchimp/Mailchimp.node.js",
"dist/nodes/Mailchimp/MailchimpTrigger.node.js",
"dist/nodes/Mailhook/Mailhook.node.js",
"dist/nodes/MailerLite/MailerLite.node.js",
"dist/nodes/MailerLite/MailerLiteTrigger.node.js",
"dist/nodes/Mailgun/Mailgun.node.js",

View file

@ -1111,6 +1111,7 @@ export interface IHookFunctions
getWebhookName(): string;
getWebhookDescription(name: string): IWebhookDescription | undefined;
getNodeWebhookUrl: (name: string) => string | undefined;
getDomain(): string;
getNodeParameter(
parameterName: string,
fallbackValue?: any,
@ -2306,6 +2307,7 @@ export interface IWorkflowExecuteAdditionalData {
webhookBaseUrl: string;
webhookWaitingBaseUrl: string;
webhookTestBaseUrl: string;
domain: string;
currentNodeParameters?: INodeParameters;
executionTimeoutTimestamp?: number;
userId?: string;

View file

@ -1444,6 +1444,9 @@ importers:
n8n-workflow:
specifier: workspace:*
version: link:../workflow
nanoid:
specifier: 'catalog:'
version: 3.3.6
pinia:
specifier: ^2.2.4
version: 2.2.4(typescript@5.6.2)(vue@3.5.11(typescript@5.6.2))