Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zendesk fix for deprecated targets, replaced with webhooks #3025

Merged
merged 4 commits into from
Apr 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/nodes-base/credentials/ZendeskApi.credentials.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {
ICredentialDataDecryptedObject,
ICredentialTestRequest,
ICredentialType,
IHttpRequestOptions,
INodeProperties,
} from 'n8n-workflow';

Expand Down Expand Up @@ -29,4 +32,17 @@ export class ZendeskApi implements ICredentialType {
default: '',
},
];
async authenticate(credentials: ICredentialDataDecryptedObject, requestOptions: IHttpRequestOptions): Promise<IHttpRequestOptions> {
requestOptions.auth = {
username: `${credentials.email}/token`,
password: credentials.apiToken as string,
};
return requestOptions;
}
test: ICredentialTestRequest = {
request: {
baseURL: '=https://{{$credentials.subdomain}}.zendesk.com/api/v2',
url: '/ticket_fields.json',
},
};
}
42 changes: 24 additions & 18 deletions packages/nodes-base/nodes/Zendesk/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,27 @@ import {
} from 'n8n-core';

import {
IDataObject, NodeApiError, NodeOperationError,
IDataObject,
JsonObject,
NodeApiError,
} 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<any> { // tslint:disable-line:no-any
const authenticationMethod = this.getNodeParameter('authentication', 0);

let credentials;

if (authenticationMethod === 'apiToken') {
credentials = await this.getCredentials('zendeskApi') as { subdomain: string };
} else {
credentials = await this.getCredentials('zendeskOAuth2Api') as { subdomain: string };
}

let options: OptionsWithUri = {
headers: {},
method,
qs,
body,
//@ts-ignore
uri,
uri: uri || getUri(resource, credentials.subdomain),
json: true,
qsStringifyOptions: {
arrayFormat: 'brackets',
Expand All @@ -33,23 +41,13 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions
if (Object.keys(options.body).length === 0) {
delete options.body;
}
try {
if (authenticationMethod === 'apiToken') {
const credentials = await this.getCredentials('zendeskApi');

const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64');
options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`;
options.headers!['Authorization'] = `Basic ${base64Key}`;
return await this.helpers.request!(options);
} else {
const credentials = await this.getCredentials('zendeskOAuth2Api');

options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`;
const credentialType = authenticationMethod === 'apiToken' ? 'zendeskApi' : 'zendeskOAuth2Api';

return await this.helpers.requestOAuth2!.call(this, 'zendeskOAuth2Api', options);
}
try {
return await this.helpers.requestWithAuthentication.call(this, credentialType, options);
} catch(error) {
throw new NodeApiError(this.getNode(), error);
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}

Expand Down Expand Up @@ -89,3 +87,11 @@ export function validateJSON(json: string | undefined): any { // tslint:disable-
}
return result;
}

function getUri(resource: string, subdomain: string) {
if (resource.includes('webhooks')) {
return `https://${subdomain}.zendesk.com/api/v2${resource}`;
} else {
return `https://${subdomain}.zendesk.com/api/v2${resource}.json`;
}
}
37 changes: 0 additions & 37 deletions packages/nodes-base/nodes/Zendesk/Zendesk.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ export class Zendesk implements INodeType {
],
},
},
testedBy: 'zendeskSoftwareApiTest',
},
{
name: 'zendeskOAuth2Api',
Expand Down Expand Up @@ -153,42 +152,6 @@ export class Zendesk implements INodeType {
};

methods = {
credentialTest: {
async zendeskSoftwareApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
const credentials = credential.data;
const subdomain = credentials!.subdomain;
const email = credentials!.email;
const apiToken = credentials!.apiToken;

const base64Key = Buffer.from(`${email}/token:${apiToken}`).toString('base64');
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${base64Key}`,
},
method: 'GET',
uri: `https://${subdomain}.zendesk.com/api/v2/ticket_fields.json`,
qs: {
recent: 0,
},
json: true,
timeout: 5000,
};

try {
await this.helpers.request!(options);
} catch (error) {
return {
status: 'Error',
message: `Connection details not valid: ${error.message}`,
};
}
return {
status: 'OK',
message: 'Authentication successful!',
};
},
},
loadOptions: {
// Get all the custom fields to display them to user so that he can
// select them easily
Expand Down
58 changes: 34 additions & 24 deletions packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,11 @@ export class ZendeskTrigger implements INodeType {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const webhookData = this.getWorkflowStaticData('node');
const conditions = this.getNodeParameter('conditions') as IDataObject;
const conditionsAll = conditions.all as [IDataObject];

let endpoint = '';
const resultAll = [], resultAny = [];

const conditionsAll = conditions.all as [IDataObject];
if (conditionsAll) {
for (const conditionAll of conditionsAll) {
const aux: IDataObject = {};
Expand Down Expand Up @@ -282,12 +282,12 @@ export class ZendeskTrigger implements INodeType {
}
}

// check if there is a target already created
endpoint = `/targets`;
const targets = await zendeskApiRequestAllItems.call(this, 'targets', 'GET', endpoint);
for (const target of targets) {
if (target.target_url === webhookUrl) {
webhookData.targetId = target.id.toString();
// get all webhooks
// https://developer.zendesk.com/api-reference/event-connectors/webhooks/webhooks/#list-webhooks
const { webhooks } = await zendeskApiRequest.call(this, 'GET', '/webhooks');
for (const webhook of webhooks) {
if (webhook.endpoint === webhookUrl) {
webhookData.targetId = webhook.id;
break;
}
}
Expand All @@ -299,6 +299,7 @@ export class ZendeskTrigger implements INodeType {

endpoint = `/triggers/active`;
const triggers = await zendeskApiRequestAllItems.call(this, 'triggers', 'GET', endpoint);

for (const trigger of triggers) {
const toDeleteTriggers = [];
// this trigger belong to the current target
Expand All @@ -317,23 +318,26 @@ export class ZendeskTrigger implements INodeType {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const webhookData = this.getWorkflowStaticData('node');
const service = this.getNodeParameter('service') as string;

if (service === 'support') {
const message: IDataObject = {};
const resultAll = [], resultAny = [];
const conditions = this.getNodeParameter('conditions') as IDataObject;
const options = this.getNodeParameter('options') as IDataObject;

if (Object.keys(conditions).length === 0) {
throw new NodeOperationError(this.getNode(), 'You must have at least one condition');
}

if (options.fields) {
// @ts-ignore
for (const field of options.fields) {
for (const field of options.fields as string[]) {
// @ts-ignore
message[field] = `{{${field}}}`;
}
} else {
message['ticket.id'] = '{{ticket.id}}';
}

const conditionsAll = conditions.all as [IDataObject];
if (conditionsAll) {
for (const conditionAll of conditionsAll) {
Expand All @@ -349,6 +353,7 @@ export class ZendeskTrigger implements INodeType {
resultAll.push(aux);
}
}

const conditionsAny = conditions.any as [IDataObject];
if (conditionsAny) {
for (const conditionAny of conditionsAny) {
Expand All @@ -364,30 +369,35 @@ export class ZendeskTrigger implements INodeType {
resultAny.push(aux);
}
}
const urlParts = urlParse(webhookUrl);

const urlParts = new URL(webhookUrl);

const bodyTrigger: IDataObject = {
trigger: {
title: `n8n-webhook:${urlParts.path}`,
title: `n8n-webhook:${urlParts.pathname}`,
conditions: {
all: resultAll,
any: resultAny,
},
actions: [
{
field: 'notification_target',
field: 'notification_webhook',
value: [],
},
],
},
};

const bodyTarget: IDataObject = {
target: {
title: 'n8n webhook',
type: 'http_target',
target_url: webhookUrl,
method: 'POST',
active: true,
content_type: 'application/json',
webhook: {
name:'n8n webhook',
endpoint: webhookUrl,
http_method:'POST',
status:'active',
request_format:'json',
subscriptions: [
'conditional_ticket_events',
],
},
};
let target: IDataObject = {};
Expand All @@ -397,14 +407,14 @@ export class ZendeskTrigger implements INodeType {
if (webhookData.targetId !== undefined) {
target.id = webhookData.targetId;
} else {
target = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget);
target = target.target as IDataObject;
// create a webhook
// https://developer.zendesk.com/api-reference/event-connectors/webhooks/webhooks/#create-or-clone-webhook
target = (await zendeskApiRequest.call(this, 'POST', '/webhooks', bodyTarget)).webhook as IDataObject;
}

// @ts-ignore
bodyTrigger.trigger.actions[0].value = [target.id, JSON.stringify(message)];

//@ts-ignore
const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger);
webhookData.webhookId = trigger.id;
webhookData.targetId = target.id;
Expand All @@ -415,11 +425,11 @@ export class ZendeskTrigger implements INodeType {
const webhookData = this.getWorkflowStaticData('node');
try {
await zendeskApiRequest.call(this, 'DELETE', `/triggers/${webhookData.webhookId}`);
await zendeskApiRequest.call(this, 'DELETE', `/targets/${webhookData.targetId}`);
await zendeskApiRequest.call(this, 'DELETE', `/webhooks/${webhookData.targetId}`);
} catch(error) {
return false;
}
delete webhookData.webhookId;
delete webhookData.triggerId;
delete webhookData.targetId;
return true;
},
Expand Down