Skip to content

Commit 8f77182

Browse files
committed
Add PDF API for schedule
1 parent f3d04a5 commit 8f77182

27 files changed

+2146
-208
lines changed

functions/.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
BUCKET=aaaaa.appspot.com
2-
SERVICE_API_KEY=xxxx
2+
SERVICE_API_KEY=xxxx
3+
G_FIREBASE_PROJECT_ID=xxx

functions/package-lock.json

Lines changed: 1449 additions & 161 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

functions/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"firebase-admin": "^11.11.0",
3030
"firebase-functions": "^4.5.0",
3131
"luxon": "^3.4.4",
32+
"puppeteer": "^24.10.1",
3233
"ts-custom-error": "^3.3.1",
3334
"uuid": "^9.0.1"
3435
},

functions/src/api/dao/eventDao.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ export class EventDao {
1313
if (!data) {
1414
throw new Error('Event not found')
1515
}
16-
return data as Event
16+
const baseData = data
17+
if (data.dates) {
18+
baseData.dates = {
19+
start: data.dates.start.toDate(),
20+
end: data.dates.end.toDate(),
21+
}
22+
}
23+
return baseData as Event
1724
}
1825

1926
public static async createCategory(
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { FastifyInstance } from 'fastify'
2+
import { exportSchedulePdfRoute } from './exportSchedulePdf'
3+
import { getEventRoute } from './getEvent'
4+
5+
export const eventRoutes = (fastify: FastifyInstance, options: any, done: () => any) => {
6+
getEventRoute(fastify, options, () => {})
7+
exportSchedulePdfRoute(fastify, options, () => {})
8+
done()
9+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { Type } from '@sinclair/typebox'
2+
import { FastifyInstance } from 'fastify'
3+
import { EventDao } from '../../dao/eventDao'
4+
import { isDev } from '../../setupFastify'
5+
import { getFirebaseProjectId } from '../../../utils/getFirebaseProjectId'
6+
import { getServiceAPIKey } from '../../../serviceApi/serviceApiKeyPreHandler'
7+
import { getIndividualDays } from '../../../../../src/utils/dates/diffDays'
8+
import { uploadBufferToStorage } from '../file/utils/uploadBufferToStorage'
9+
10+
const ExportPdfReply = Type.Object({
11+
pdf: Type.String(),
12+
})
13+
14+
export const exportSchedulePdfRoute = (fastify: FastifyInstance, options: any, done: () => any) => {
15+
fastify.post<{ Params: { eventId: string }; Body: { apiKey: string } }>(
16+
'/v1/:eventId/event/export-pdf',
17+
{
18+
schema: {
19+
tags: ['event'],
20+
summary: 'Export event schedule to PDF',
21+
description:
22+
'Export event schedule to PDF. The PDF will be generated is the event public website is enabled. Do not forget to set the timezone in the event settings. The PDF will be stored in the event storage, and the public file url will be returned. The file will stay the same and will not be deleted if you disable the public website.',
23+
params: Type.Object({
24+
eventId: Type.String(),
25+
}),
26+
response: {
27+
200: ExportPdfReply,
28+
400: Type.String(),
29+
401: Type.String(),
30+
},
31+
security: [
32+
{
33+
apiKey: [],
34+
},
35+
],
36+
},
37+
preHandler: fastify.auth([fastify.verifyApiKey]),
38+
},
39+
async (request, reply) => {
40+
const { eventId } = request.params
41+
42+
const event = await EventDao.getEvent(fastify.firebase, eventId)
43+
44+
if (!event.publicEnabled) {
45+
reply.status(401).send(
46+
JSON.stringify({
47+
error: 'Event is not public',
48+
})
49+
)
50+
return
51+
}
52+
53+
if (!event.files?.public) {
54+
reply.status(401).send(
55+
JSON.stringify({
56+
error: 'Missing public file, did you forgot to "Update website" once?',
57+
})
58+
)
59+
return
60+
}
61+
62+
const individualEventDaysAsyearMonthDay = getIndividualDays(event.dates.start, event.dates.end)
63+
const scheduleUrls = individualEventDaysAsyearMonthDay.map((day) => {
64+
return `https://openplanner.fr/public/event/${eventId}/schedule/${day.start.toFormat(
65+
'yyyy-MM-dd'
66+
)}?hideHeader=true`
67+
})
68+
69+
const timezone = event.timezone
70+
71+
try {
72+
const firebaseProjectId = getFirebaseProjectId()
73+
const serviceApiKey = getServiceAPIKey()
74+
const pdfServiceUrl = isDev()
75+
? `http://localhost:5001/${firebaseProjectId}/europe-west1/serviceApi`
76+
: `https://service.openplanner.fr/`
77+
const response = await fetch(`${pdfServiceUrl}/v1/convert?apiKey=${serviceApiKey}`, {
78+
method: 'POST',
79+
headers: {
80+
'Content-Type': 'application/json',
81+
},
82+
body: JSON.stringify({
83+
urls: scheduleUrls,
84+
settings: {
85+
viewport: {
86+
width: 4200,
87+
height: 8000,
88+
deviceScaleFactor: 4,
89+
},
90+
pdf: {
91+
format: 'A3',
92+
landscape: false,
93+
scale: 0.65,
94+
margin: {
95+
top: '0px',
96+
right: '0px',
97+
bottom: '0px',
98+
left: '0px',
99+
},
100+
},
101+
timezone: timezone,
102+
},
103+
}),
104+
})
105+
106+
if (!response.ok) {
107+
console.error(await response.text())
108+
throw new Error(`PDF service responded with status: ${response.status}`)
109+
}
110+
111+
const pdfArrayBuffer = await response.arrayBuffer()
112+
const pdfBuffer = Buffer.from(pdfArrayBuffer)
113+
114+
const [success, publicFileUrlOrError] = await uploadBufferToStorage(
115+
fastify.firebase,
116+
pdfBuffer,
117+
eventId,
118+
`schedule`,
119+
false
120+
)
121+
122+
if (!success) {
123+
return reply.status(400).send(publicFileUrlOrError)
124+
}
125+
126+
reply.status(200).send({
127+
pdf: publicFileUrlOrError,
128+
})
129+
} catch (err) {
130+
const error = err as Error
131+
reply.status(400).send(
132+
JSON.stringify({
133+
error: 'Failed to generate PDF',
134+
details: error.message,
135+
})
136+
)
137+
}
138+
}
139+
)
140+
done()
141+
}

functions/src/api/routes/event/event.ts renamed to functions/src/api/routes/event/getEvent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const GetEventReply = Type.Union([
1212
])
1313
type GetEventReplyType = Static<typeof GetEventReply>
1414

15-
export const eventRoutes = (fastify: FastifyInstance, options: any, done: () => any) => {
15+
export const getEventRoute = (fastify: FastifyInstance, options: any, done: () => any) => {
1616
fastify.get<{ Reply: GetEventReplyType }>(
1717
'/v1/:eventId/event',
1818
{

functions/src/api/routes/file/utils/uploadBufferToStorage.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export const uploadBufferToStorage = async (
77
firebase: firebase.app.App,
88
buffer: Buffer,
99
eventId: string,
10-
fileName: string
10+
fileName: string,
11+
addUuid: boolean = true
1112
): Promise<[boolean, string]> => {
1213
const storageBucket = getStorageBucketName()
1314

@@ -21,7 +22,7 @@ export const uploadBufferToStorage = async (
2122

2223
const bucket = firebase.storage().bucket(storageBucket)
2324
const fileName50char = fileName.slice(0, 50)
24-
const path = `events/${eventId}/${uuidv4()}_${fileName50char}.${extension}`
25+
const path = `events/${eventId}/${addUuid ? uuidv4() + '_' : ''}${fileName50char}.${extension}`
2526
const bucketFile = bucket.file(path)
2627

2728
try {

functions/src/api/setupFastify.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import { filesRoutes } from './routes/file/files'
1515
import { sessionsSpeakers } from './routes/sessionsSpeakers/sessionsSpeakers'
1616
import { helloRoute } from './routes/hello/hello'
1717
import { fastifyErrorHandler } from './other/fastifyErrorHandler'
18-
import { eventRoutes } from './routes/event/event'
18+
import { eventRoutes } from './routes/event/eventRoutes'
1919
import { bupherRoutes } from './routes/bupher/bupher'
2020
import { deployFilesRoutes } from './routes/deploy/getDeployFiles'
2121
import { deployRoutes } from './routes/deploy/deploy'
22-
import { noCacheHook } from '../fastifyUtils/noCacheHook'
22+
import { noCacheHook } from '../utils/noCacheHook'
2323

2424
type Firebase = firebaseApp.App
2525
declare module 'fastify' {
@@ -29,13 +29,16 @@ declare module 'fastify' {
2929
}
3030
}
3131

32+
export const isDev = () => {
33+
return !!(process.env.FUNCTIONS_EMULATOR && process.env.FUNCTIONS_EMULATOR === 'true')
34+
}
35+
3236
export const setupFastify = () => {
33-
const isDev = !!(process.env.FUNCTIONS_EMULATOR && process.env.FUNCTIONS_EMULATOR === 'true')
3437
const isNodeEnvDev = process.env.NODE_ENV === 'development'
3538
const isNodeEnvTest = process.env.NODE_ENV === 'test'
3639

3740
const fastify = Fastify({
38-
logger: isDev,
41+
logger: isDev() || isNodeEnvDev || isNodeEnvTest,
3942
}).withTypeProvider<TypeBoxTypeProvider>()
4043

4144
if (!isNodeEnvDev && !isNodeEnvTest) {

functions/src/api/swagger.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import FastifySwaggerUi from '@fastify/swagger-ui'
22
import { FastifyInstance } from 'fastify'
33
import FastifySwagger from '@fastify/swagger'
4+
import { isDev } from './setupFastify'
5+
import { getFirebaseProjectId } from '../utils/getFirebaseProjectId'
46

57
export const registerSwagger = (fastify: FastifyInstance) => {
68
fastify.register(FastifySwagger, {
@@ -9,8 +11,8 @@ export const registerSwagger = (fastify: FastifyInstance) => {
911
title: 'OpenPlanner API Documentation',
1012
version: '1.0.0',
1113
},
12-
host: 'api.openplanner.fr/',
13-
schemes: ['https'],
14+
host: isDev() ? `localhost:5001/${getFirebaseProjectId()}/europe-west1/api/` : 'api.openplanner.fr/',
15+
schemes: isDev() ? ['http'] : ['https'],
1416
consumes: ['application/json'],
1517
produces: ['application/json'],
1618
securityDefinitions: {

0 commit comments

Comments
 (0)