diff --git a/manifest.json b/manifest.json index 4fb9142..4906835 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "docs-in-tickets", "vendor": "vtexhelp", - "version": "1.0.3", + "version": "1.0.4", "title": "Docs in Tickets", "description": "Process information about documentation links sent in support tickets.", "categories": [], @@ -28,6 +28,13 @@ "path": "*" } }, + { + "name": "outbound-access", + "attrs": { + "host": "hooks.slack.com", + "path": "*" + } + }, { "name": "outbound-access", "attrs": { diff --git a/node/clients/index.ts b/node/clients/index.ts index 040b56a..c8a06f6 100644 --- a/node/clients/index.ts +++ b/node/clients/index.ts @@ -2,6 +2,7 @@ import { IOClients } from '@vtex/api' import RedshiftClient from './redshift' import ZendeskClient from './zendesk' +import SlackClient from './slack' // Extend the default IOClients implementation with our own custom clients. export class Clients extends IOClients { @@ -12,4 +13,8 @@ export class Clients extends IOClients { public get zendesk() { return this.getOrSet('zendesk', ZendeskClient) } + + public get slack() { + return this.getOrSet('slack', SlackClient) + } } diff --git a/node/clients/slack.ts b/node/clients/slack.ts new file mode 100644 index 0000000..b3c015b --- /dev/null +++ b/node/clients/slack.ts @@ -0,0 +1,41 @@ +// sendLog sends a message to the slack channel #docs-in-tickets-logs. +// ref: https://api.slack.com/messaging/webhooks + +import { ExternalClient } from '@vtex/api' +import type { IOContext, InstanceOptions } from '@vtex/api' + +const url = 'https://hooks.slack.com/services/xxxxxxxxxx' +const requestHeaders = { + 'Content-Type': 'application/json', +} + +export default class SlackClient extends ExternalClient { + constructor(ctx: IOContext, options?: InstanceOptions) { + super('https://hooks.slack.com', ctx, { + ...options, + retries: 2, + timeout: 2000, + headers: requestHeaders, + }) + } + + public async sendLog(log: string, type: string) { + + const logTypeMap: { [key: string]: string} = { + info: ':ms_information_3d:', + warning: ':warning: *Warning*', + error: ':error: *Error*' + } + const typeMarker = logTypeMap[type] || '' + + return this.http.post( + url, + { + text: `${typeMarker}\n${log}` + }, + { + headers: requestHeaders, + } + ) + } +} diff --git a/node/clients/zendesk.ts b/node/clients/zendesk.ts index daba282..6975083 100644 --- a/node/clients/zendesk.ts +++ b/node/clients/zendesk.ts @@ -2,13 +2,14 @@ import { ExternalClient, IOContext, InstanceOptions } from '@vtex/api' -const username = 'xxxxxxxxxxxxxxxxx' -const token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' +// These are the real credentials +const username = 'xxxxxxxxxx' +const token = 'xxxxxxxxxx' const authValue = btoa(`${username}/token:${token}`) -// Below are sandbox auth variables, just to facilitate debugging. Note that the format is different. -// const username = 'xxxxxxxxxxxxxxxxx' -// const token = 'xxxxxxxxxxxxxxxxx' +// Below are sandbox auth variables, just to facilitate debugging. Note that the authvalue format is different. +// const username = 'xxxxxxxxxx' +// const token = 'xxxxxxxxxx' // const authValue = btoa(`${username}:${token}`) const requestHeaders = { diff --git a/node/middlewares/errorLogs.ts b/node/middlewares/errorLogs.ts new file mode 100644 index 0000000..d5b5dd3 --- /dev/null +++ b/node/middlewares/errorLogs.ts @@ -0,0 +1,18 @@ +// Returning and logging errors + +export async function returnError( + zendeskTicket: string, + status: number, + errorMessage: string, + ctx: Context +) { + + const slack = ctx.clients.slack + slack.sendLog(`ticket: ${zendeskTicket}\n${errorMessage}`, 'error') + + ctx.status = status + ctx.response.body = { + ticketId: zendeskTicket, + message: errorMessage + } +} diff --git a/node/middlewares/processTicket.ts b/node/middlewares/processTicket.ts index 6dda19a..74c097f 100644 --- a/node/middlewares/processTicket.ts +++ b/node/middlewares/processTicket.ts @@ -3,6 +3,7 @@ import { or } from 'ramda' import type { MessageData } from '../clients/redshift' +import { returnError } from './errorLogs' // Defining URLs that will be used to parse and process the comment data // Substrings to look for and to exclude @@ -24,10 +25,9 @@ export async function processTicket( ctx: Context, next: () => Promise> ) { - console.info('Running processTicket') + console.log('Running processTicket') const requestBody = ctx.state.body - console.info(requestBody) const zendeskTicket = requestBody.ticketId const zendesk = ctx.clients.zendesk @@ -35,11 +35,25 @@ export async function processTicket( let allCommentsWithUrls = [] let page = 1 + interface Comment { + html_body: string, + id: number, + author_id: number, + created_at: string + } + let ticketComments: Array + let nextPage: number while (1==1) { - const zendeskData = await zendesk.getComments(zendeskTicket, page) - const ticketComments = zendeskData.comments - const nextPage = zendeskData.next_page + + try { + const zendeskData = await zendesk.getComments(zendeskTicket, page) + ticketComments = zendeskData.comments + nextPage = zendeskData.next_page + } catch (error) { + returnError(zendeskTicket, 500, `Error trying to get ticket data from Zendesk >>> ${error}`, ctx) + return + } // Iterate over comments for (const comment of ticketComments) { @@ -99,7 +113,13 @@ export async function processTicket( allCommentsWithUrls.push(messageData) - await redshift.saveMessage(messageData) + try { + console.log('try redshift') + await redshift.saveMessage(messageData) + console.log(messageData.numberOfArticleUrls) + } catch (error) { + returnError(zendeskTicket, 500, `Error trying to save comment data to RedShift >>> ${error}`, ctx) + } } } @@ -115,7 +135,9 @@ export async function processTicket( message: 'ticket processed', docsUrlsData: allCommentsWithUrls, } - console.info(allCommentsWithUrls) + + console.log(`comments processed: ${allCommentsWithUrls.length}`) + console.log('processTicket done') await next() } diff --git a/node/middlewares/verifyZendeskSignature.ts b/node/middlewares/verifyZendeskSignature.ts index 47589d9..4f8db2a 100644 --- a/node/middlewares/verifyZendeskSignature.ts +++ b/node/middlewares/verifyZendeskSignature.ts @@ -2,9 +2,10 @@ // https://developer.zendesk.com/documentation/webhooks/verifying/#verifying-the-signature import bodyParser from 'co-body' import * as crypto from 'crypto' +import { returnError } from './errorLogs' -const ZENDESK_SECRET_KEY_PRODUCTION = "xxxxxxxxxxxxxxxxx" -const ZENDESK_SECRET_KEY_SANDBOX = "xxxxxxxxxxxxxxxxxx" +const ZENDESK_SECRET_KEY_PRODUCTION = "xxxxxxxxxx" +const ZENDESK_SECRET_KEY_SANDBOX = "xxxxxxxxxx" const SIGNING_SECRET_ALGORITHM = "sha256" export async function verifyZendeskSignature ( @@ -27,43 +28,41 @@ export async function verifyZendeskSignature ( return (comparison === 0) } + const requestBody = await bodyParser(ctx.req, { returnRawBody: true }) + ctx.state.body = requestBody.parsed + console.log(requestBody.parsed.ticketId) + + if (requestBody.parsed.ticketId == undefined) { + returnError('undefined', 400, 'ticketId not found.', ctx) + } + + const zendeskTicket = requestBody.parsed.ticketId + if (ctx.request.headers['x-zendesk-webhook-signature'] !== undefined) { - const requestBody = await bodyParser(ctx.req, { returnRawBody: true }) - ctx.state.body = requestBody.parsed - console.info(ctx.state.body) + const requestSignature: string = ctx.request.headers["x-zendesk-webhook-signature"] as string const requestSignatureTimestamp: string = ctx.request.headers["x-zendesk-webhook-signature-timestamp"] as string if (isValidSignature(requestSignature, requestBody.raw, requestSignatureTimestamp, ZENDESK_SECRET_KEY_PRODUCTION)) { - console.info('Zendesk production signature verified successfully.') + // console.info('Zendesk production signature verified successfully.') await next() } else { if (isValidSignature(requestSignature, requestBody.raw, requestSignatureTimestamp, ZENDESK_SECRET_KEY_SANDBOX)) { - console.info('Zendesk sandbox signature verified successfully.') + // console.info('Zendesk sandbox signature verified successfully.') await next() } else { - - ctx.status = 400 - ctx.response.body = { - message: 'Zendesk signature not valid.' - } - console.info('Zendesk signature not valid.') + returnError(zendeskTicket, 400, 'Zendesk signature not valid.', ctx) return - } } } else { - ctx.status = 400 - ctx.response.body = { - message: 'Zendesk signature not found.' - } - console.info('Zendesk signature not found') + returnError(zendeskTicket, 400, `Zendesk signature not found.`, ctx) return } }