diff --git a/packages/nodes-base/credentials/AcuitySchedulingApi.credentials.ts b/packages/nodes-base/credentials/AcuitySchedulingApi.credentials.ts new file mode 100644 index 000000000..5ee2344ef --- /dev/null +++ b/packages/nodes-base/credentials/AcuitySchedulingApi.credentials.ts @@ -0,0 +1,23 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class AcuitySchedulingApi implements ICredentialType { + name = 'acuitySchedulingApi'; + displayName = 'Acuity Scheduling API'; + properties = [ + { + displayName: 'User ID', + name: 'userId', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/credentials/CopperApi.credentials.ts b/packages/nodes-base/credentials/CopperApi.credentials.ts new file mode 100644 index 000000000..ea3105054 --- /dev/null +++ b/packages/nodes-base/credentials/CopperApi.credentials.ts @@ -0,0 +1,25 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class CopperApi implements ICredentialType { + name = 'copperApi'; + displayName = 'Copper API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + required: true, + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Email', + name: 'email', + required: true, + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.ts b/packages/nodes-base/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.ts new file mode 100644 index 000000000..2166b5cc8 --- /dev/null +++ b/packages/nodes-base/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.ts @@ -0,0 +1,163 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + acuitySchedulingApiRequest, +} from './GenericFunctions'; + +export class AcuitySchedulingTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Acuity Scheduling Trigger', + name: 'acuitySchedulingTrigger', + icon: 'file:acuityScheduling.png', + group: ['trigger'], + version: 1, + description: 'Handle Acuity Scheduling events via webhooks', + defaults: { + name: 'Acuity Scheduling Trigger', + color: '#000000', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'acuitySchedulingApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'appointment.scheduled', + value: 'appointment.scheduled', + description: 'is called once when an appointment is initially booked', + }, + { + name: 'appointment.rescheduled', + value: 'appointment.rescheduled', + description: 'is called when the appointment is rescheduled to a new time', + }, + { + name: 'appointment.canceled', + value: 'appointment.canceled', + description: 'is called whenever an appointment is canceled', + }, + { + name: 'appointment.changed', + value: 'appointment.changed', + description: 'is called when the appointment is changed in any way', + }, + { + name: 'order.completed', + value: 'order.completed', + description: 'is called when an order is completed', + }, + ], + }, + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + default: true, + description: 'By default does the webhook-data only contain the ID of the object.
If this option gets activated it will resolve the data automatically.', + }, + ], + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId === undefined) { + return false; + } + const endpoint = '/webhooks'; + const webhooks = await acuitySchedulingApiRequest.call(this, 'GET', endpoint); + if (Array.isArray(webhooks)) { + for (const webhook of webhooks) { + if (webhook.id === webhookData.webhookId) { + return true; + } + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const event = this.getNodeParameter('event') as string; + const endpoint = '/webhooks'; + const body: IDataObject = { + target: webhookUrl, + event, + }; + const { id } = await acuitySchedulingApiRequest.call(this, 'POST', endpoint, body); + webhookData.webhookId = id; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const endpoint = `/webhooks/${webhookData.webhookId}`; + try { + await acuitySchedulingApiRequest.call(this, 'DELETE', endpoint); + } catch(error) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + + const resolveData = this.getNodeParameter('resolveData', false) as boolean; + + if (resolveData === false) { + // Return the data as it got received + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } + + // Resolve the data by requesting the information via API + const event = this.getNodeParameter('event', false) as string; + const eventType = event.split('.').shift(); + const endpoint = `/${eventType}s/${req.body.id}`; + const responseData = await acuitySchedulingApiRequest.call(this, 'GET', endpoint, {}); + + return { + workflowData: [ + this.helpers.returnJsonArray(responseData), + ], + }; + + + } +} diff --git a/packages/nodes-base/nodes/AcuityScheduling/GenericFunctions.ts b/packages/nodes-base/nodes/AcuityScheduling/GenericFunctions.ts new file mode 100644 index 000000000..9a2d0fc59 --- /dev/null +++ b/packages/nodes-base/nodes/AcuityScheduling/GenericFunctions.ts @@ -0,0 +1,42 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export async function acuitySchedulingApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('acuitySchedulingApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + auth: { + user: credentials.userId as string, + password: credentials.apiKey as string, + }, + method, + qs, + body, + uri: uri ||`https://acuityscheduling.com/api/v1${resource}`, + json: true + }; + try { + return await this.helpers.request!(options); + } catch (error) { + + let errorMessage = error.message; + if (error.response.body && error.response.body.message) { + errorMessage = `[${error.response.body.status_code}] ${error.response.body.message}`; + } + + throw new Error('Acuity Scheduling Error: ' + errorMessage); + } +} diff --git a/packages/nodes-base/nodes/AcuityScheduling/acuityScheduling.png b/packages/nodes-base/nodes/AcuityScheduling/acuityScheduling.png new file mode 100644 index 000000000..df087b60e Binary files /dev/null and b/packages/nodes-base/nodes/AcuityScheduling/acuityScheduling.png differ diff --git a/packages/nodes-base/nodes/Copper/CopperTrigger.node.ts b/packages/nodes-base/nodes/Copper/CopperTrigger.node.ts new file mode 100644 index 000000000..2c95c25eb --- /dev/null +++ b/packages/nodes-base/nodes/Copper/CopperTrigger.node.ts @@ -0,0 +1,174 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + copperApiRequest, + getAutomaticSecret, +} from './GenericFunctions'; + +export class CopperTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Copper Trigger', + name: 'copperTrigger', + icon: 'file:copper.png', + group: ['trigger'], + version: 1, + description: 'Handle Copper events via webhooks', + defaults: { + name: 'Copper Trigger', + color: '#ff2564', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'copperApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Company', + value: 'company', + }, + { + name: 'Lead', + value: 'lead', + }, + { + name: 'Opportunity', + value: 'opportunity', + }, + { + name: 'Person', + value: 'person', + }, + { + name: 'Project', + value: 'project', + }, + { + name: 'Task', + value: 'task', + }, + ], + description: 'The resource which will fire the event.', + }, + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Delete', + value: 'delete', + description: 'An existing record is removed', + }, + { + name: 'New', + value: 'new', + description: 'A new record is created', + }, + { + name: 'Update', + value: 'update', + description: 'Any field in the existing entity record is changed', + }, + ], + description: 'The event to listen to.', + }, + ], + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId === undefined) { + return false; + } + const endpoint = `/webhooks/${webhookData.webhookId}`; + try { + await copperApiRequest.call(this, 'GET', endpoint); + } catch (err) { + return false; + } + return true; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const resource = this.getNodeParameter('resource') as string; + const event = this.getNodeParameter('event') as string; + const endpoint = '/webhooks'; + const body: IDataObject = { + target: webhookUrl, + type: resource, + event, + }; + + const credentials = this.getCredentials('copperApi'); + body.secret = { + secret: getAutomaticSecret(credentials!), + }; + + const { id } = await copperApiRequest.call(this, 'POST', endpoint, body); + webhookData.webhookId = id; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const endpoint = `/webhooks/${webhookData.webhookId}`; + try { + await copperApiRequest.call(this, 'DELETE', endpoint); + } catch(error) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const credentials = this.getCredentials('copperApi'); + const req = this.getRequestObject(); + + // Check if the supplied secret matches. If not ignore request. + if (req.body.secret !== getAutomaticSecret(credentials!)) { + return {}; + } + + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Copper/GenericFunctions.ts b/packages/nodes-base/nodes/Copper/GenericFunctions.ts new file mode 100644 index 000000000..31bd1379c --- /dev/null +++ b/packages/nodes-base/nodes/Copper/GenericFunctions.ts @@ -0,0 +1,63 @@ +import { createHash } from 'crypto'; +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; +import { + ICredentialDataDecryptedObject, + IDataObject, +} from 'n8n-workflow'; + +export async function copperApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('copperApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + let options: OptionsWithUri = { + headers: { + 'X-PW-AccessToken': credentials.apiKey, + 'X-PW-Application': 'developer_api', + 'X-PW-UserEmail': credentials.email, + 'Content-Type': 'application/json', + }, + method, + qs, + body, + uri: uri ||`https://api.prosperworks.com/developer_api/v1${resource}`, + json: true + }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + let errorMessage = error.message; + if (error.response.body && error.response.body.message) { + errorMessage = error.response.body.message; + } + + throw new Error('Copper Error: ' + errorMessage); + } +} + + +/** + * Creates a secret from the credentials + * + * @export + * @param {ICredentialDataDecryptedObject} credentials + * @returns + */ +export function getAutomaticSecret(credentials: ICredentialDataDecryptedObject) { + const data = `${credentials.email},${credentials.apiKey}`; + return createHash('md5').update(data).digest("hex"); +} diff --git a/packages/nodes-base/nodes/Copper/copper.png b/packages/nodes-base/nodes/Copper/copper.png new file mode 100644 index 000000000..befa65c18 Binary files /dev/null and b/packages/nodes-base/nodes/Copper/copper.png differ diff --git a/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts b/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts index 53a0a7530..709b9858c 100644 --- a/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts +++ b/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts @@ -19,7 +19,7 @@ import { export class WebflowTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Webflow Trigger', - name: 'webflow', + name: 'webflowTrigger', icon: 'file:webflow.png', group: ['trigger'], version: 1, diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 8835a31ec..4087568d5 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -27,6 +27,7 @@ "n8n": { "credentials": [ "dist/credentials/ActiveCampaignApi.credentials.js", + "dist/credentials/AcuitySchedulingApi.credentials.js", "dist/credentials/AirtableApi.credentials.js", "dist/credentials/Amqp.credentials.js", "dist/credentials/AsanaApi.credentials.js", @@ -35,6 +36,7 @@ "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/CodaApi.credentials.js", "dist/credentials/ClickUpApi.credentials.js", + "dist/credentials/CopperApi.credentials.js", "dist/credentials/DropboxApi.credentials.js", "dist/credentials/EventbriteApi.credentials.js", "dist/credentials/FreshdeskApi.credentials.js", @@ -86,6 +88,7 @@ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", "dist/nodes/ActiveCampaign/ActiveCampaignTrigger.node.js", "dist/nodes/Airtable/Airtable.node.js", + "dist/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.js", "dist/nodes/Amqp/Amqp.node.js", "dist/nodes/Amqp/AmqpTrigger.node.js", "dist/nodes/Asana/Asana.node.js", @@ -98,6 +101,7 @@ "dist/nodes/Coda/Coda.node.js", "dist/nodes/ClickUp/ClickUp.node.js", "dist/nodes/ClickUp/ClickUpTrigger.node.js", + "dist/nodes/Copper/CopperTrigger.node.js", "dist/nodes/Cron.node.js", "dist/nodes/Discord/Discord.node.js", "dist/nodes/Dropbox/Dropbox.node.js",