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..5a3f7c2ef9 --- /dev/null +++ b/packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts @@ -0,0 +1,330 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; +import { + INodeTypeDescription, + INodeType, + IDataObject, + IWebhookResponseData, +} from 'n8n-workflow'; +import { + apiRequest, +} from './GenericFunctions'; + +export class CustomerIoTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Customer.io Trigger', + name: 'customerIo', + group: ['trigger'], + icon: 'file:customer.Io.png', + version: 1, + subtitle: '=Updates: {{$parameter["updates"].join(", ")}}', + description: 'Starts the workflow on a Customer.io update.', + 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: 'slack.attempted', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS bounced', + value: 'slack.bounced', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS clicked', + value: 'slack.clicked', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS delivered', + value: 'slack.delivered', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS drafted', + value: 'slack.drafted', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS failed', + value: 'slack.failed', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'SMS sent', + value: 'slack.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 webhookData = this.getWorkflowStaticData('node'); + console.log("in checkexists function") + if (webhookData.webhookId === undefined) { + // No webhook id is set so no webhook can exist + return false; + } + const endpoint = `/reporting_webhooks/${webhookData.webhookId}`; + try { + await apiRequest.call(this, 'GET', endpoint, {}); + } catch (err) { + if (err.statusCode === 404) { + return false; + } + throw new Error(`Customer.io Error: ${err}`); + } + + return true; + }, + async create(this: IHookFunctions): Promise { + let webhook; + const webhookUrl = this.getNodeWebhookUrl('default'); + const events = this.getNodeParameter('events', []) as string[]; + + const endpoint1 = '/reporting_webhooks'; + for (const event of events) { + var obj = event.split('.'); + + // var obj2 = JSON.stringify(obj); + // var key = obj[0]; //push + // var val = JSON.stringify(obj[1]); //attempted + + // var obj1: { obj2: boolean; } = { obj2: true }; //{'attempted':true} + + + } + const body = { + endpoint: webhookUrl, + // events: events.reduce((object, currentValue) => { + // // @ts-ignore + // var obj = currentValue.split('.'); + + // //object[currentValue] = true; + + // return object; + // }, {}), + "events": { + "customer": { + "subscribed": false, + "unsubscribed": true + }, + }, + }; + console.log(body); + try { + webhook = await apiRequest.call(this, 'POST', endpoint1, body); + } catch (e) { + throw e; + } + if (webhook.id === undefined) { + return false; + } + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = webhook.id as string; + webhookData.events = events; + 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, endpoint, 'DELETE', {}); + } catch (e) { + return false; + } + delete webhookData.webhookId; + delete webhookData.events; + + } + return true; + }, + } + }; + + + + async webhook(this: IWebhookFunctions): Promise { + const bodyData = this.getBodyData(); + const webhookData = this.getWorkflowStaticData('node') as IDataObject; + const req = this.getRequestObject(); + if (req.body.id !== webhookData.id) { + return {}; + } + // @ts-ignore + if (!webhookData.events.includes(req.body.type) + // @ts-ignore + && !webhookData.sources.includes(req.body.type)) { + return {}; + } + 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..5474ec3397 --- /dev/null +++ b/packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts @@ -0,0 +1,48 @@ +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { OptionsWithUri } from 'request'; +import { IDataObject } from 'n8n-workflow'; + +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, + }; + console.log(options); + 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; + } +} 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 c19d8e2349..ddae92ebcd 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -45,6 +45,7 @@ "dist/credentials/CodaApi.credentials.js", "dist/credentials/CopperApi.credentials.js", "dist/credentials/CalendlyApi.credentials.js", + "dist/credentials/CustomerIoApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", "dist/credentials/DriftApi.credentials.js", "dist/credentials/DriftOAuth2Api.credentials.js", @@ -176,6 +177,7 @@ "dist/nodes/Copper/CopperTrigger.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", @@ -202,7 +204,7 @@ "dist/nodes/Google/Calendar/GoogleCalendar.node.js", "dist/nodes/Google/Drive/GoogleDrive.node.js", "dist/nodes/Google/Sheet/GoogleSheets.node.js", - "dist/nodes/Google/Task/GoogleTasks.node.js", + "dist/nodes/Google/Task/GoogleTasks.node.js", "dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/Gumroad/GumroadTrigger.node.js", "dist/nodes/Harvest/Harvest.node.js",