diff --git a/packages/cli/package.json b/packages/cli/package.json index a7798fab7a..eb6d08b804 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.34.0", + "version": "0.35.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -89,9 +89,9 @@ "jwks-rsa": "^1.6.0", "localtunnel": "^1.9.1", "mongodb": "^3.2.3", - "n8n-core": "~0.14.0", + "n8n-core": "~0.15.0", "n8n-editor-ui": "~0.25.0", - "n8n-nodes-base": "~0.29.0", + "n8n-nodes-base": "~0.30.0", "n8n-workflow": "~0.15.0", "open": "^6.1.0", "pg": "^7.11.0", diff --git a/packages/core/package.json b/packages/core/package.json index 9ae72a2fdf..7d5f6c7833 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.14.0", + "version": "0.15.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/nodes-base/credentials/RocketchatApi.credentials.ts b/packages/nodes-base/credentials/RocketchatApi.credentials.ts new file mode 100644 index 0000000000..0231d5a72b --- /dev/null +++ b/packages/nodes-base/credentials/RocketchatApi.credentials.ts @@ -0,0 +1,31 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class RocketchatApi implements ICredentialType { + name = 'rocketchatApi'; + displayName = 'Rocket API'; + properties = [ + { + displayName: 'User Id', + name: 'userId', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth Key', + name: 'authKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Sub Domain', + name: 'subdomain', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'n8n' + }, + ]; +} diff --git a/packages/nodes-base/nodes/Cron.node.ts b/packages/nodes-base/nodes/Cron.node.ts index e361929531..90ad6214ce 100644 --- a/packages/nodes-base/nodes/Cron.node.ts +++ b/packages/nodes-base/nodes/Cron.node.ts @@ -53,6 +53,10 @@ export class Cron implements INodeType { name: 'mode', type: 'options', options: [ + { + name: 'Every Minute', + value: 'everyMinute' + }, { name: 'Every Hour', value: 'everyHour' @@ -90,6 +94,7 @@ export class Cron implements INodeType { mode: [ 'custom', 'everyHour', + 'everyMinute' ], }, }, @@ -108,6 +113,7 @@ export class Cron implements INodeType { hide: { mode: [ 'custom', + 'everyMinute' ], }, }, @@ -226,6 +232,10 @@ export class Cron implements INodeType { cronTimes.push(item.cronExpression as string); continue; } + if (item.mode === 'everyMinute') { + cronTimes.push(`${Math.floor(Math.random() * 60).toString()} * * * * *`); + continue; + } for (parameterName of parameterOrder) { if (item[parameterName] !== undefined) { diff --git a/packages/nodes-base/nodes/Freshdesk/Freshdesk.node.ts b/packages/nodes-base/nodes/Freshdesk/Freshdesk.node.ts index ca64589ba5..7cbc55b928 100644 --- a/packages/nodes-base/nodes/Freshdesk/Freshdesk.node.ts +++ b/packages/nodes-base/nodes/Freshdesk/Freshdesk.node.ts @@ -36,7 +36,7 @@ enum Source { Chat = 7, Mobihelp = 8, FeedbackWidget = 9, - OutboundEmail = 10 + OutboundEmail = 10, } interface ICreateTicketBody { @@ -683,6 +683,7 @@ export class Freshdesk implements INodeType { // @ts-ignore source: Source[capitalize(source)] }; + if (requester === 'requesterId') { // @ts-ignore if (isNaN(value)) { diff --git a/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts b/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts index 4b3d6d7545..ada88cd067 100644 --- a/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts +++ b/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts @@ -349,7 +349,7 @@ export class NextCloud implements INodeType { displayOptions: { show: { binaryDataUpload: [ - true + false ], operation: [ 'upload' diff --git a/packages/nodes-base/nodes/Redis/Redis.node.ts b/packages/nodes-base/nodes/Redis/Redis.node.ts index 5e62d2f36b..f3a574a522 100644 --- a/packages/nodes-base/nodes/Redis/Redis.node.ts +++ b/packages/nodes-base/nodes/Redis/Redis.node.ts @@ -303,9 +303,6 @@ export class Redis implements INodeType { type = await clientType(keyName); } - console.log(keyName + ': ' + type); - - if (type === 'string') { const clientGet = util.promisify(client.get).bind(client); return await clientGet(keyName); @@ -394,6 +391,7 @@ export class Redis implements INodeType { } else if (['delete', 'get', 'keys', 'set'].includes(operation)) { const items = this.getInputData(); + const returnItems: INodeExecutionData[] = []; let item: INodeExecutionData; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { @@ -405,6 +403,7 @@ export class Redis implements INodeType { const clientDel = util.promisify(client.del).bind(client); // @ts-ignore await clientDel(keyDelete); + returnItems.push(items[itemIndex]); } else if (operation === 'get') { const propertyName = this.getNodeParameter('propertyName', itemIndex) as string; const keyGet = this.getNodeParameter('key', itemIndex) as string; @@ -412,6 +411,7 @@ export class Redis implements INodeType { const value = await getValue(client, keyGet, keyType); set(item.json, propertyName, value); + returnItems.push(item); } else if (operation === 'keys') { const keyPattern = this.getNodeParameter('keyPattern', itemIndex) as string; @@ -424,23 +424,23 @@ export class Redis implements INodeType { for (const keyName of keys) { promises[keyName] = await getValue(client, keyName); - console.log(promises[keyName]); - } for (const keyName of keys) { set(item.json, keyName, await promises[keyName]); } + returnItems.push(item); } else if (operation === 'set') { const keySet = this.getNodeParameter('key', itemIndex) as string; const value = this.getNodeParameter('value', itemIndex) as string; const keyType = this.getNodeParameter('keyType', itemIndex) as string; await setValue(client, keySet, value, keyType); + returnItems.push(items[itemIndex]); } } - resolve(this.prepareOutputData(items)); + resolve(this.prepareOutputData(returnItems)); } }); }); diff --git a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts new file mode 100644 index 0000000000..b058e67e6c --- /dev/null +++ b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts @@ -0,0 +1,55 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, +} from 'n8n-core'; + +export async function rocketchatApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, resource: string, method: string, operation: string, body: any = {}, headers?: object): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('rocketchatApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const headerWithAuthentication = Object.assign({}, headers, + { 'X-Auth-Token': credentials.authKey, 'X-User-Id': credentials.userId }); + + const endpoint = 'rocket.chat/api/v1'; + + const options: OptionsWithUri = { + headers: headerWithAuthentication, + method, + body, + uri: `https://${credentials.subdomain}.${endpoint}${resource}.${operation}`, + json: true + }; + + if (Object.keys(options.body).length === 0) { + delete options.body; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + console.error(error); + + const errorMessage = error.response.body.message || error.response.body.Message; + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} + +export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = []; + } + return result; +} diff --git a/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts b/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts new file mode 100644 index 0000000000..5b05f8cb70 --- /dev/null +++ b/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts @@ -0,0 +1,504 @@ +import { + IExecuteSingleFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType +} from 'n8n-workflow'; + import { + rocketchatApiRequest, + validateJSON +} from './GenericFunctions'; + +interface IField { + short?: boolean; + title?: string; + value?: string; +} + +interface IAttachment { + color?: string; + text?: string; + ts?: string; + title?: string; + thumb_url?: string; + message_link?: string; + collapsed?: boolean; + author_name?: string; + author_link?: string; + author_icon?: string; + title_link?: string; + title_link_download?: boolean; + image_url?: string; + audio_url?: string; + video_url?: string; + fields?: IField[]; +} + +interface IPostMessageBody { + channel: string; + text?: string; + alias?: string; + emoji?: string; + avatar?: string; + attachments?: IAttachment[]; +} + +export class Rocketchat implements INodeType { + description: INodeTypeDescription = { + displayName: 'Rocketchat', + name: 'Rocketchat', + icon: 'file:rocketchat.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Consume Rocketchat API', + defaults: { + name: 'Rocketchat', + color: '#c02428', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'rocketchatApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Chat', + value: 'chat', + }, + ], + default: 'chat', + description: 'The resource to operate on.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'chat', + ], + }, + }, + options: [ + { + name: 'Post Message', + value: 'postMessage', + description: 'Post a message to a channel or a direct message', + }, + ], + default: 'postMessage', + description: 'The operation to perform.', + }, + { + displayName: 'Channel', + name: 'channel', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'chat', + ], + operation: [ + 'postMessage' + ] + }, + }, + default: '', + description: 'The channel name with the prefix in front of it.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + displayOptions: { + show: { + resource: [ + 'chat', + ], + operation: [ + 'postMessage' + ] + }, + }, + default: '', + description: 'The text of the message to send, is optional because of attachments.', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'chat' + ], + operation: [ + 'postMessage', + ] + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'chat', + ], + operation: [ + 'postMessage', + ], + }, + }, + options: [ + { + displayName: 'Alias', + name: 'alias', + type: 'string', + default: '', + description: 'This will cause the message’s name to appear as the given alias, but your username will still display.', + }, + { + displayName: 'Avatar', + name: 'avatar', + type: 'string', + default: '', + description: 'If provided, this will make the avatar use the provided image url.', + }, + { + displayName: 'Emoji', + name: 'emoji', + type: 'string', + default: '', + description: 'This will cause the message’s name to appear as the given alias, but your username will still display.', + } + ] + }, + { + displayName: 'Attachments', + name: 'attachments', + type: 'collection', + default: {}, + placeholder: 'Add Attachment Item', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Attachment', + + }, + displayOptions: { + show: { + resource: [ + 'chat', + ], + operation: [ + 'postMessage', + ], + jsonParameters: [ + false + ], + }, + }, + options: [ + { + displayName: 'Color', + name: 'color', + type: 'color', + default: '#ff0000', + description: 'The color you want the order on the left side to be, any value background-css supports.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The text to display for this attachment, it is different than the message’s text.', + }, + { + displayName: 'Timestamp', + name: 'ts', + type: 'dateTime', + default: '', + description: 'Displays the time next to the text portion.', + }, + { + displayName: 'Thumb URL', + name: 'thumbUrl', + type: 'string', + default: '', + description: 'An image that displays to the left of the text, looks better when this is relatively small.', + }, + { + displayName: 'Message Link', + name: 'messageLink', + type: 'string', + default: '', + description: 'Only applicable if the timestamp is provided, as it makes the time clickable to this link.', + }, + { + displayName: 'Collapsed', + name: 'collapsed', + type: 'boolean', + default: false, + description: 'Causes the image, audio, and video sections to be hiding when collapsed is true.', + }, + { + displayName: 'Author Name', + name: 'authorName', + type: 'string', + default: '', + description: 'Name of the author.', + }, + { + displayName: 'Author Link', + name: 'authorLink', + type: 'string', + default: '', + description: 'Providing this makes the author name clickable and points to this link.', + }, + { + displayName: 'Author Icon', + name: 'authorIcon', + type: 'string', + default: '', + placeholder: 'https://site.com/img.png', + description: 'Displays a tiny icon to the left of the Author’s name.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title to display for this attachment, displays under the author.', + }, + { + displayName: 'Title Link', + name: 'titleLink', + type: 'string', + default: '', + description: 'Providing this makes the title clickable, pointing to this link.', + }, + { + displayName: 'Title Link Download', + name: 'titleLinkDownload', + type: 'boolean', + default: false, + description: 'When this is true, a download icon appears and clicking this saves the link to file.', + }, + { + displayName: 'Image URL', + name: 'imageUrl', + type: 'string', + default: '', + description: 'The image to display, will be “big” and easy to see.', + }, + { + displayName: 'Audio URL', + name: 'audioUrl', + type: 'string', + default: '', + placeholder: 'https://site.com/aud.mp3', + description: 'Audio file to play, only supports what html audio does.', + }, + { + displayName: 'video URL', + name: 'videoUrl', + type: 'string', + default: '', + placeholder: 'https://site.com/vid.mp4', + description: 'Video file to play, only supports what html video does.', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'fixedCollection', + placeholder: 'Add Field Item', + typeOptions: { + multipleValues: true, + }, + default: '', + options: [ + { + name: 'fieldsValues', + displayName: 'Fields', + values: [ + { + displayName: 'Short', + name: 'short', + type: 'boolean', + default: false, + description: 'Whether this field should be a short field.' + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'The title of this field.' + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value of this field, displayed underneath the title value.' + }, + ], + }, + ], + }, + ] + }, + { + displayName: 'Attachments', + name: 'attachmentsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'chat' + ], + operation: [ + 'postMessage' + ], + jsonParameters: [ + true + ], + }, + }, + default: '', + required: false, + description: '', + } + ] + }; + + async executeSingle(this: IExecuteSingleFunctions): Promise { + const resource = this.getNodeParameter('resource') as string; + const opeation = this.getNodeParameter('operation') as string; + let response; + + if (resource === 'chat') { + //https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage + if (opeation === 'postMessage') { + const channel = this.getNodeParameter('channel') as string; + const text = this.getNodeParameter('text') as string; + const options = this.getNodeParameter('options') as IDataObject; + const jsonActive = this.getNodeParameter('jsonParameters') as boolean; + + const body: IPostMessageBody = { + channel, + text, + }; + + if (options.alias) { + body.alias = options.alias as string; + } + if (options.avatar) { + body.avatar = options.avatar as string; + } + if (options.emoji) { + body.emoji = options.emoji as string; + } + + if (!jsonActive) { + const optionsAttachments = this.getNodeParameter('attachments') as IDataObject[]; + if (optionsAttachments.length > 0) { + const attachments: IAttachment[] = []; + for (let i = 0; i < optionsAttachments.length; i++) { + const attachment: IAttachment = {}; + for (const option of Object.keys(optionsAttachments[i])) { + if (option === 'color') { + attachment.color = optionsAttachments[i][option] as string; + } else if (option === 'text') { + attachment.text = optionsAttachments[i][option] as string; + } else if (option === 'ts') { + attachment.ts = optionsAttachments[i][option] as string; + } else if (option === 'messageLinks') { + attachment.message_link = optionsAttachments[i][option] as string; + } else if (option === 'thumbUrl') { + attachment.thumb_url = optionsAttachments[i][option] as string; + } else if (option === 'collapsed') { + attachment.collapsed = optionsAttachments[i][option] as boolean; + } else if (option === 'authorName') { + attachment.author_name = optionsAttachments[i][option] as string; + } else if (option === 'authorLink') { + attachment.author_link = optionsAttachments[i][option] as string; + } else if (option === 'authorIcon') { + attachment.author_icon = optionsAttachments[i][option] as string; + } else if (option === 'title') { + attachment.title = optionsAttachments[i][option] as string; + } else if (option === 'titleLink') { + attachment.title_link = optionsAttachments[i][option] as string; + } else if (option === 'titleLinkDownload') { + attachment.title_link_download = optionsAttachments[i][option] as boolean; + } else if (option === 'imageUrl') { + attachment.image_url = optionsAttachments[i][option] as string; + } else if (option === 'audioUrl') { + attachment.audio_url = optionsAttachments[i][option] as string; + } else if (option === 'videoUrl') { + attachment.video_url = optionsAttachments[i][option] as string; + } else if (option === 'fields') { + const fieldsValues = (optionsAttachments[i][option] as IDataObject).fieldsValues as IDataObject[]; + if (fieldsValues.length > 0) { + const fields: IField[] = []; + for (let i = 0; i < fieldsValues.length; i++) { + const field: IField = {}; + for (const key of Object.keys(fieldsValues[i])) { + if (key === 'short') { + field.short = fieldsValues[i][key] as boolean; + } else if (key === 'title') { + field.title = fieldsValues[i][key] as string; + } else if (key === 'value') { + field.value = fieldsValues[i][key] as string; + } + } + fields.push(field); + attachment.fields = fields; + } + } + } + } + attachments.push(attachment); + } + body.attachments = attachments; + } + } else { + body.attachments = validateJSON(this.getNodeParameter('attachmentsJson') as string); + } + + try { + response = await rocketchatApiRequest.call(this, '/chat', 'POST', 'postMessage', body); + } catch (err) { + throw new Error(`Rocketchat Error: ${err}`); + } + } + } + + return { + json: response + }; + } +} diff --git a/packages/nodes-base/nodes/Rocketchat/rocketchat.png b/packages/nodes-base/nodes/Rocketchat/rocketchat.png new file mode 100644 index 0000000000..7d29851295 Binary files /dev/null and b/packages/nodes-base/nodes/Rocketchat/rocketchat.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 6de583c5b3..4c1afc425f 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.29.0", + "version": "0.30.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -52,6 +52,7 @@ "dist/credentials/PipedriveApi.credentials.js", "dist/credentials/Postgres.credentials.js", "dist/credentials/Redis.credentials.js", + "dist/credentials/RocketchatApi.credentials.js", "dist/credentials/SlackApi.credentials.js", "dist/credentials/Smtp.credentials.js", "dist/credentials/StripeApi.credentials.js", @@ -59,6 +60,9 @@ "dist/credentials/TodoistApi.credentials.js", "dist/credentials/TrelloApi.credentials.js", "dist/credentials/TwilioApi.credentials.js", + "dist/credentials/TypeformApi.credentials.js", + "dist/credentials/MandrillApi.credentials.js", + "dist/credentials/TodoistApi.credentials.js", "dist/credentials/TypeformApi.credentials.js" ], "nodes": [ @@ -107,6 +111,7 @@ "dist/nodes/Pipedrive/Pipedrive.node.js", "dist/nodes/Pipedrive/PipedriveTrigger.node.js", "dist/nodes/Postgres/Postgres.node.js", + "dist/nodes/Rocketchat/Rocketchat.node.js", "dist/nodes/ReadBinaryFile.node.js", "dist/nodes/ReadBinaryFiles.node.js", "dist/nodes/Redis/Redis.node.js", @@ -128,6 +133,9 @@ "dist/nodes/Typeform/TypeformTrigger.node.js", "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Webhook.node.js", + "dist/nodes/Xml.node.js", + "dist/nodes/Mandrill/Mandrill.node.js", + "dist/nodes/Todoist/Todoist.node.js", "dist/nodes/Xml.node.js" ] }, @@ -165,7 +173,7 @@ "lodash.set": "^4.3.2", "lodash.unset": "^4.5.2", "mongodb": "^3.3.2", - "n8n-core": "~0.14.0", + "n8n-core": "~0.15.0", "nodemailer": "^5.1.1", "pdf-parse": "^1.1.1", "pg-promise": "^9.0.3",