diff --git a/packages/nodes-base/credentials/ZendeskApi.credentials.ts b/packages/nodes-base/credentials/ZendeskApi.credentials.ts new file mode 100644 index 0000000000..29048c1172 --- /dev/null +++ b/packages/nodes-base/credentials/ZendeskApi.credentials.ts @@ -0,0 +1,29 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class ZendeskApi implements ICredentialType { + name = 'zendeskApi'; + displayName = 'Zendesk API'; + properties = [ + { + displayName: 'URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Token', + name: 'apiToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts b/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts new file mode 100644 index 0000000000..802ba004f9 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts @@ -0,0 +1,343 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const conditionFields = [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Ticket', + value: 'ticket', + }, + ], + default: 'ticket', + description: '', + }, + { + displayName: 'Field', + name: 'field', + type: 'options', + displayOptions: { + show: { + 'resource': [ + 'ticket' + ] + } + }, + options: [ + { + name: 'Status', + value: 'status', + }, + { + name: 'Type', + value: 'type', + }, + { + name: 'Priority', + value: 'priority', + }, + { + name: 'Group', + value: 'group', + }, + { + name: 'Assignee', + value: 'assignee', + }, + ], + default: 'status', + description: '', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Is', + value: 'is', + }, + { + name: 'Is Not', + value: 'is_not', + }, + { + name: 'Less Than', + value: 'less_than', + }, + { + name: 'Greater Than', + value: 'greater_than', + }, + { + name: 'Changed', + value: 'changed', + }, + { + name: 'Changed To', + value: 'value', + }, + { + name: 'Changed From', + value: 'value_previous', + }, + { + name: 'Not Changed', + value: 'not_changed', + }, + { + name: 'Not Changed To', + value: 'not_value', + }, + { + name: 'Not Changed From', + value: 'not_value_previous', + }, + ], + displayOptions: { + hide: { + field: [ + 'assignee', + ] + } + }, + default: 'is', + description: '', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Is', + value: 'is', + }, + { + name: 'Is Not', + value: 'is_not', + }, + { + name: 'Changed', + value: 'changed', + }, + { + name: 'Changed To', + value: 'value', + }, + { + name: 'Changed From', + value: 'value_previous', + }, + { + name: 'Not Changed', + value: 'not_changed', + }, + { + name: 'Not Changed To', + value: 'not_value', + }, + { + name: 'Not Changed From', + value: 'not_value_previous', + }, + ], + displayOptions: { + show: { + field: [ + 'assignee', + ] + } + }, + default: 'is', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'status' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'priority', + 'type', + ], + } + }, + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: 'open', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'type' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'priority', + 'status', + ], + } + }, + options: [ + { + name: 'Question', + value: 'question', + }, + { + name: 'Incident', + value: 'incident', + }, + { + name: 'Problem', + value: 'problem', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: 'question', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'priority' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'type', + 'status', + ], + } + }, + options: [ + { + name: 'Low', + value: 'low', + }, + { + name: 'Normal', + value: 'normal', + }, + { + name: 'High', + value: 'high', + }, + { + name: 'Urgent', + value: 'urgent', + }, + ], + default: 'low', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + displayOptions: { + show: { + field: [ + 'group' + ], + }, + hide: { + field: [ + 'assignee', + 'priority', + 'type', + 'status', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + field: [ + 'assignee' + ], + }, + hide: { + field: [ + 'group', + 'priority', + 'type', + 'status', + ], + }, + }, + default: '', + description: '', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts new file mode 100644 index 0000000000..0c87d87738 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -0,0 +1,57 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('zendeskApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64') + let options: OptionsWithUri = { + headers: { 'Authorization': `Basic ${base64Key}`}, + method, + qs, + body, + uri: uri ||`${credentials.url}/api/v2${resource}.json`, + json: true + }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + return await this.helpers.request!(options); + } catch (err) { + throw new Error(err); + } +} + +/** + * Make an API request to paginated flow endpoint + * and return all results + */ +export async function zendeskApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + let uri: string | undefined; + + do { + responseData = await zendeskApiRequest.call(this, method, resource, body, query, uri); + uri = responseData.next_page + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.next_page !== undefined && + responseData.next_page !== null + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Zendesk/TicketDescription.ts b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts new file mode 100644 index 0000000000..601beb71fe --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts @@ -0,0 +1,503 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const ticketOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a ticket', + }, + { + name: 'Update', + value: 'update', + description: 'Update a ticket' + }, + { + name: 'Get', + value: 'get', + description: 'Get a ticket' + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all tickets' + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a ticket' + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const ticketFields = [ + +/* -------------------------------------------------------------------------- */ +/* ticket:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + description: 'The first comment on the ticket', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'External ID', + name: 'externalId', + type: 'string', + default: '', + description: 'An id you can use to link Zendesk Support tickets to local records', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: 'The value of the subject field for this ticket', + }, + { + displayName: 'Recipient', + name: 'recipient', + type: 'string', + default: '', + description: 'The original recipient e-mail address of the ticket', + }, + { + displayName: 'Group', + name: 'group', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + default: '', + description: 'The group this ticket is assigned to', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'The array of tags applied to this ticket', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Question', + value: 'question', + }, + { + name: 'Incident', + value: 'incident', + }, + { + name: 'Problem', + value: 'problem', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: '', + description: 'The type of this ticket', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: '', + description: 'The state of the ticket', + } + ], + }, +/* -------------------------------------------------------------------------- */ +/* ticket:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'update', + ], + }, + }, + description: 'Ticket ID', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'External ID', + name: 'externalId', + type: 'string', + default: '', + description: 'An id you can use to link Zendesk Support tickets to local records', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: 'The value of the subject field for this ticket', + }, + { + displayName: 'Recipient', + name: 'recipient', + type: 'string', + default: '', + description: 'The original recipient e-mail address of the ticket', + }, + { + displayName: 'Group', + name: 'group', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + default: '', + description: 'The group this ticket is assigned to', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'The array of tags applied to this ticket', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Question', + value: 'question', + }, + { + name: 'Incident', + value: 'incident', + }, + { + name: 'Problem', + value: 'problem', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: '', + description: 'The type of this ticket', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: '', + description: 'The state of the ticket', + } + ], + }, +/* -------------------------------------------------------------------------- */ +/* ticket:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Ticket ID', + }, +/* -------------------------------------------------------------------------- */ +/* ticket:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: '', + description: 'The state of the ticket', + }, + { + displayName: 'Sort By', + name: 'sortBy', + type: 'options', + options: [ + { + name: 'Updated At', + value: 'updated_at', + }, + { + name: 'Created At', + value: 'created_at', + }, + { + name: 'Priority', + value: 'priority', + }, + { + name: 'Status', + value: 'status', + }, + { + name: 'Ticket Type', + value: 'ticket_type', + }, + ], + default: 'updated_at', + description: 'Defaults to sorting by relevance', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'Asc', + value: 'asc', + }, + { + name: 'Desc', + value: 'desc', + }, + ], + default: 'desc', + description: 'Sort order', + } + ], + }, + +/* -------------------------------------------------------------------------- */ +/* ticket:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'Ticket ID', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zendesk/TicketInterface.ts b/packages/nodes-base/nodes/Zendesk/TicketInterface.ts new file mode 100644 index 0000000000..fc4eb75697 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/TicketInterface.ts @@ -0,0 +1,16 @@ +import { IDataObject } from "n8n-workflow"; + +export interface ITicket { + subject?: string; + comment?: IComment; + type?: string; + group?: string; + external_id?: string; + tags?: string[]; + status?: string; + recipient?: string; +} + +export interface IComment { + body?: string; +} diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts new file mode 100644 index 0000000000..49f91ceb56 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts @@ -0,0 +1,238 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; +import { + zendeskApiRequest, + zendeskApiRequestAllItems, +} from './GenericFunctions'; +import { + ticketFields, + ticketOperations +} from './TicketDescription'; +import { + ITicket, + IComment, + } from './TicketInterface'; + +export class Zendesk implements INodeType { + description: INodeTypeDescription = { + displayName: 'Zendesk', + name: 'zendesk', + icon: 'file:zendesk.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Zendesk API', + defaults: { + name: 'Zendesk', + color: '#13353c', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'zendeskApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Ticket', + value: 'ticket', + description: 'Tickets are the means through which your end users (customers) communicate with agents in Zendesk Support.', + }, + ], + default: 'ticket', + description: 'Resource to consume.', + }, + ...ticketOperations, + ...ticketFields, + ], + }; + + methods = { + loadOptions: { + // Get all the groups to display them to user so that he can + // select them easily + async getGroups(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const groups = await zendeskApiRequestAllItems.call(this, 'groups', 'GET', '/groups'); + for (const group of groups) { + const groupName = group.name; + const groupId = group.id; + returnData.push({ + name: groupName, + value: groupId, + }); + } + return returnData; + }, + // Get all the tags to display them to user so that he can + // select them easily + async getTags(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const tags = await zendeskApiRequestAllItems.call(this, 'tags', 'GET', '/tags'); + for (const tag of tags) { + const tagName = tag.name; + const tagId = tag.name; + returnData.push({ + name: tagName, + value: tagId, + }); + } + return returnData; + }, + } + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let qs: IDataObject = {}; + let responseData; + for (let i = 0; i < length; i++) { + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + //https://developer.zendesk.com/rest_api/docs/support/introduction + if (resource === 'ticket') { + //https://developer.zendesk.com/rest_api/docs/support/tickets + if (operation === 'create') { + const description = this.getNodeParameter('description', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const comment: IComment = { + body: description, + }; + const body: ITicket = { + comment, + }; + if (additionalFields.type) { + body.type = additionalFields.type as string; + } + if (additionalFields.externalId) { + body.external_id = additionalFields.externalId as string; + } + if (additionalFields.subject) { + body.subject = additionalFields.subject as string; + } + if (additionalFields.status) { + body.status = additionalFields.status as string; + } + if (additionalFields.recipient) { + body.recipient = additionalFields.recipient as string; + } + if (additionalFields.group) { + body.group = additionalFields.group as string; + } + if (additionalFields.tags) { + body.tags = additionalFields.tags as string[]; + } + try { + responseData = await zendeskApiRequest.call(this, 'POST', '/tickets', { ticket: body }); + responseData = responseData.ticket; + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + //https://developer.zendesk.com/rest_api/docs/support/tickets#update-ticket + if (operation === 'update') { + const ticketId = this.getNodeParameter('id', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: ITicket = {}; + if (updateFields.type) { + body.type = updateFields.type as string; + } + if (updateFields.externalId) { + body.external_id = updateFields.externalId as string; + } + if (updateFields.subject) { + body.subject = updateFields.subject as string; + } + if (updateFields.status) { + body.status = updateFields.status as string; + } + if (updateFields.recipient) { + body.recipient = updateFields.recipient as string; + } + if (updateFields.group) { + body.group = updateFields.group as string; + } + if (updateFields.tags) { + body.tags = updateFields.tags as string[]; + } + try { + responseData = await zendeskApiRequest.call(this, 'PUT', `/tickets/${ticketId}`, { ticket: body }); + responseData = responseData.ticket; + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + //https://developer.zendesk.com/rest_api/docs/support/tickets#show-ticket + if (operation === 'get') { + const ticketId = this.getNodeParameter('id', i) as string; + try { + responseData = await zendeskApiRequest.call(this, 'GET', `/tickets/${ticketId}`, {}); + responseData = responseData.ticket; + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + //https://developer.zendesk.com/rest_api/docs/support/search#list-search-results + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + qs.query = 'type:ticket' + if (options.status) { + qs.query += ` status:${options.status}` + } + if (options.sortBy) { + qs.sort_by = options.sortBy; + } + if (options.sortOrder) { + qs.sort_order = options.sortOrder; + } + try { + if (returnAll) { + responseData = await zendeskApiRequestAllItems.call(this, 'results', 'GET', `/search`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.per_page = limit; + responseData = await zendeskApiRequest.call(this, 'GET', `/search`, {}, qs); + responseData = responseData.results; + } + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + //https://developer.zendesk.com/rest_api/docs/support/tickets#delete-ticket + if (operation === 'delete') { + const ticketId = this.getNodeParameter('id', i) as string; + try { + responseData = await zendeskApiRequest.call(this, 'DELETE', `/tickets/${ticketId}`, {}); + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts new file mode 100644 index 0000000000..a380fa07be --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -0,0 +1,533 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + INodeTypeDescription, + INodeType, + IWebhookResponseData, + IDataObject, + INodePropertyOptions, + ILoadOptionsFunctions, +} from 'n8n-workflow'; + +import { + zendeskApiRequest, + zendeskApiRequestAllItems, +} from './GenericFunctions'; +import { + conditionFields + } from './ConditionDescription'; + +export class ZendeskTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Zendesk Trigger', + name: 'zendesk', + icon: 'file:zendesk.png', + group: ['trigger'], + version: 1, + description: 'Handle Zendesk events via webhooks', + defaults: { + name: 'Zendesk Trigger', + color: '#13353c', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'zendeskApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Service', + name: 'service', + type: 'options', + required: true, + options: [ + { + name: 'Support', + value: 'support', + } + ], + default: 'support', + description: '', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + displayOptions: { + show: { + service: [ + 'support' + ] + } + }, + required: true, + default: '', + description: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + service: [ + 'support' + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + default: [], + options: [ + { + name: 'Title', + value: 'ticket.title', + description: `Ticket's subject`, + }, + { + name: 'Description', + value: 'ticket.description', + description: `Ticket's description`, + }, + { + name: 'URL', + value: 'ticket.url', + description: `Ticket's URL`, + }, + { + name: 'ID', + value: 'ticket.id', + description: `Ticket's ID`, + }, + { + name: 'External ID', + value: 'ticket.external_id', + description: `Ticket's external ID`, + }, + { + name: 'Via', + value: 'ticket.via', + description: `Ticket's source` + }, + { + name: 'Status', + value: 'ticket.status', + description: `Ticket's status`, + }, + { + name: 'Priority', + value: 'ticket.priority', + description: `Ticket's priority`, + }, + { + name: 'Type', + value: 'ticket.ticket_type', + description: `Ticket's type`, + }, + { + name: 'Group Name', + value: 'ticket.group.name', + description: `Ticket's assigned group`, + }, + { + name: 'Brand Name', + value: 'ticket.brand.name', + description: `Ticket's brand`, + }, + { + name: 'Due Date', + value: 'ticket.due_date', + description: `Ticket's due date (relevant for tickets of type Task)`, + }, + { + name: 'Account', + value: 'ticket.account', + description: `This Zendesk Support's account name`, + }, + { + name: 'Assignee Email', + value: 'ticket.assignee.email', + description: `Ticket assignee email (if any)`, + }, + { + name: 'Assignee Name', + value: 'ticket.assignee.name', + description: `Assignee's full name`, + }, + { + name: 'Assignee First Name', + value: 'ticket.assignee.first_name', + description: `Assignee's first name`, + }, + { + name: 'Assignee Last Name', + value: 'ticket.assignee.last_name', + description: `Assignee's last name`, + }, + { + name: 'Requester Full Name', + value: 'ticket.requester.name', + description: `Requester's full name`, + }, + { + name: 'Requester First Name', + value: 'ticket.requester.first_name', + description: `Requester's first name`, + }, + { + name: 'Requester Last Name', + value: 'ticket.requester.last_name', + description: `Requester's last name`, + }, + { + name: 'Requester Email', + value: 'ticket.requester.email', + description: `Requester's email`, + }, + { + name: 'Requester Language', + value: 'ticket.requester.language', + description: `Requester's language`, + }, + { + name: 'Requester Phone', + value: 'ticket.requester.phone', + description: `Requester's phone number`, + }, + { + name: 'Requester External ID', + value: 'ticket.requester.external_id', + description: `Requester's external ID`, + }, + { + name: 'Requester Field', + value: 'ticket.requester.requester_field', + description: `Name or email`, + }, + { + name: 'Requester Details', + value: 'ticket.requester.details', + description: `Detailed information about the ticket's requester`, + }, + { + name: 'Requester Organization', + value: 'ticket.organization.name', + description: `Requester's organization`, + }, + { + name: `Ticket's Organization External ID`, + value: 'ticket.organization.external_id', + description: `Ticket's organization external ID`, + }, + { + name: `Organization details`, + value: 'ticket.organization.details', + description: `The details about the organization of the ticket's requester`, + }, + { + name: `Organization Note`, + value: 'ticket.organization.notes', + description: `The notes about the organization of the ticket's requester`, + }, + { + name: `Ticket's CCs`, + value: 'ticket.ccs', + description: `Ticket's CCs`, + }, + { + name: `Ticket's CCs names`, + value: 'ticket.cc_names', + description: `Ticket's CCs names`, + }, + { + name: `Ticket's tags`, + value: 'ticket.tags', + description: `Ticket's tags`, + }, + { + name: `Current Holiday Name`, + value: 'ticket.current_holiday_name', + description: `Displays the name of the current holiday on the ticket's schedule`, + }, + { + name: `Current User Name `, + value: 'current_user.name', + description: `Your full name`, + }, + { + name: `Current User First Name `, + value: 'current_user.first_name', + description: 'Your first name', + }, + { + name: `Current User Email `, + value: 'current_user.email', + description: 'Your primary email', + }, + { + name: `Current User Organization Name `, + value: 'current_user.organization.name', + description: 'Your default organization', + }, + { + name: `Current User Organization Details `, + value: 'current_user.organization.details', + description: `Your default organization's details`, + }, + { + name: `Current User Organization Notes `, + value: 'current_user.organization.notes', + description: `Your default organization's note`, + }, + { + name: `Current User Language `, + value: 'current_user.language', + description: `Your chosen language`, + }, + { + name: `Current User External ID `, + value: 'current_user.external_id', + description: 'Your external ID', + }, + { + name: `Current User Notes `, + value: 'current_user.notes', + description: 'Your notes, stored in your profile', + }, + { + name: `Satisfation Current Rating `, + value: 'satisfaction.current_rating', + description: 'The text of the current satisfaction rating', + }, + { + name: `Satisfation Current Comment `, + value: 'satisfaction.current_comment', + description: 'The text of the current satisfaction rating comment``', + }, + ], + }, + ], + placeholder: 'Add Option', + }, + { + displayName: 'Conditions', + name: 'conditions', + placeholder: 'Add Condition', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + service: [ + 'support' + ], + } + }, + description: 'The condition to set.', + default: {}, + options: [ + { + name: 'all', + displayName: 'All', + values: [ + ...conditionFields, + ] + }, + { + name: 'any', + displayName: 'Any', + values: [ + ...conditionFields, + ] + }, + ], + }, + ], + + }; + methods = { + loadOptions: { + // Get all the groups to display them to user so that he can + // select them easily + async getGroups(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const groups = await zendeskApiRequestAllItems.call(this, 'groups', 'GET', '/groups'); + for (const group of groups) { + const groupName = group.name; + const groupId = group.id; + returnData.push({ + name: groupName, + value: groupId, + }); + } + return returnData; + }, + // Get all the users to display them to user so that he can + // select them easily + async getUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const users = await zendeskApiRequestAllItems.call(this, 'users', 'GET', '/users'); + for (const user of users) { + const userName = user.name; + const userId = user.id; + returnData.push({ + name: userName, + value: userId, + }); + } + returnData.push({ + name: 'Current User', + value: 'current_user', + }) + returnData.push({ + name: 'Requester', + value: 'requester_id', + }) + return returnData; + }, + } + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId === undefined) { + return false; + } + const endpoint = `/triggers/${webhookData.webhookId}`; + try { + await zendeskApiRequest.call(this, 'GET', endpoint); + } catch (e) { + return false; + } + return true; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const service = this.getNodeParameter('service') as string; + if (service === 'support') { + const aux: IDataObject = {}; + const message: IDataObject = {}; + const resultAll = [], resultAny = []; + const title = this.getNodeParameter('title') as string; + const conditions = this.getNodeParameter('conditions') as IDataObject; + const options = this.getNodeParameter('options') as IDataObject; + if (Object.keys(conditions).length === 0) { + throw new Error('You must have at least one condition'); + } + if (options.fields) { + // @ts-ignore + for (let field of options.fields) { + // @ts-ignore + message[field] = `{{${field}}}`; + } + } else { + message['ticket.id'] = '{{ticket.id}}' + } + const conditionsAll = conditions.all as [IDataObject]; + if (conditionsAll) { + for (let conditionAll of conditionsAll) { + aux.field = conditionAll.field; + aux.operator = conditionAll.operation; + if (conditionAll.operation !== 'changed' + && conditionAll.operation !== 'not_changed') { + aux.value = conditionAll.value; + } else { + aux.value = null; + } + resultAll.push(aux) + } + } + const conditionsAny = conditions.any as [IDataObject]; + if (conditionsAny) { + for (let conditionAny of conditionsAny) { + aux.field = conditionAny.field; + aux.operator = conditionAny.operation; + if (conditionAny.operation !== 'changed' + && conditionAny.operation !== 'not_changed') { + aux.value = conditionAny.value; + } else { + aux.value = null; + } + resultAny.push(aux) + } + } + const bodyTrigger: IDataObject = { + trigger: { + title, + conditions: { + all: resultAll, + any: resultAny, + }, + actions: [ + { + field: 'notification_target', + value: [], + } + ] + }, + } + const bodyTarget: IDataObject = { + target: { + title: 'N8N webhook', + type: 'http_target', + target_url: webhookUrl, + method: 'POST', + active: true, + content_type: 'application/json', + }, + }; + const { target } = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget); + // @ts-ignore + bodyTrigger.trigger.actions[0].value = [target.id, JSON.stringify(message)]; + const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger); + webhookData.webhookId = trigger.id; + webhookData.targetId = target.id; + } + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + try { + await zendeskApiRequest.call(this, 'DELETE', `/triggers/${webhookData.webhookId}`); + await zendeskApiRequest.call(this, 'DELETE', `/targets/${webhookData.targetId}`); + } catch(error) { + return false; + } + delete webhookData.webhookId; + delete webhookData.targetId + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + return { + workflowData: [ + this.helpers.returnJsonArray(req.body) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Zendesk/zendesk.png b/packages/nodes-base/nodes/Zendesk/zendesk.png new file mode 100644 index 0000000000..f8c5d2e744 Binary files /dev/null and b/packages/nodes-base/nodes/Zendesk/zendesk.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index fa50146007..0ec15ab9d6 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -78,7 +78,8 @@ "dist/credentials/TypeformApi.credentials.js", "dist/credentials/TogglApi.credentials.js", "dist/credentials/VeroApi.credentials.js", - "dist/credentials/WordpressApi.credentials.js" + "dist/credentials/WordpressApi.credentials.js", + "dist/credentials/ZendeskApi.credentials.js" ], "nodes": [ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", @@ -171,7 +172,9 @@ "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Webhook.node.js", "dist/nodes/Wordpress/Wordpress.node.js", - "dist/nodes/Xml.node.js" + "dist/nodes/Xml.node.js", + "dist/nodes/Zendesk/ZendeskTrigger.node.js", + "dist/nodes/Zendesk/Zendesk.node.js" ] }, "devDependencies": {