diff --git a/packages/nodes-base/credentials/CustomerIoApi.credentials.ts b/packages/nodes-base/credentials/CustomerIoApi.credentials.ts new file mode 100644 index 0000000000..2cc92c81af --- /dev/null +++ b/packages/nodes-base/credentials/CustomerIoApi.credentials.ts @@ -0,0 +1,19 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class CustomerIoApi implements ICredentialType { + name = 'customerIoApi'; + displayName = 'Customer.io API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + + ]; +} diff --git a/packages/nodes-base/nodes/Aws/GenericFunctions.ts b/packages/nodes-base/nodes/Aws/GenericFunctions.ts index 6ffa92c8ee..ef334b1d67 100644 --- a/packages/nodes-base/nodes/Aws/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Aws/GenericFunctions.ts @@ -19,8 +19,8 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I const endpoint = `${service}.${credentials.region}.amazonaws.com`; // Sign AWS API request with the user credentials - const signOpts = {headers: headers || {}, host: endpoint, method, path, body}; - sign(signOpts, {accessKeyId: `${credentials.accessKeyId}`, secretAccessKey: `${credentials.secretAccessKey}`}); + const signOpts = { headers: headers || {}, host: endpoint, method, path, body }; + sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`, secretAccessKey: `${credentials.secretAccessKey}` }); const options: OptionsWithUri = { headers: signOpts.headers, diff --git a/packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts b/packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts new file mode 100644 index 0000000000..5db8a4f899 --- /dev/null +++ b/packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts @@ -0,0 +1,328 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + INodeTypeDescription, + INodeType, + IDataObject, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + apiRequest, + eventExists, +} from './GenericFunctions'; + +interface IEvent{ + customer?: IDataObject; + email?: IDataObject; + push?: IDataObject; + slack?: IDataObject; + sms?: IDataObject; + webhook?: IDataObject; +} + +export class CustomerIoTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Customer.io Trigger', + name: 'customerIo', + group: ['trigger'], + icon: 'file:customer.Io.png', + version: 1, + description: 'Starts the workflow on a Customer.io update. (Beta)', + defaults: { + name: 'Customer.io Trigger', + color: '#00FF00', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'customerIoApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + default: [], + description: 'The events that can trigger the webhook and whether they are enabled.', + options: [ + { + name: 'Customer Subscribed', + value: 'customer.subscribed', + description: 'Whether the webhook is triggered when a list subscriber is added.', + }, + { + name: 'Customer Unsubscribe', + value: 'customer.unsubscribed', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Email Attempted', + value: 'email.attempted', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Email Bounced', + value: 'email.bounced', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Email clicked', + value: 'email.clicked', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Email converted', + value: 'email.converted', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Email delivered', + value: 'email.delivered', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Email drafted', + value: 'email.drafted', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Email failed', + value: 'email.failed', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Email opened', + value: 'email.opened', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Email sent', + value: 'email.sent', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Email spammed', + value: 'email.spammed', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Push attempted', + value: 'push.attempted', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Push bounced', + value: 'push.bounced', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Push clicked', + value: 'push.clicked', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Push delivered', + value: 'push.delivered', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Push drafted', + value: 'push.drafted', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Push failed', + value: 'push.failed', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Push opened', + value: 'push.opened', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Push sent', + value: 'push.sent', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Slack attempted', + value: 'slack.attempted', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Slack clicked', + value: 'slack.clicked', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Slack drafted', + value: 'slack.drafted', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Slack failed', + value: 'slack.failed', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Slack sent', + value: 'slack.sent', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS attempted', + value: 'sms.attempted', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS bounced', + value: 'sms.bounced', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS clicked', + value: 'sms.clicked', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS delivered', + value: 'sms.delivered', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS drafted', + value: 'sms.drafted', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS failed', + value: 'sms.failed', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS sent', + value: 'sms.sent', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + ], + }, + ], + }; + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + + const webhookData = this.getWorkflowStaticData('node'); + + const currentEvents = this.getNodeParameter('events', []) as string[]; + + const endpoint = '/reporting_webhooks'; + + let { reporting_webhooks: webhooks } = await apiRequest.call(this, 'GET', endpoint, {}); + + if (webhooks === null) { + webhooks = []; + } + + for (const webhook of webhooks) { + if (webhook.endpoint === webhookUrl && + eventExists(currentEvents, webhook.events)) { + webhookData.webhookId = webhook.id; + return true; + } + } + + return false; + }, + async create(this: IHookFunctions): Promise { + let webhook; + const webhookUrl = this.getNodeWebhookUrl('default'); + const events = this.getNodeParameter('events', []) as string[]; + + const endpoint = '/reporting_webhooks'; + + const data: IEvent = { + customer: {}, + email: {}, + push: {}, + slack: {}, + sms: {}, + webhook: {}, + }; + + for (const event of events) { + const option = event.split('.')[1]; + if (event.startsWith('customer')) { + data.customer![option] = true; + } + if (event.startsWith('email')) { + data.email![option] = true; + } + if (event.startsWith('push')) { + data.push![option] = true; + } + if (event.startsWith('slack')) { + data.slack![option] = true; + } + if (event.startsWith('sms')) { + data.sms![option] = true; + } + if (event.startsWith('webhook')) { + data.webhook![option] = true; + } + } + const body = { + endpoint: webhookUrl, + events: data, + }; + + webhook = await apiRequest.call(this, 'POST', endpoint, body); + + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = webhook.id as string; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + + if (webhookData.webhookId !== undefined) { + const endpoint = `/reporting_webhooks/${webhookData.webhookId}`; + try { + await apiRequest.call(this, 'DELETE', endpoint, {}); + } catch (e) { + return false; + } + delete webhookData.webhookId; + } + return true; + }, + } + }; + + async webhook(this: IWebhookFunctions): Promise { + const bodyData = this.getBodyData(); + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts b/packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts new file mode 100644 index 0000000000..48e475f533 --- /dev/null +++ b/packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts @@ -0,0 +1,65 @@ +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + OptionsWithUri, +} from 'request'; + +import { + IDataObject, +} from 'n8n-workflow'; + +import { + get, +} from 'lodash'; + +export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('customerIoApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + query = query || {}; + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${credentials.apiKey}`, + }, + method, + body, + qs: query, + uri: `https://beta-api.customer.io/v1/api${endpoint}`, + json: true, + }; + try { + return await this.helpers.request!(options); + } catch (error) { + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The Customer.io credentials are not valid!'); + } + + if (error.response && error.response.body && error.response.body.error_code) { + // Try to return the error prettier + const errorBody = error.response.body; + throw new Error(`Customer.io error response [${errorBody.error_code}]: ${errorBody.description}`); + } + + // Expected error data did not get returned so throw the actual error + throw error; + } +} + +export function eventExists (currentEvents : string[], webhookEvents: IDataObject) { + for (const currentEvent of currentEvents) { + if (get(webhookEvents, `${currentEvent.split('.')[0]}.${currentEvent.split('.')[1]}`) !== true) { + return false; + } + } + return true; +} diff --git a/packages/nodes-base/nodes/CustomerIo/customer.Io.png b/packages/nodes-base/nodes/CustomerIo/customer.Io.png new file mode 100644 index 0000000000..7c5ae3010a Binary files /dev/null and b/packages/nodes-base/nodes/CustomerIo/customer.Io.png differ diff --git a/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts b/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts index 9fbd80a0f5..870078dc0b 100644 --- a/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts +++ b/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts @@ -1,19 +1,19 @@ import { IHookFunctions, IWebhookFunctions, - } from 'n8n-core'; +} from 'n8n-core'; - import { +import { IDataObject, INodeTypeDescription, INodeType, IWebhookResponseData, ILoadOptionsFunctions, INodePropertyOptions, - } from 'n8n-workflow'; - import { +} from 'n8n-workflow'; +import { mailchimpApiRequest, - } from './GenericFunctions'; +} from './GenericFunctions'; export class MailchimpTrigger implements INodeType { description: INodeTypeDescription = { @@ -24,8 +24,8 @@ export class MailchimpTrigger implements INodeType { version: 1, description: 'Handle Mailchimp events via webhooks', defaults: { - name: 'Mailchimp Trigger', - color: '#32325d', + name: 'Mailchimp Trigger', + color: '#32325d', }, inputs: [], outputs: ['main'], @@ -285,8 +285,8 @@ export class MailchimpTrigger implements INodeType { } // @ts-ignore if (!webhookData.events.includes(req.body.type) - // @ts-ignore - && !webhookData.sources.includes(req.body.type)) { + // @ts-ignore + && !webhookData.sources.includes(req.body.type)) { return {}; } return { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 1a0c4905c2..6f22c97458 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -48,6 +48,7 @@ "dist/credentials/CodaApi.credentials.js", "dist/credentials/CopperApi.credentials.js", "dist/credentials/CalendlyApi.credentials.js", + "dist/credentials/CustomerIoApi.credentials.js", "dist/credentials/CrateDb.credentials.js", "dist/credentials/DisqusApi.credentials.js", "dist/credentials/DriftApi.credentials.js", @@ -197,6 +198,7 @@ "dist/nodes/CrateDb/CrateDb.node.js", "dist/nodes/Cron.node.js", "dist/nodes/Crypto.node.js", + "dist/nodes/CustomerIo/CustomerIoTrigger.node.js", "dist/nodes/DateTime.node.js", "dist/nodes/Discord/Discord.node.js", "dist/nodes/Disqus/Disqus.node.js",