From 1cecd804881dc3ed7cfad855945a8921547f29ce Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 3 Dec 2019 08:54:30 -0500 Subject: [PATCH 1/3] done done done --- .../nodes/Mailchimp/MailchimpTrigger.node.ts | 248 ++++++++++++++++++ .../nodes/Shopify/GenericFunctions.ts | 42 +++ packages/nodes-base/package.json | 1 + 3 files changed, 291 insertions(+) create mode 100644 packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Shopify/GenericFunctions.ts diff --git a/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts b/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts new file mode 100644 index 0000000000..ebe1961a04 --- /dev/null +++ b/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts @@ -0,0 +1,248 @@ +import { + IHookFunctions, + IWebhookFunctions, + } from 'n8n-core'; + + import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, + ILoadOptionsFunctions, + INodePropertyOptions, + } from 'n8n-workflow'; + import { + mailchimpApiRequest, + } from './GenericFunctions'; + +export class MailchimpTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Mailchimp Trigger', + name: 'Mailchimp', + icon: 'file:mailchimp.png', + group: ['trigger'], + version: 1, + description: 'Handle Mailchimp events via webhooks', + defaults: { + name: 'Mailchimp Trigger', + color: '#32325d', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'mailchimpApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + reponseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'List', + name: 'list', + type: 'options', + required: true, + default: [], + description: 'The list that is gonna fire the event.', + typeOptions: { + loadOptionsMethod: 'getLists' + }, + options: [], + }, + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + required: true, + default: [], + description: 'The events that can trigger the webhook and whether they are enabled.', + options: [ + { + name: 'Subscribe', + value: 'subscribe', + description: 'Whether the webhook is triggered when a list subscriber is added.', + }, + { + name: 'Unsubscribe', + value: 'unsubscribe', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Profile Updated', + value: 'profile', + description: `Whether the webhook is triggered when a subscriber's profile is updated.`, + }, + { + name: 'Cleaned', + value: 'cleaned', + description: `Whether the webhook is triggered when a subscriber's email address is cleaned from the list.`, + }, + { + name: 'Email Address Updated', + value: 'upemail', + description: `Whether the webhook is triggered when a subscriber's email address is changed.`, + }, + { + name: 'Campaign Sent', + value: 'campaign', + description: `Whether the webhook is triggered when a campaign is sent or cancelled.`, + }, + ], + }, + { + displayName: 'Sources', + name: 'sources', + type: 'multiOptions', + required: true, + default: [], + description: 'The possible sources of any events that can trigger the webhook and whether they are enabled.', + options: [ + { + name: 'User', + value: 'user', + description: 'Whether the webhook is triggered by subscriber-initiated actions.', + }, + { + name: 'Admin', + value: 'admin', + description: 'Whether the webhook is triggered by admin-initiated actions in the web interface.', + }, + { + name: 'API', + value: 'api', + description: `Whether the webhook is triggered by actions initiated via the API.`, + }, + ], + } + ], + }; + + methods = { + loadOptions: { + // Get all the available lists to display them to user so that he can + // select them easily + async getLists(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let lists, response; + try { + response = await mailchimpApiRequest.call(this, '/lists', 'GET'); + lists = response.lists; + } catch (err) { + throw new Error(`Mailchimp Error: ${err}`); + } + for (const list of lists) { + const listName = list.name; + const listId = list.id; + + returnData.push({ + name: listName, + value: listId, + }); + } + return returnData; + }, + }, + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const listId = this.getNodeParameter('list') as string; + if (webhookData.webhookId === undefined) { + // No webhook id is set so no webhook can exist + return false; + } + const endpoint = `/lists/${listId}/webhooks/${webhookData.webhookId}`; + try { + await mailchimpApiRequest.call(this, endpoint, 'GET'); + } catch (err) { + if (err.statusCode === 404) { + return false; + } + throw new Error(`Mailchimp Error: ${err}`); + } + return true; + }, + + async create(this: IHookFunctions): Promise { + let webhook; + const webhookUrl = this.getNodeWebhookUrl('default'); + const listId = this.getNodeParameter('list') as string; + const events = this.getNodeParameter('events', []) as string[]; + const sources = this.getNodeParameter('sources', []) as string[]; + const body = { + url: webhookUrl, + events: events.reduce((object, currentValue) => { + // @ts-ignore + object[currentValue] = true; + return object; + }, {}), + sources: sources.reduce((object, currentValue) => { + // @ts-ignore + object[currentValue] = true; + return object; + }, {}), + }; + const endpoint = `/lists/${listId}/webhooks`; + try { + webhook = await mailchimpApiRequest.call(this, endpoint, 'POST', 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; + webhookData.sources = sources; + return true; + }, + + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const listId = this.getNodeParameter('list') as string; + if (webhookData.webhookId !== undefined) { + const endpoint = `/lists/${listId}/webhooks/${webhookData.webhookId}`; + try { + await mailchimpApiRequest.call(this, endpoint, 'DELETE', {}); + } catch (e) { + return false; + } + delete webhookData.webhookId; + delete webhookData.events; + delete webhookData.sources; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + 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(req.body) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Shopify/GenericFunctions.ts b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts new file mode 100644 index 0000000000..728f439f82 --- /dev/null +++ b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts @@ -0,0 +1,42 @@ +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, + BINARY_ENCODING +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function shopifyApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('shopifyApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const headerWithAuthentication = Object.assign({}, + { Authorization: ` Basic ${Buffer.from(`${credentials.apiKey}:${credentials.password}`).toString(BINARY_ENCODING)}` }); + + const options: OptionsWithUri = { + headers: headerWithAuthentication, + method, + qs: query, + uri: uri || `https://${credentials.shopName}.myshopify.com/admin/api/2019-10${resource}`, + body, + json: true + }; + + try { + return await this.helpers.request!(options); + } catch (error) { + const errorMessage = error.response.body.message || error.response.body.Message; + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 60f74c27c2..ae750d95e4 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -106,6 +106,7 @@ "dist/nodes/Jira/JiraSoftwareCloud.node.js", "dist/nodes/LinkFish/LinkFish.node.js", "dist/nodes/Mailchimp/Mailchimp.node.js", + "dist/nodes/Mailchimp/MailchimpTrigger.node.js", "dist/nodes/Mailgun/Mailgun.node.js", "dist/nodes/Mandrill/Mandrill.node.js", "dist/nodes/Mattermost/Mattermost.node.js", From b838d4c0b59abbebc7cbe55dbd2245d5ec03b78d Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 3 Dec 2019 08:54:30 -0500 Subject: [PATCH 2/3] :sparkles: Mailchimp trigger done done done --- .../nodes/Mailchimp/MailchimpTrigger.node.ts | 248 ++++++++++++++++++ .../nodes/Shopify/GenericFunctions.ts | 42 +++ packages/nodes-base/package.json | 1 + 3 files changed, 291 insertions(+) create mode 100644 packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Shopify/GenericFunctions.ts diff --git a/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts b/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts new file mode 100644 index 0000000000..ebe1961a04 --- /dev/null +++ b/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts @@ -0,0 +1,248 @@ +import { + IHookFunctions, + IWebhookFunctions, + } from 'n8n-core'; + + import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, + ILoadOptionsFunctions, + INodePropertyOptions, + } from 'n8n-workflow'; + import { + mailchimpApiRequest, + } from './GenericFunctions'; + +export class MailchimpTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Mailchimp Trigger', + name: 'Mailchimp', + icon: 'file:mailchimp.png', + group: ['trigger'], + version: 1, + description: 'Handle Mailchimp events via webhooks', + defaults: { + name: 'Mailchimp Trigger', + color: '#32325d', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'mailchimpApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + reponseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'List', + name: 'list', + type: 'options', + required: true, + default: [], + description: 'The list that is gonna fire the event.', + typeOptions: { + loadOptionsMethod: 'getLists' + }, + options: [], + }, + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + required: true, + default: [], + description: 'The events that can trigger the webhook and whether they are enabled.', + options: [ + { + name: 'Subscribe', + value: 'subscribe', + description: 'Whether the webhook is triggered when a list subscriber is added.', + }, + { + name: 'Unsubscribe', + value: 'unsubscribe', + description: 'Whether the webhook is triggered when a list member unsubscribes.', + }, + { + name: 'Profile Updated', + value: 'profile', + description: `Whether the webhook is triggered when a subscriber's profile is updated.`, + }, + { + name: 'Cleaned', + value: 'cleaned', + description: `Whether the webhook is triggered when a subscriber's email address is cleaned from the list.`, + }, + { + name: 'Email Address Updated', + value: 'upemail', + description: `Whether the webhook is triggered when a subscriber's email address is changed.`, + }, + { + name: 'Campaign Sent', + value: 'campaign', + description: `Whether the webhook is triggered when a campaign is sent or cancelled.`, + }, + ], + }, + { + displayName: 'Sources', + name: 'sources', + type: 'multiOptions', + required: true, + default: [], + description: 'The possible sources of any events that can trigger the webhook and whether they are enabled.', + options: [ + { + name: 'User', + value: 'user', + description: 'Whether the webhook is triggered by subscriber-initiated actions.', + }, + { + name: 'Admin', + value: 'admin', + description: 'Whether the webhook is triggered by admin-initiated actions in the web interface.', + }, + { + name: 'API', + value: 'api', + description: `Whether the webhook is triggered by actions initiated via the API.`, + }, + ], + } + ], + }; + + methods = { + loadOptions: { + // Get all the available lists to display them to user so that he can + // select them easily + async getLists(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let lists, response; + try { + response = await mailchimpApiRequest.call(this, '/lists', 'GET'); + lists = response.lists; + } catch (err) { + throw new Error(`Mailchimp Error: ${err}`); + } + for (const list of lists) { + const listName = list.name; + const listId = list.id; + + returnData.push({ + name: listName, + value: listId, + }); + } + return returnData; + }, + }, + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const listId = this.getNodeParameter('list') as string; + if (webhookData.webhookId === undefined) { + // No webhook id is set so no webhook can exist + return false; + } + const endpoint = `/lists/${listId}/webhooks/${webhookData.webhookId}`; + try { + await mailchimpApiRequest.call(this, endpoint, 'GET'); + } catch (err) { + if (err.statusCode === 404) { + return false; + } + throw new Error(`Mailchimp Error: ${err}`); + } + return true; + }, + + async create(this: IHookFunctions): Promise { + let webhook; + const webhookUrl = this.getNodeWebhookUrl('default'); + const listId = this.getNodeParameter('list') as string; + const events = this.getNodeParameter('events', []) as string[]; + const sources = this.getNodeParameter('sources', []) as string[]; + const body = { + url: webhookUrl, + events: events.reduce((object, currentValue) => { + // @ts-ignore + object[currentValue] = true; + return object; + }, {}), + sources: sources.reduce((object, currentValue) => { + // @ts-ignore + object[currentValue] = true; + return object; + }, {}), + }; + const endpoint = `/lists/${listId}/webhooks`; + try { + webhook = await mailchimpApiRequest.call(this, endpoint, 'POST', 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; + webhookData.sources = sources; + return true; + }, + + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const listId = this.getNodeParameter('list') as string; + if (webhookData.webhookId !== undefined) { + const endpoint = `/lists/${listId}/webhooks/${webhookData.webhookId}`; + try { + await mailchimpApiRequest.call(this, endpoint, 'DELETE', {}); + } catch (e) { + return false; + } + delete webhookData.webhookId; + delete webhookData.events; + delete webhookData.sources; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + 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(req.body) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Shopify/GenericFunctions.ts b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts new file mode 100644 index 0000000000..728f439f82 --- /dev/null +++ b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts @@ -0,0 +1,42 @@ +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, + BINARY_ENCODING +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function shopifyApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('shopifyApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const headerWithAuthentication = Object.assign({}, + { Authorization: ` Basic ${Buffer.from(`${credentials.apiKey}:${credentials.password}`).toString(BINARY_ENCODING)}` }); + + const options: OptionsWithUri = { + headers: headerWithAuthentication, + method, + qs: query, + uri: uri || `https://${credentials.shopName}.myshopify.com/admin/api/2019-10${resource}`, + body, + json: true + }; + + try { + return await this.helpers.request!(options); + } catch (error) { + const errorMessage = error.response.body.message || error.response.body.Message; + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 60f74c27c2..ae750d95e4 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -106,6 +106,7 @@ "dist/nodes/Jira/JiraSoftwareCloud.node.js", "dist/nodes/LinkFish/LinkFish.node.js", "dist/nodes/Mailchimp/Mailchimp.node.js", + "dist/nodes/Mailchimp/MailchimpTrigger.node.js", "dist/nodes/Mailgun/Mailgun.node.js", "dist/nodes/Mandrill/Mandrill.node.js", "dist/nodes/Mattermost/Mattermost.node.js", From c3f25a79ec476e00e13425dc2eb88f727884b9f7 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 11 Dec 2019 18:31:12 -0500 Subject: [PATCH 3/3] registered GET method --- .../nodes/Mailchimp/MailchimpTrigger.node.ts | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts b/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts index ebe1961a04..fc9aa7284c 100644 --- a/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts +++ b/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts @@ -36,12 +36,18 @@ export class MailchimpTrigger implements INodeType { } ], webhooks: [ - { - name: 'default', - httpMethod: 'POST', - reponseMode: 'onReceived', - path: 'webhook', - }, + { + name: 'setup', + httpMethod: 'GET', + reponseMode: 'onReceived', + path: 'webhook', + }, + { + name: 'default', + httpMethod: 'POST', + reponseMode: 'onReceived', + path: 'webhook', + } ], properties: [ { @@ -229,6 +235,15 @@ export class MailchimpTrigger implements INodeType { async webhook(this: IWebhookFunctions): Promise { const webhookData = this.getWorkflowStaticData('node') as IDataObject; + const webhookName = this.getWebhookName(); + if (webhookName === 'setup') { + // Is a create webhook confirmation request + const res = this.getResponseObject(); + res.status(200).end(); + return { + noWebhookResponse: true, + }; + } const req = this.getRequestObject(); if (req.body.id !== webhookData.id) { return {};