From 7d625572c1f7d0dac24859fb38d141698558787a Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Tue, 30 Jun 2020 13:35:31 -0700 Subject: [PATCH] add basic customer.io trigger --- .../credentials/CustomerIoApi.credentials.ts | 19 + .../nodes-base/nodes/Aws/GenericFunctions.ts | 4 +- .../CustomerIo/CustomerIoTrigger.node.ts | 330 ++++++++++++++++++ .../nodes/CustomerIo/GenericFunctions.ts | 48 +++ .../nodes/CustomerIo/customer.Io.png | Bin 0 -> 1790 bytes .../nodes/Mailchimp/MailchimpTrigger.node.ts | 18 +- packages/nodes-base/package.json | 4 +- 7 files changed, 411 insertions(+), 12 deletions(-) create mode 100644 packages/nodes-base/credentials/CustomerIoApi.credentials.ts create mode 100644 packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts create mode 100644 packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/CustomerIo/customer.Io.png 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 0000000000000000000000000000000000000000..7c5ae3010a4c7490ce71d9a7d3d611ad5de5c436 GIT binary patch literal 1790 zcmZ{lX*Ao38pi*LBoai1s-#*(#Zt9|5=&JjmP$*hr4cC;yHqq8!MWNDA-L7UZE7cR z%d`e#X=bR=rg{}cV`=Cu%BWPemfGeprBvNG_tX7w&wHNtInQ&>@BRAb`dvJSRMAlZ z002q$a`#ub^MT+n#diAoCQ)IKSfURR0Dir#`ZXM?$k<3Pe;)w2ZVUjtECAS1RJ;WM zNX7%e5?w)+0RWmUdVJ~N5G>-tId@?HV5&N53l$B5?M3APfcb?3fojf_)GJEmB(jf( z@*GU}kcQ3Tm5(d{fUc0;iGgu%S1M>stsqQa`l*)RS073e7{5;?S{WtCawhvdtg)KN z478@3JF=Iy`5cp%`zOfG3?(fj7#kaJSM>_&5Ohe5icSPR4itaFFT}jYf+^`RUrDjz zaLdM4Q>IqJLVg|iR`?=jcu>Sin?F5&dWPFl@EwV*Ony48+Yg%OaXKC4$Jpu)%G`NV zWzmWh6e~&EXT;wj!jnnuXn`vQfT8-^%$3yJ+-A^tHpY`i{44&$Eo9zd`Gop#CEZlG z(d@<9b!jB++shuJJu6({0^{l902$0dw$H%&40~p5t$5JQG?nqsuI;QC7-#|Q`bAmJ zP0|DJ`jVzD!jo%kJu&EKqkMX^u<+VYe(B{s*@hk@fCo*`&I`-_=N=Zk0pA{DV0KP) z)R=#3lRVABRAGWn3hafq_#1CH}V!%Fd` zf;++YIDRZDIp5OEzd%c1QLZ#l;xLh+?y!4gGjyca4i=mZeeD;bdLAs#3J5VFPZ&*O zn9J4R+OEAP1a3J<;~!0a1h&wP?|T$Oe1G}E$<+^1qi+p@*~Jmn-URE8N4Vj??;TT_&a-Gc1FvCa%z6!vf@P07H!Oq4 zo?lsVQRP>@@}(GHUm^%w=0K^`b8P7tVhwT6|LiDHqNA&R-fvy4Xq;b9QmRnlJ~4lw zqWz;G ziB3h{I)uBIvT%i7ue2o;FMl_;c5~4m2S?v_O+ObAdw-#NZna!j8~3GcF363kyTgC= z4x|a~hf!xMU5%_HQomV|geOL2QVf<~f7S@qz-VU{=u=n@@S$^6E2edK7((~DisbR9 z5ZI@|PI9!fO7s~?evJr$(kQ=1l!AAoPtLova$28-{K&HjuWj zQ_}t{W@K!!Xvj>={xMN3=-L%ivL>vPN(v3W7ugA*9!&(j>;Jk{5OaE=YAJ2vt8Ed5 zFN*4E{zWzdIm-p7?=@CVS%@R7gNOc##*2lPF@Ml7Be~MyPFAG%TkKT_wd*D@@=Zonwl;No!(8jZ`4X&$9G=2epJsT%6$@m9k{SwbuB6RA+C|m6w6CVp{lz&vT-n zkWRowV>Wdks-7b`@q`&U@U1>P@x^e#6FSIv0OyTyccXdB>-iNXwkK*wD}{3C*hvAo9#>+7l&r@SdgY8iYGvi%*f877=u zypMGHxT!Kge}4@*L6V7u;oa^pu=QQrg(M5hK;+m`R*;b19Vrj%ZR85d zUb}N~YU%dR$_?4?w6-4`XzIj!>3v$$x*GyHq(>X!w?Hm8^Cz3cVx1!DS1`fkd%eKJ z+)&vVyeU?`V}x1A=(I>^y4_9wt&QNL&DL^bK+PMnjqKz}Ci>OWDe^c(b;iz(@H$kE zGS;@_k?iQRWk9IfXo;uBvi4>O@_pfIdpa=3YYBCfGb!p0Q^iWdDIfkD$IoWk>dv;# z>>a!eqa=^uq^R(uXs5`;XoUebcpJP0o?v0)7-;S2WNq(chd+tOJK^z*R-=;t0TNhI YjF{B_50oWdRaO81+2f*nGl`!0ANL6eeE