From ea2902816fb1e17832d2610bef7eeb07f1e83641 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 21 Oct 2020 17:30:07 -0400 Subject: [PATCH] :sparkles: Add Pushover Node (#1069) --- .../credentials/PushOverApi.credentials.ts | 17 + .../nodes/Pushover/GenericFunctions.ts | 51 +++ .../nodes/Pushover/Pushover.node.ts | 395 ++++++++++++++++++ .../nodes-base/nodes/Pushover/pushover.png | Bin 0 -> 6310 bytes packages/nodes-base/package.json | 2 + 5 files changed, 465 insertions(+) create mode 100644 packages/nodes-base/credentials/PushOverApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Pushover/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Pushover/Pushover.node.ts create mode 100644 packages/nodes-base/nodes/Pushover/pushover.png diff --git a/packages/nodes-base/credentials/PushOverApi.credentials.ts b/packages/nodes-base/credentials/PushOverApi.credentials.ts new file mode 100644 index 000000000..3ec6fa905 --- /dev/null +++ b/packages/nodes-base/credentials/PushOverApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class PushoverApi implements ICredentialType { + name = 'pushoverApi'; + displayName = 'Pushover API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Pushover/GenericFunctions.ts b/packages/nodes-base/nodes/Pushover/GenericFunctions.ts new file mode 100644 index 000000000..cd1abd6c4 --- /dev/null +++ b/packages/nodes-base/nodes/Pushover/GenericFunctions.ts @@ -0,0 +1,51 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function pushoverApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, option = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('pushoverApi') as IDataObject; + + if (method === 'GET') { + qs.token = credentials.apiKey; + } else { + body.token = credentials.apiKey as string; + } + + const options: OptionsWithUri = { + method, + formData: body, + qs, + uri: `https://api.pushover.net/1${path}`, + json: true, + }; + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.request.call(this, options); + } catch (error) { + if (error.response && error.response.body && error.response.body.errors) { + + let errors = error.response.body.errors; + + errors = errors.map((e: IDataObject) => e); + // Try to return the error prettier + throw new Error( + `PushOver error response [${error.statusCode}]: ${errors.join('|')}` + ); + } + throw error; + } +} diff --git a/packages/nodes-base/nodes/Pushover/Pushover.node.ts b/packages/nodes-base/nodes/Pushover/Pushover.node.ts new file mode 100644 index 000000000..e57a16652 --- /dev/null +++ b/packages/nodes-base/nodes/Pushover/Pushover.node.ts @@ -0,0 +1,395 @@ +import { + BINARY_ENCODING, + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryData, + IBinaryKeyData, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + pushoverApiRequest, +} from './GenericFunctions'; + +export class Pushover implements INodeType { + description: INodeTypeDescription = { + displayName: 'Pushover', + name: 'pushover', + icon: 'file:pushover.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Pushover API.', + defaults: { + name: 'Pushover', + color: '#4b9cea', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'pushoverApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Message', + value: 'message', + }, + ], + default: 'message', + description: 'The resource to operate on.' + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'message', + ], + }, + }, + options: [ + { + name: 'Push', + value: 'push', + }, + ], + default: 'push', + description: 'The resource to operate on.' + }, + { + displayName: 'User Key', + name: 'userKey', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'push', + ], + }, + }, + default: '', + description: `The user/group key (not e-mail address) of your user (or you),
+ viewable when logged into our dashboard (often referred to as USER_KEY in our and code examples)` + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'push', + ], + }, + }, + default: '', + description: `Your message` + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'push', + ], + }, + }, + options: [ + { + name: 'Lowest Priority', + value: -2, + }, + { + name: 'Low Priority', + value: -1, + }, + { + name: 'Normal Priority', + value: 0, + }, + { + name: 'High Priority', + value: 1, + }, + { + name: 'Emergency Priority', + value: 2, + }, + ], + default: -2, + description: `send as -2 to generate no notification/alert,
+ -1 to always send as a quiet notification,
+ 1 to display as high-priority and bypass the user's quiet hours, or
+ 2 to also require confirmation from the user`, + }, + { + displayName: 'Retry (seconds)', + name: 'retry', + type: 'number', + typeOptions: { + minValue: 0, + }, + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'push', + ], + priority: [ + 2, + ], + }, + }, + default: 30, + description: `Specifies how often (in seconds) the Pushover servers will send the same notification to the user.
+ This parameter must have a value of at least 30 seconds between retries.` + }, + { + displayName: 'Expire (seconds)', + name: 'expire', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 10800, + }, + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'push', + ], + priority: [ + 2, + ], + }, + }, + default: 30, + description: `Specifies how many seconds your notification will continue to be retried for (every retry seconds)` + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'push', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Attachment', + name: 'attachmentsUi', + placeholder: 'Add Attachments', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + options: [ + { + name: 'attachmentsValues', + displayName: 'Attachment Property', + values: [ + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: '', + placeholder: 'data', + description: 'Name of the binary properties which contain data which should be added to email as attachment', + }, + ], + }, + ], + default: '', + }, + { + displayName: 'Device', + name: 'device', + type: 'string', + default: '', + description: `Your user's device name to send the message directly to that device,
+ rather than all of the user's devices (multiple devices may be separated by a comma)`, + }, + { + displayName: 'Sound', + name: 'sound', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSounds', + }, + default: '', + description: `The name of one of the sounds supported by device clients to override the user's default sound choice`, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: `Your message's title, otherwise your app's name is used`, + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'dateTime', + default: '', + description: `A Unix timestamp of your message's date and time to display to the user, rather than the time your message is received by our API`, + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: `a supplementary URL to show with your message`, + }, + { + displayName: 'URL Title', + name: 'url_title', + type: 'string', + default: '', + description: `A title for your supplementary URL, otherwise just the URL is shown`, + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + async getSounds(this: ILoadOptionsFunctions): Promise { + const { sounds } = await pushoverApiRequest.call(this, 'GET', '/sounds.json', {}); + const returnData: INodePropertyOptions[] = []; + for (const key of Object.keys(sounds)) { + returnData.push({ + name: sounds[key], + value: key, + }); + } + 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 === 'message') { + if (operation === 'push') { + const userKey = this.getNodeParameter('userKey', i) as string; + + const message = this.getNodeParameter('message', i) as string; + + const priority = this.getNodeParameter('priority', i) as number; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + user: userKey, + message, + priority, + } + + if (priority === 2) { + body.retry = this.getNodeParameter('retry', i) as number; + + body.expire = this.getNodeParameter('expire', i) as number; + } + + Object.assign(body, additionalFields); + + if (body.attachmentsUi) { + const attachment = (body.attachmentsUi as IDataObject).attachmentsValues as IDataObject; + + if (attachment) { + + const binaryPropertyName = attachment.binaryPropertyName as string; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryPropertyName] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + body.attachment = { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + } + }; + + delete body.attachmentsUi; + } + } + + responseData = await pushoverApiRequest.call( + this, + 'POST', + `/messages.json`, + 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/Pushover/pushover.png b/packages/nodes-base/nodes/Pushover/pushover.png new file mode 100644 index 0000000000000000000000000000000000000000..280bbad6307e1583c7933237bb3e041d05793931 GIT binary patch literal 6310 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 M$*aqi%fLea2gv=mvH$=8 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 410b99be2..da9653bb3 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -141,6 +141,7 @@ "dist/credentials/PhilipsHueOAuth2Api.credentials.js", "dist/credentials/Postgres.credentials.js", "dist/credentials/PostmarkApi.credentials.js", + "dist/credentials/PushoverApi.credentials.js", "dist/credentials/QuestDb.credentials.js", "dist/credentials/Redis.credentials.js", "dist/credentials/RocketchatApi.credentials.js", @@ -331,6 +332,7 @@ "dist/nodes/Pipedrive/PipedriveTrigger.node.js", "dist/nodes/Postgres/Postgres.node.js", "dist/nodes/Postmark/PostmarkTrigger.node.js", + "dist/nodes/Pushover/Pushover.node.js", "dist/nodes/PhilipsHue/PhilipsHue.node.js", "dist/nodes/QuestDb/QuestDb.node.js", "dist/nodes/ReadBinaryFile.node.js",