diff --git a/packages/nodes-base/credentials/PushcutApi.credentials.ts b/packages/nodes-base/credentials/PushcutApi.credentials.ts new file mode 100644 index 0000000000..e52dc2a29d --- /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 0000000000..0bbbc05e30 --- /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 0000000000..66aafe9d05 --- /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 0000000000..64093c85cf --- /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 0000000000..82a9d9f87e Binary files /dev/null and b/packages/nodes-base/nodes/Pushcut/pushcut.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 72e93575ad..56c1922704 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",