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/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/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 0343726121..07b986b4d7 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -51,6 +51,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", @@ -58,6 +59,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": [ @@ -105,6 +109,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", @@ -126,6 +131,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" ] },