From d0fc7209bf0d1997fc6cc159c38343a4caaf6b56 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 27 Oct 2020 15:14:55 -0400 Subject: [PATCH] :sparkles: Add Pushbullet-Node (#1073) * :sparkles: Pushbullet-Node * :zap: Small improvements * :zap: Improvements Co-authored-by: Jan --- .../PushbulletOAuth2Api.credentials.ts | 46 ++ .../nodes/Pushbullet/GenericFunctions.ts | 60 ++ .../nodes/Pushbullet/Pushbullet.node.ts | 608 ++++++++++++++++++ .../nodes/Pushbullet/pushbullet.svg | 1 + packages/nodes-base/package.json | 4 +- 5 files changed, 718 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/credentials/PushbulletOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Pushbullet/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts create mode 100644 packages/nodes-base/nodes/Pushbullet/pushbullet.svg diff --git a/packages/nodes-base/credentials/PushbulletOAuth2Api.credentials.ts b/packages/nodes-base/credentials/PushbulletOAuth2Api.credentials.ts new file mode 100644 index 0000000000..4bc2b88234 --- /dev/null +++ b/packages/nodes-base/credentials/PushbulletOAuth2Api.credentials.ts @@ -0,0 +1,46 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class PushbulletOAuth2Api implements ICredentialType { + name = 'pushbulletOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Pushbullet OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://www.pushbullet.com/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.pushbullet.com/oauth2/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body' + }, + ]; +} diff --git a/packages/nodes-base/nodes/Pushbullet/GenericFunctions.ts b/packages/nodes-base/nodes/Pushbullet/GenericFunctions.ts new file mode 100644 index 0000000000..27c852a17b --- /dev/null +++ b/packages/nodes-base/nodes/Pushbullet/GenericFunctions.ts @@ -0,0 +1,60 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function pushbulletApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, uri?: string | undefined, option = {}): Promise { // tslint:disable-line:no-any + + const options: OptionsWithUri = { + method, + body, + qs, + uri: uri || `https://api.pushbullet.com/v2${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.requestOAuth2.call(this, 'pushbulletOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.error) { + + const message = error.response.body.error.message; + + // Try to return the error prettier + throw new Error( + `Pushbullet error response [${error.statusCode}]: ${message}` + ); + } + throw error; + } +} + +export async function pushbulletApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + do { + responseData = await pushbulletApiRequest.call(this, method, endpoint, body, query); + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.cursor !== undefined + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts b/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts new file mode 100644 index 0000000000..e0c95eb593 --- /dev/null +++ b/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts @@ -0,0 +1,608 @@ +import { + BINARY_ENCODING, + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryKeyData, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + pushbulletApiRequest, + pushbulletApiRequestAllItems, +} from './GenericFunctions'; + +import * as moment from 'moment-timezone'; + +export class Pushbullet implements INodeType { + description: INodeTypeDescription = { + displayName: 'Pushbullet', + name: 'pushbullet', + icon: 'file:pushbullet.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Pushbullet API.', + defaults: { + name: 'Pushbullet', + color: '#457854', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'pushbulletOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Push', + value: 'push', + }, + ], + default: 'push', + description: 'The resource to operate on.' + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'push', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a push', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a push', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all pushes', + }, + { + name: 'Update', + value: 'update', + description: 'Update a push', + }, + ], + default: 'create', + description: 'The resource to operate on.' + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'File', + value: 'file', + }, + { + name: 'Link', + value: 'link', + }, + { + name: 'Note', + value: 'note', + }, + ], + required: true, + displayOptions: { + show: { + resource: [ + 'push', + ], + operation: [ + 'create', + ], + }, + }, + default: 'note', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'push', + ], + operation: [ + 'create', + ], + type: [ + 'note', + 'link' + ], + }, + }, + default: '', + description: `Title of the push, used for all types of pushes` + }, + { + displayName: 'Body', + name: 'body', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'push', + ], + operation: [ + 'create', + ], + type: [ + 'note', + 'link', + 'file', + ], + }, + }, + default: '', + description: `Body of the push, used for all types of pushes` + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'push', + ], + operation: [ + 'create', + ], + type: [ + 'link', + ], + }, + }, + default: '', + description: `Body of the push, used for all types of pushes` + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + resource: [ + 'push', + ], + operation: [ + 'create', + ], + type: [ + 'file', + ], + }, + }, + placeholder: '', + description: 'Name of the binary property which contains
the data for the file to be created.', + }, + { + displayName: 'Target', + name: 'target', + type: 'options', + options: [ + { + name: 'Channel Tag', + value: 'channel_tag', + description: 'Send the push to all subscribers to your channel that has this tag', + }, + { + name: 'Default', + value: 'default', + description: `Broadcast it to all of the user's devices`, + }, + { + name: 'Device ID', + value: 'device_iden', + description: 'Send the push to a specific device', + }, + { + name: 'Email', + value: 'email', + description: 'Send the push to this email address', + }, + ], + required: true, + displayOptions: { + show: { + resource: [ + 'push', + ], + operation: [ + 'create', + ], + }, + }, + default: 'default', + description: 'Define the medium that will be used to send the push', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'push', + ], + operation: [ + 'create', + ], + }, + hide: { + target: [ + 'default', + 'device_iden', + ], + }, + }, + default: '', + description: `The value to be set depending on the target selected.
+ For example, if the target selected is email then this field would take the email address
+ of the person you are trying to send the push to.`, + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDevices', + }, + required: true, + displayOptions: { + show: { + resource: [ + 'push', + ], + operation: [ + 'create', + ], + target: [ + 'device_iden', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Push ID', + name: 'pushId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'push', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'push', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'push', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'push', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Active', + name: 'active', + type: 'boolean', + default: false, + description: `Don't return deleted pushes`, + }, + { + displayName: 'Modified After', + name: 'modified_after', + type: 'dateTime', + default: '', + description: `Request pushes modified after this timestamp`, + }, + ], + }, + { + displayName: 'Push ID', + name: 'pushId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'push', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + }, + { + displayName: 'Dismissed', + name: 'dismissed', + type: 'boolean', + required: true, + displayOptions: { + show: { + resource: [ + 'push', + ], + operation: [ + 'update', + ], + }, + }, + default: false, + description: 'Marks a push as having been dismissed by the user, will cause any notifications for the push to be hidden if possible.', + }, + ], + }; + + methods = { + loadOptions: { + async getDevices(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { devices } = await pushbulletApiRequest.call(this, 'GET', '/devices'); + for (const device of devices) { + returnData.push({ + name: device.nickname, + value: device.iden, + }); + } + 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 === 'push') { + if (operation === 'create') { + const type = this.getNodeParameter('type', i) as string; + + const message = this.getNodeParameter('body', i) as string; + + const target = this.getNodeParameter('target', i) as string; + + const body: IDataObject = { + type, + body: message, + }; + + if (target !== 'default') { + const value = this.getNodeParameter('value', i) as string; + body[target as string] = value; + } + + if (['note', 'link'].includes(type)) { + body.title = this.getNodeParameter('title', i) as string; + + if (type === 'link') { + body.url = this.getNodeParameter('url', i) as string; + } + } + + if (type === 'file') { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + //@ts-ignore + if (items[i].binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName]; + + //create upload url + const { + upload_url: uploadUrl, + file_name, + file_type, + file_url, + } = await pushbulletApiRequest.call( + this, + 'POST', + `/upload-request`, + { + file_name: binaryData.fileName, + file_type: binaryData.mimeType, + }, + ); + + //upload the file + await pushbulletApiRequest.call( + this, + 'POST', + '', + {}, + {}, + uploadUrl, + { + formData: { + file: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + } + }, + }, + json: false, + }, + ); + + body.file_name = file_name; + body.file_type = file_type; + body.file_url = file_url; + } + + responseData = await pushbulletApiRequest.call( + this, + 'POST', + `/pushes`, + body, + ); + } + + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + const filters = this.getNodeParameter('filters', i) as IDataObject; + + Object.assign(qs, filters); + + if (qs.modified_after) { + qs.modified_after = moment(qs.modified_after as string).unix(); + } + + if (returnAll) { + responseData = await pushbulletApiRequestAllItems.call(this, 'pushes', 'GET', '/pushes', {}, qs); + + } else { + qs.limit = this.getNodeParameter('limit', 0) as number; + + responseData = await pushbulletApiRequest.call(this, 'GET', '/pushes', {}, qs); + + responseData = responseData.pushes; + } + } + + if (operation === 'delete') { + const pushId = this.getNodeParameter('pushId', i) as string; + + responseData = await pushbulletApiRequest.call( + this, + 'DELETE', + `/pushes/${pushId}`, + ); + + responseData = { success: true }; + } + + if (operation === 'update') { + const pushId = this.getNodeParameter('pushId', i) as string; + + const dismissed = this.getNodeParameter('dismissed', i) as boolean; + + responseData = await pushbulletApiRequest.call( + this, + 'POST', + `/pushes/${pushId}`, + { + dismissed, + } + ); + } + } + } + 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/Pushbullet/pushbullet.svg b/packages/nodes-base/nodes/Pushbullet/pushbullet.svg new file mode 100644 index 0000000000..0cc5e90197 --- /dev/null +++ b/packages/nodes-base/nodes/Pushbullet/pushbullet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 6bd69d62e6..9dfdac5926 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -143,6 +143,7 @@ "dist/credentials/PhilipsHueOAuth2Api.credentials.js", "dist/credentials/Postgres.credentials.js", "dist/credentials/PostmarkApi.credentials.js", + "dist/credentials/PushbulletOAuth2Api.credentials.js", "dist/credentials/PushoverApi.credentials.js", "dist/credentials/QuestDb.credentials.js", "dist/credentials/Redis.credentials.js", @@ -334,10 +335,11 @@ "dist/nodes/PayPal/PayPalTrigger.node.js", "dist/nodes/Pipedrive/Pipedrive.node.js", "dist/nodes/Pipedrive/PipedriveTrigger.node.js", + "dist/nodes/PhilipsHue/PhilipsHue.node.js", "dist/nodes/Postgres/Postgres.node.js", "dist/nodes/Postmark/PostmarkTrigger.node.js", + "dist/nodes/Pushbullet/Pushbullet.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", "dist/nodes/ReadBinaryFiles.node.js",