From 23b61475d954fda96c75acd4813fc05ba6abb07b Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 12 Dec 2020 02:58:58 -0500 Subject: [PATCH] :sparkles: Add Pushcut Node & Trigger (#1075) * :sparkles: Pushcut Node & Trigger * :zap: Small improvements Co-authored-by: Jan --- .../credentials/PushcutApi.credentials.ts | 17 ++ .../nodes/Pushcut/GenericFunctions.ts | 50 ++++ .../nodes-base/nodes/Pushcut/Pushcut.node.ts | 215 ++++++++++++++++++ .../nodes/Pushcut/PushcutTrigger.node.ts | 130 +++++++++++ packages/nodes-base/nodes/Pushcut/pushcut.png | Bin 0 -> 4009 bytes packages/nodes-base/package.json | 3 + 6 files changed, 415 insertions(+) create mode 100644 packages/nodes-base/credentials/PushcutApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Pushcut/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Pushcut/Pushcut.node.ts create mode 100644 packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Pushcut/pushcut.png diff --git a/packages/nodes-base/credentials/PushcutApi.credentials.ts b/packages/nodes-base/credentials/PushcutApi.credentials.ts new file mode 100644 index 000000000..e52dc2a29 --- /dev/null +++ b/packages/nodes-base/credentials/PushcutApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class PushcutApi implements ICredentialType { + name = 'pushcutApi'; + displayName = 'Pushcut API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Pushcut/GenericFunctions.ts b/packages/nodes-base/nodes/Pushcut/GenericFunctions.ts new file mode 100644 index 000000000..0bbbc05e3 --- /dev/null +++ b/packages/nodes-base/nodes/Pushcut/GenericFunctions.ts @@ -0,0 +1,50 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, +} from 'n8n-workflow'; + +export async function pushcutApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, uri?: string | undefined, option = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('pushcutApi') as IDataObject; + + const options: OptionsWithUri = { + headers: { + 'API-Key': credentials.apiKey, + }, + method, + body, + qs, + uri: uri || `https://api.pushcut.io/v1${path}`, + json: true, + }; + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + //@ts-ignore + return await this.helpers.request.call(this, options); + } catch (error) { + if (error.response && error.response.body && error.response.body.error) { + + const message = error.response.body.error; + + // Try to return the error prettier + throw new Error( + `Pushcut error response [${error.statusCode}]: ${message}` + ); + } + throw error; + } +} diff --git a/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts b/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts new file mode 100644 index 000000000..66aafe9d0 --- /dev/null +++ b/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts @@ -0,0 +1,215 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + pushcutApiRequest, +} from './GenericFunctions'; + +export class Pushcut implements INodeType { + description: INodeTypeDescription = { + displayName: 'Pushcut', + name: 'pushcut', + icon: 'file:pushcut.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Pushcut API.', + defaults: { + name: 'Pushcut', + color: '#1f2957', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'pushcutApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Notification', + value: 'notification', + }, + ], + default: 'notification', + description: 'The resource to operate on.' + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'notification', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Send a notification', + }, + ], + default: 'send', + description: 'The resource to operate on.' + }, + { + displayName: 'Notification Name', + name: 'notificationName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getNotifications', + }, + displayOptions: { + show: { + resource: [ + 'notification', + ], + operation: [ + 'send', + ], + }, + }, + default: 'Notification Name', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'notification', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Devices', + name: 'devices', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getDevices', + }, + default: '', + description: 'List of devices this notification is sent to. (default is all devices)', + }, + { + displayName: 'Input', + name: 'input', + type: 'string', + default: '', + description: 'Value that is passed as input to the notification action.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'Text that is used instead of the one defined in the app.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title that is used instead of the one defined in the app.', + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + // Get all the available devices to display them to user so that he can + // select them easily + async getDevices(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const devices = await pushcutApiRequest.call(this, 'GET', '/devices'); + for (const device of devices) { + returnData.push({ + name: device.id, + value: device.id, + }); + } + return returnData; + }, + // Get all the available notifications to display them to user so that he can + // select them easily + async getNotifications(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const notifications = await pushcutApiRequest.call(this, 'GET', '/notifications'); + for (const notification of notifications) { + returnData.push({ + name: notification.title, + value: notification.id, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + + if (resource === 'notification') { + if (operation === 'send') { + const notificationName = this.getNodeParameter('notificationName', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = {}; + + Object.assign(body, additionalFields); + + responseData = await pushcutApiRequest.call( + this, + 'POST', + `/notifications/${encodeURI(notificationName)}`, + body, + ); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts b/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts new file mode 100644 index 000000000..64093c85c --- /dev/null +++ b/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts @@ -0,0 +1,130 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + pushcutApiRequest, +} from './GenericFunctions'; + +export class PushcutTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Pushcut Trigger', + name: 'pushcutTrigger', + icon: 'file:pushcut.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when a Github events occurs.', + defaults: { + name: 'Pushcut Trigger', + color: '#1f2957', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'pushcutApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Action Name', + name: 'actionName', + type: 'string', + description: 'Choose any name you would like. It will show up as a server action in the app', + default: '', + }, + ], + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const actionName = this.getNodeParameter('actionName'); + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const endpoint = '/subscriptions'; + const webhooks = await pushcutApiRequest.call(this, 'GET', endpoint, {}); + + for (const webhook of webhooks) { + if (webhook.url === webhookUrl && + webhook.actionName === actionName) { + webhookData.webhookId = webhook.id; + return true; + } + } + + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const actionName = this.getNodeParameter('actionName'); + + const endpoint = '/subscriptions'; + + const body = { + actionName, + url: webhookUrl + }; + + const responseData = await pushcutApiRequest.call(this, 'POST', endpoint, body); + + if (responseData.id === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.id as string; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + + const endpoint = `/subscriptions/${webhookData.webhookId}`; + + try { + await pushcutApiRequest.call(this, 'DELETE', endpoint); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const body = this.getBodyData() as IDataObject; + + return { + workflowData: [ + this.helpers.returnJsonArray(body) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Pushcut/pushcut.png b/packages/nodes-base/nodes/Pushcut/pushcut.png new file mode 100644 index 0000000000000000000000000000000000000000..82a9d9f87e2b1e021bec1c28af0701c72862f69e GIT binary patch literal 4009 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 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 72e93575a..56c192270 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -166,6 +166,7 @@ "dist/credentials/ProfitWellApi.credentials.js", "dist/credentials/PushbulletOAuth2Api.credentials.js", "dist/credentials/PushoverApi.credentials.js", + "dist/credentials/PushcutApi.credentials.js", "dist/credentials/QuestDb.credentials.js", "dist/credentials/QuickBaseApi.credentials.js", "dist/credentials/Redis.credentials.js", @@ -394,6 +395,8 @@ "dist/nodes/Postmark/PostmarkTrigger.node.js", "dist/nodes/ProfitWell/ProfitWell.node.js", "dist/nodes/Pushbullet/Pushbullet.node.js", + "dist/nodes/Pushcut/Pushcut.node.js", + "dist/nodes/Pushcut/PushcutTrigger.node.js", "dist/nodes/Pushover/Pushover.node.js", "dist/nodes/QuestDb/QuestDb.node.js", "dist/nodes/QuickBase/QuickBase.node.js",