diff --git a/packages/nodes-base/credentials/TwakeCloudApi.credentials.ts b/packages/nodes-base/credentials/TwakeCloudApi.credentials.ts new file mode 100644 index 0000000000..8cd47128aa --- /dev/null +++ b/packages/nodes-base/credentials/TwakeCloudApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class TwakeCloudApi implements ICredentialType { + name = 'twakeCloudApi'; + displayName = 'Twake API'; + properties = [ + { + displayName: 'Workspace Key', + name: 'workspaceKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/credentials/TwakeServerApi.credentials.ts b/packages/nodes-base/credentials/TwakeServerApi.credentials.ts new file mode 100644 index 0000000000..b1377ce743 --- /dev/null +++ b/packages/nodes-base/credentials/TwakeServerApi.credentials.ts @@ -0,0 +1,29 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class TwakeServerApi implements ICredentialType { + name = 'twakeServerApi'; + displayName = 'Twake API'; + properties = [ + { + displayName: 'Host URL', + name: 'hostUrl', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Public ID', + name: 'publicId', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Private API Key', + name: 'privateApiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Twake/GenericFunctions.ts b/packages/nodes-base/nodes/Twake/GenericFunctions.ts new file mode 100644 index 0000000000..84aaf9e20b --- /dev/null +++ b/packages/nodes-base/nodes/Twake/GenericFunctions.ts @@ -0,0 +1,67 @@ +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + OptionsWithUri, +} from 'request'; +/** + * Make an API request to Twake + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} url + * @param {object} body + * @returns {Promise} + */ +export async function twakeApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: object, query?: object, uri?: string): Promise { // tslint:disable-line:no-any + + const authenticationMethod = this.getNodeParameter('twakeVersion', 0, 'twakeCloudApi') as string; + + const options: OptionsWithUri = { + headers: {}, + method, + body, + qs: query, + uri: uri || `https://connectors.albatros.twakeapp.com/n8n${resource}`, + json: true, + }; + + + if (authenticationMethod === 'cloud') { + const credentials = this.getCredentials('twakeCloudApi'); + options.headers!.Authorization = `Bearer ${credentials!.workspaceKey}`; + + } else { + + const credentials = this.getCredentials('twakeServerApi'); + options.auth = { user: credentials!.publicId as string, pass: credentials!.privateApiKey as string }; + options.uri = `${credentials!.hostUrl}/api/v1${resource}`; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + if( error.error.code === "ECONNREFUSED"){ + throw new Error('Twake host is not accessible!'); + + } + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The Twake credentials are not valid!'); + } + + if (error.response && error.response.body && error.response.body.errors) { + // Try to return the error prettier + const errorMessages = error.response.body.errors.map((errorData: { message: string }) => { + return errorData.message; + }); + throw new Error(`Twake error response [${error.statusCode}]: ${errorMessages.join(' | ')}`); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} diff --git a/packages/nodes-base/nodes/Twake/Twake.node.ts b/packages/nodes-base/nodes/Twake/Twake.node.ts new file mode 100644 index 0000000000..d890e6b6cd --- /dev/null +++ b/packages/nodes-base/nodes/Twake/Twake.node.ts @@ -0,0 +1,247 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + INodeExecutionData, + IDataObject, + INodeType, + INodeTypeDescription, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + twakeApiRequest, +} from './GenericFunctions'; + +export class Twake implements INodeType { + description: INodeTypeDescription = { + displayName: 'Twake', + name: 'twake', + group: ['transform'], + version: 1, + icon: 'file:twake.png', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Twake API', + defaults: { + name: 'Twake', + color: '#7168ee', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'twakeCloudApi', + required: true, + displayOptions: { + show: { + twakeVersion: [ + 'cloud', + ], + }, + }, + }, + { + name: 'twakeServerApi', + required: true, + displayOptions: { + show: { + twakeVersion: [ + 'server', + ], + }, + }, + }, + ], + properties: [ + { + displayName: 'Twake Version', + name: 'twakeVersion', + type: 'options', + options: [ + { + name: 'Cloud', + value: 'cloud', + }, + // { + // name: 'Server (Self Hosted)', + // value: 'server', + // }, + ], + default: 'cloud', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Message', + value: 'message', + description: 'Send data to the message app', + }, + ], + default: 'message', + description: 'The operation to perform.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'message', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Send a message', + }, + ], + default: 'send', + description: 'The operation to perform.', + }, + { + displayName: 'Channel ID', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'send', + ], + }, + }, + default: '', + description: `Channel's ID`, + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + }, + }, + default: '', + description: 'Message content', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'send', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Sender Icon', + name: 'senderIcon', + type: 'string', + default: '', + description: 'URL of the image/icon', + }, + { + displayName: 'Sender Name', + name: 'senderName', + type: 'string', + default: '', + description: 'Sender name', + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + async getChannels(this: ILoadOptionsFunctions): Promise { + const responseData = await twakeApiRequest.call(this, 'POST', '/channel', {}); + if (responseData === undefined) { + throw new Error('No data got returned'); + } + + const returnData: INodePropertyOptions[] = []; + for (const channel of responseData) { + returnData.push({ + name: channel.name, + value: channel.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 === 'message') { + if (operation === 'send') { + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const message: IDataObject = { + channel_id: this.getNodeParameter('channelId', i), + content: { + formatted: this.getNodeParameter('content', i) as string, + }, + hidden_data: { + allow_delete: 'everyone', + }, + }; + + if (additionalFields.senderName) { + //@ts-ignore + message.hidden_data!.custom_title = additionalFields.senderName as string; + } + + if (additionalFields.senderIcon) { + //@ts-ignore + message.hidden_data!.custom_icon = additionalFields.senderIcon as string; + } + + const body = { + object: message, + }; + + const endpoint = '/actions/message/save'; + + responseData = await twakeApiRequest.call(this, 'POST', endpoint, body); + + responseData = responseData.object; + } + } + } + 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/Twake/twake.png b/packages/nodes-base/nodes/Twake/twake.png new file mode 100644 index 0000000000..3b8408938b Binary files /dev/null and b/packages/nodes-base/nodes/Twake/twake.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b2fd3c01c6..23ef2c5ec2 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -149,6 +149,8 @@ "dist/credentials/TypeformApi.credentials.js", "dist/credentials/TypeformOAuth2Api.credentials.js", "dist/credentials/TogglApi.credentials.js", + "dist/credentials/TwakeCloudApi.credentials.js", + "dist/credentials/TwakeServerApi.credentials.js", "dist/credentials/UpleadApi.credentials.js", "dist/credentials/VeroApi.credentials.js", "dist/credentials/WebflowApi.credentials.js", @@ -318,6 +320,7 @@ "dist/nodes/Twilio/Twilio.node.js", "dist/nodes/Twitter/Twitter.node.js", "dist/nodes/Typeform/TypeformTrigger.node.js", + "dist/nodes/Twake/Twake.node.js", "dist/nodes/Uplead/Uplead.node.js", "dist/nodes/Vero/Vero.node.js", "dist/nodes/Webflow/WebflowTrigger.node.js",