Skip to content

Commit

Permalink
feat(side-convo): Generate email side converstation PDFs
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Balmos <[email protected]>
  • Loading branch information
abalmos committed Jan 12, 2024
1 parent 87966f0 commit 03ddb74
Show file tree
Hide file tree
Showing 10 changed files with 429 additions and 54 deletions.
10 changes: 9 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
searchTickets,
Ticket,
} from './zd/zendesk.js';
import { generatePdf } from './zd/pdf.js';
import { generatePdf, generateSideConverstationPdf } from './zd/pdf.js';
import { writeFileSync } from 'node:fs';
// Stuff from config
const { token, domain } = config.get('oada');
Expand Down Expand Up @@ -157,6 +157,14 @@ export async function handleTicket(

const pdf = await generatePdf(archive);

let i = 1;
for (let sideConv of archive.sideConversations) {
const pdf = await generateSideConverstationPdf(archive, sideConv);
info(`Writing output side PDF ${i}`);
writeFileSync(`./side-${i}.pdf`, pdf);
i++;
}

// FIXME: Remove
info('Writing output PDF');
writeFileSync('./output.pdf', pdf);
Expand Down
69 changes: 65 additions & 4 deletions src/zd/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import { createServer } from 'http';
import { join, extname } from 'path';
import { launch } from 'puppeteer';
import debug from 'debug';
import { TicketArchive } from './zendesk.js';
import {
SideConversation,
SideConversationWithEvents,
TicketArchive,
} from './zendesk.js';

const info = debug('zendesk-sync:pdf:info');
const warn = debug('zendesk-sync:pdf:warn');
Expand Down Expand Up @@ -51,7 +55,7 @@ const prepareFile = async (url: string) => {
() => false,
);
const found = !pathTraversal && exists;
const streamPath = found ? filePath : STATIC_PATH + '/404.html';
const streamPath = found ? filePath : STATIC_PATH + '/index.html';
const ext = extname(streamPath).substring(1).toLowerCase();
const stream = createReadStream(streamPath);
return { found, ext, stream };
Expand All @@ -62,7 +66,7 @@ let address = new Promise<string>((resolve, reject) => {
const server = createServer(async (req, res) => {
if (req.url) {
const file = await prepareFile(req.url);
const statusCode = file.found ? 200 : 404;
const statusCode = 200; // We always fallback to index.html
const mimeType = MIME_TYPES[file.ext] || MIME_TYPES.default;
res.writeHead(statusCode, { 'Content-Type': mimeType });
file.stream.pipe(res);
Expand Down Expand Up @@ -119,7 +123,64 @@ export async function generatePdf(archive: TicketArchive): Promise<Buffer> {
}
});

await page.goto(`http://${await address}`, {
await page.goto(`http://${await address}/ticket`, {
waitUntil: ['load', 'networkidle0'],
});

const pdf = await page.pdf({
format: 'letter',
margin: { top: '20px', left: '20px', right: '20px', bottom: '20px' },
printBackground: true,
});

await browser.close();

return pdf;
}

// TODO: Should we pass in sideConv, or just an index, or should this function just return an array of PDFs, or...
export async function generateSideConverstationPdf(
archive: TicketArchive,
sideConv: SideConversationWithEvents,
): Promise<Buffer> {
const browser = await launch({
headless: 'new',
args: [
'--disable-extensions',
'--no-sandbox',
'--disable-gpu',
'--disable-web-security',
],
dumpio: true,
});

let page = (await browser.newPage())
.on('load', trace)
.on('error', error)
.on('console', (message) =>
info(`${message.type().substring(0, 3).toUpperCase()} ${message.text()}`),
)
.on('pageerror', error)
.on('requestfailed', (request) =>
warn(`${request.failure()?.errorText} ${request.url()}`),
);

await page.setRequestInterception(true);

page.on('request', (request) => {
if (request.url() === 'http://127.0.0.1/_data') {
console.log(sideConv);
request.respond({
contentType: 'application/json',
headers: { 'Access-Control-Allow-Origin': '' },
body: JSON.stringify({ ticket: archive, ...sideConv }),
});
} else {
request.continue();
}
});

await page.goto(`http://${await address}/side`, {
waitUntil: ['load', 'networkidle0'],
});

Expand Down
87 changes: 78 additions & 9 deletions src/zd/zendesk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ export async function getTicketArchive(
/////////////////
let ids = comments
.map((c) => [c.author_id, ...(c.via.source.to.email_ccs || [])])
.concat(sideConversations.map((s) => s.participants.map((p) => p.user_id)))
.concat(
sideConversations.map((s) =>
s.side_conversation.participants.map((p) => p.user_id),
),
)
.flat()
.filter((user, index, array) => array.indexOf(user) === index);

Expand Down Expand Up @@ -201,8 +205,8 @@ export async function getCommentsFromTicket(

export async function getSideConversationsFromTicket(
ticket: Ticket,
): Promise<Array<SideConversation>> {
let sideConversations: Array<SideConversation> = [];
): Promise<Array<SideConversationWithEvents>> {
let sideConvos: Array<SideConversation> = [];

let r = await throttle(
async () =>
Expand All @@ -213,10 +217,10 @@ export async function getSideConversationsFromTicket(
username,
password,
},
}) as Promise<{ data: SideConverstationResponse }>,
}) as Promise<{ data: SideConversationResponse }>,
)();

sideConversations.push(...r.data.side_conversations);
sideConvos.push(...r.data.side_conversations);

while (r.data.next_page) {
r = await throttle(async () =>
Expand All @@ -230,10 +234,27 @@ export async function getSideConversationsFromTicket(
}),
)();

sideConversations.push(...r.data.side_conversations);
sideConvos.push(...r.data.side_conversations);
}

return sideConversations;
// Replace all the side conversation objects with on where `events` is side loaded expanded
return Promise.all(
sideConvos.map(async (sideConv) => {
let r = await throttle(
async () =>
axios({
method: 'get',
url: `${ZD_DOMAIN}/api/v2/tickets/${ticket.id}/side_conversations/${sideConv.id}?include=events`,
auth: {
username,
password,
},
}) as Promise<{ data: SideConversationWithEvents }>,
)();

return r.data;
}),
);
}

export async function getOrgs(
Expand Down Expand Up @@ -419,7 +440,7 @@ export interface TicketArchive {
orgs: Record<number, Org>;
groups: Record<number, Group>;
ticketFields: Record<number, TicketField>;
sideConversations: Array<SideConversation>;
sideConversations: Array<SideConversationWithEvents>;
}

export interface Org {
Expand Down Expand Up @@ -604,13 +625,61 @@ export interface SideConversation {
};
}

interface SideConverstationResponse {
interface SideConversationResponse {
side_conversations: Array<SideConversation>;
next_page: string | null;
previous_page: string | null;
count: number;
}

export interface SideConversationWithEvents {
side_conversation: SideConversation;
events?: Array<{
id: string;
side_conversation_id: string;
actor: {
user_id: number;
name: string;
email: string;
};
type: string;
via: string;
created_at: string;
message: {
subject: string;
preview_text: string;
from: {
user_id: number;
name: string;
email: string;
};
to: Array<{
user_id: number;
name: string;
email: string;
}>;
body: string;
html_body: string;
external_ids: {
ticketAuditId: string;
outboundEmail?: string;
};
attachments: Array<{
id: string;
file_name: string;
size: number;
content_url: string;
content_type: string;
width: number;
height: number;
inline: boolean;
}>;
updates: {};
ticket_id: number;
};
}>;
}

export interface TicketField {
id: number;
position: number;
Expand Down
27 changes: 14 additions & 13 deletions template/src/app.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en" data-theme="corporate">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />

<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link
href="https://fonts.googleapis.com/css2?family=DM+Sans&display=swap"
rel="stylesheet"
/>

<link href="https://fonts.googleapis.com/css2?family=DM+Sans&display=swap" rel="stylesheet" />

<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>

<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>

<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
2 changes: 1 addition & 1 deletion template/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script>
import "../app.css";
import '../app.css';
</script>

<div class="m-4">
Expand Down
2 changes: 1 addition & 1 deletion template/src/routes/+layout.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const prerender = true;
export const ssr = false;
export const ssr = false;
Loading

0 comments on commit 03ddb74

Please sign in to comment.