From 2dbde92fd561141d1d11c52c46a9aedcfb87197c Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 24 Nov 2020 17:19:41 -0500 Subject: [PATCH] :sparkles: Add Spontit Node (#1186) * Added first trial for a spontit integration * Added spontit packages * Changed option types from collection to string * :zap: Improvements to Spontit Node Improvements to #1163 Co-authored-by: Tobias Schulz-Hess --- .../credentials/SpontitApi.credentials.ts | 24 +++ .../nodes/Spontit/GenericFunctions.ts | 49 +++++ .../nodes/Spontit/PushDescription.ts | 170 ++++++++++++++++++ .../nodes-base/nodes/Spontit/Spontit.node.ts | 116 ++++++++++++ packages/nodes-base/nodes/Spontit/spontit.png | Bin 0 -> 5645 bytes packages/nodes-base/package.json | 2 + 6 files changed, 361 insertions(+) create mode 100644 packages/nodes-base/credentials/SpontitApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Spontit/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Spontit/PushDescription.ts create mode 100644 packages/nodes-base/nodes/Spontit/Spontit.node.ts create mode 100644 packages/nodes-base/nodes/Spontit/spontit.png diff --git a/packages/nodes-base/credentials/SpontitApi.credentials.ts b/packages/nodes-base/credentials/SpontitApi.credentials.ts new file mode 100644 index 0000000000..510093aeda --- /dev/null +++ b/packages/nodes-base/credentials/SpontitApi.credentials.ts @@ -0,0 +1,24 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class SpontitApi implements ICredentialType { + name = 'spontitApi'; + displayName = 'Spontit API'; + documentationUrl = 'spontit'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Username', + name: 'username', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Spontit/GenericFunctions.ts b/packages/nodes-base/nodes/Spontit/GenericFunctions.ts new file mode 100644 index 0000000000..993ede1b96 --- /dev/null +++ b/packages/nodes-base/nodes/Spontit/GenericFunctions.ts @@ -0,0 +1,49 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function spontitApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('spontitApi') as IDataObject; + + try { + const options: OptionsWithUri = { + headers: { + 'X-Authorization': credentials.apiKey as string, + 'X-UserId': credentials.username as string, + }, + method, + body, + qs, + uri: `https://api.spontit.com/v3${resource}`, + json: true, + }; + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers?.request(options); + } catch (error) { + + if (error.response && error.response.body && error.response.body.message) { + + const messages = error.response.body.message; + // Try to return the error prettier + throw new Error( + `Spontit error response [${error.statusCode}]: ${messages}`, + ); + } + throw error; + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Spontit/PushDescription.ts b/packages/nodes-base/nodes/Spontit/PushDescription.ts new file mode 100644 index 0000000000..bbb6075e6b --- /dev/null +++ b/packages/nodes-base/nodes/Spontit/PushDescription.ts @@ -0,0 +1,170 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const pushOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'push', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a push notification', + }, + ], + default: 'push', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const pushFields = [ + /* -------------------------------------------------------------------------- */ + /* push:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'push', + ], + }, + }, + description: `To provide text in a push, supply one of either "content" or "pushContent" (or both). + Limited to 2500 characters. (Required if a value for "pushContent" is not provided).`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'push', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Channel Name', + name: 'channelName', + type: 'string', + default: '', + description: 'The name of a channel you created. If you have not yet created a channel, simply don\'t provide this value and the push will be sent to your main account.', + }, + { + displayName: 'Expiration Stamp', + name: 'expirationStamp', + type: 'dateTime', + default: '', + description: 'A Unix timestamp. When to automatically expire your push notification. The default is 10 days after pushing. The push will become unaccessible within 15-30 minutes of the selected time, but will remain on all device screens until dismissed or clicked.', + }, + { + displayName: 'iOS DeepLink', + name: 'iOSDeepLink', + type: 'string', + default: '', + description: 'An iOS deep link. Use this to deep link into other apps. Alternatively, you can provide a universal link in the link attribute and set openLinkInApp to false.', + }, + { + displayName: 'Link', + name: 'link', + type: 'string', + default: '', + description: 'A link that can be attached to your push. Must be a valid URL.', + }, + { + displayName: 'Open In Home Feed', + name: 'openInHomeFeed', + type: 'boolean', + default: false, + description: 'Control whether the notification opens to the home feed or to a standalone page with the notification. The default (openInHomeFeed=False) is to open the notification on a standalone page.', + }, + { + displayName: 'Open Link In App', + name: 'openLinkInApp', + type: 'boolean', + default: false, + description: 'Whether to open the provided link within the iOS app or in Safari. Android PWA opens all links in the default web browser.', + }, + { + displayName: 'Push To Emails', + name: 'pushToEmails', + type: 'string', + default: '', + required: false, + description: `Emails (strings) to whom to send the notification.
+ If all three attributes 'pushToFollowers', 'pushToPhoneNumbers' and 'pushToEmails'
+ are not supplied, then everyone who follows the channel will receive the push notification.
+ If 'pushToFollowers' is supplied, only those listed in the array will receive the push notification.
+ If one of the userIds supplied does not follow the specified channel, then that userId value will be ignored.
+ See the "Followers" section to learn how to list the userIds of those who follow one of your channels.`, + }, + { + displayName: 'Push To Followers', + name: 'pushToFollowers', + type: 'string', + default: '', + description: `User IDs (strings) to whom to send the notification.
+ If all three attributes 'pushToFollowers', 'pushToPhoneNumbers' and 'pushToEmails'
+ are not supplied, then everyone who follows the channel will receive the push notification.
+ If 'pushToFollowers' is supplied, only those listed in the array will receive the push notification.
+ If one of the userIds supplied does not follow the specified channel, then that userId value will be ignored.
+ See the "Followers" section to learn how to list the userIds of those who follow one of your channels.`, + }, + { + displayName: 'Push To Phone Numbers', + name: 'pushToPhoneNumbers', + type: 'string', + default: '', + description: `Phone numbers (strings) to whom to send the notification.
+ If all three attributes 'pushToFollowers', 'pushToPhoneNumbers' and 'pushToEmails'
+ are not supplied, then everyone who follows the channel will receive the push notification.
+ If 'pushToFollowers' is supplied, only those listed in the array will receive the push notification.
+ If one of the userIds supplied does not follow the specified channel, then that userId value will be ignored.
+ See the "Followers" section to learn how to list the userIds of those who follow one of your channels.`, + }, + { + displayName: 'Subtitle', + name: 'subtitle', + type: 'string', + default: '', + description: 'The subtitle of your push. Limited to 20 characters. Only appears on iOS devices.', + }, + { + displayName: 'Title', + name: 'pushTitle', + type: 'string', + default: '', + description: 'The title of push. Appears in bold at the top. Limited to 100 characters.', + }, + { + displayName: 'Schedule', + name: 'schedule', + type: 'dateTime', + default: '', + description: 'A Unix timestamp. Schedule a push to be sent at a later date and time.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Spontit/Spontit.node.ts b/packages/nodes-base/nodes/Spontit/Spontit.node.ts new file mode 100644 index 0000000000..72fd75d596 --- /dev/null +++ b/packages/nodes-base/nodes/Spontit/Spontit.node.ts @@ -0,0 +1,116 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + spontitApiRequest, +} from './GenericFunctions'; + +import { + pushFields, + pushOperations, +} from './PushDescription'; + +import * as moment from 'moment'; + +export class Spontit implements INodeType { + description: INodeTypeDescription = { + displayName: 'Spontit', + name: 'spontit', + icon: 'file:spontit.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Spontit API', + defaults: { + name: 'Spontit', + color: '#00deff', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'spontitApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Push', + value: 'push', + }, + ], + default: 'push', + description: 'The resource to operate on.', + }, + ...pushOperations, + ...pushFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const timezone = this.getTimezone(); + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < items.length; i++) { + if (resource === 'push') { + if (operation === 'create') { + const content = this.getNodeParameter('content', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + content, + }; + + Object.assign(body, additionalFields); + + if (body.pushToFollowers) { + body.pushToFollowers = (body.pushToFollowers as string).split(','); + } + + if (body.pushToPhoneNumbers) { + body.pushToPhoneNumbers = (body.pushToPhoneNumbers as string).split(','); + } + + if (body.pushToEmails) { + body.pushToEmails = (body.pushToEmails as string).split(','); + } + + if (body.schedule) { + body.scheduled = moment.tz(body.schedule, timezone).unix(); + } + + if (body.expirationStamp) { + body.expirationStamp = moment.tz(body.expirationStamp, timezone).unix(); + } + + responseData = await spontitApiRequest.call(this, 'POST', '/push', body); + + responseData = responseData.data; + } + } + 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/Spontit/spontit.png b/packages/nodes-base/nodes/Spontit/spontit.png new file mode 100644 index 0000000000000000000000000000000000000000..ef3981b614f40257d76ee7f8c08c90fda1fd585b GIT binary patch literal 5645 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