import Fastify from 'fastify' import prismaPlugin from './plugins/prisma' import vodsRoutes from './plugins/vods' import uploadsRoutes from './plugins/uploads' import vtubersRoutes from './plugins/vtubers' import usersRoutes from './plugins/users' import indexRoutes from './plugins/index' import streamsRoutes from './plugins/streams' import adminRoutes from './plugins/admin' import hls from './plugins/hls.ts' import fastifyStatic from '@fastify/static' import fastifySecureSession from '@fastify/secure-session' import path, { basename } from 'node:path' import fastifyFormbody from '@fastify/formbody' import fastifyView from "@fastify/view" import { env } from './config/env' import { constants } from './config/constants' import authRoutes from './plugins/auth' import Handlebars from 'handlebars' import graphileWorker from './plugins/graphileWorker' import fastifySwagger from "@fastify/swagger" import fastifySwaggerUi from "@fastify/swagger-ui" import { join } from 'node:path' import { format } from 'date-fns' import * as jdenticon from 'jdenticon' import { Role } from '../generated/prisma' import fastifyFlash from '@fastify/flash' import { isModerator, hasRole } from './utils/privs' import { signUrl } from './utils/cdn' import { extractBasePath } from './utils/filesystem' import { truncate } from './utils/formatters.ts' import { icons } from './utils/icons.ts' import logger from './utils/logger.ts' import fastifyCaching from '@fastify/caching' export function buildApp() { const app = Fastify() Handlebars.registerHelper('formatDate', function (dateString) { if (!dateString) return '' return format(new Date(dateString), 'yyyy-MM-dd') }) Handlebars.registerHelper('identicon', function (str, size = 48) { return jdenticon.toSvg(str, size) }) Handlebars.registerHelper('safeJson', function (context) { return new Handlebars.SafeString(JSON.stringify(context)); }); Handlebars.registerHelper('json', function (context) { return JSON.stringify(context) }) Handlebars.registerHelper('patron', function (user) { if (!user.roles) { throw new Error( 'patron hbs helper was called without roles. This usually means you forgot to include roles relationship in the query.' ); } return user.roles.some((r: Role) => r.name.startsWith('supporter')); }); Handlebars.registerHelper('notEqual', function (a, b) { return a !== b; }); Handlebars.registerHelper('isEqual', function (a, b) { logger.trace(`isEqual a=${a} b=${b}`) return a == b }); Handlebars.registerHelper('isModerator', function (user) { return isModerator(user) }) Handlebars.registerHelper('hasRole', hasRole) Handlebars.registerHelper('breaklines', function (text) { text = Handlebars.Utils.escapeExpression(text); text = text.replace(/(\r\n|\n|\r)/gm, '
'); return new Handlebars.SafeString(text); }); Handlebars.registerHelper('getCdnUrl', function (s3Key) { // Before you remove this log, find a way to memoize this function! logger.info(`getCdnUrl called with CDN_ORIGIN=${env.CDN_ORIGIN} and CDN_TOKEN_SECRET=${env.CDN_TOKEN_SECRET}`) return signUrl(`${env.CDN_ORIGIN}/${s3Key}`, { securityKey: env.CDN_TOKEN_SECRET, expirationTime: constants.timeUnits.sevenDaysInSeconds, }) }) /** * @see https://github.com/video-dev/hls.js/issues/2152 */ Handlebars.registerHelper('signedHlsUrl', function (s3Key) { if (!s3Key) throw new Error(`signedHlsUrl called with falsy s3Key=${s3Key}`); const pathAllowed = extractBasePath(s3Key) const url = signUrl(`${env.CDN_ORIGIN}/${s3Key}`, { securityKey: env.CDN_TOKEN_SECRET, pathAllowed, isDirectory: true, expirationTime: constants.timeUnits.sevenDaysInSeconds, }) logger.debug(`pathAllowed=${pathAllowed} url=${url}`) return url }) Handlebars.registerHelper('basename', function (url: string) { return basename(url) }) Handlebars.registerHelper('trunc', function (str, length = 6) { return truncate(str, length) }); Handlebars.registerHelper('icon', function (name: string, size = 20) { const svg = icons[name]; if (!svg) { return new Handlebars.SafeString(``); } // Inject width/height if not already set const sizedSvg = svg .replace(/]*)>/, ``); return new Handlebars.SafeString(sizedSvg); }); const __dirname = import.meta.dirname; const swaggerOptions = { swagger: { info: { title: constants.site.title, description: constants.site.description, version: constants.site.version, }, host: env.ORIGIN, schemes: ["http", "https"], consumes: ["application/json"], produces: ["application/json"], }, }; const swaggerUiOptions = { routePrefix: "/api/docs", exposeRoute: true, }; app.register(fastifySwagger, swaggerOptions); app.register(fastifySwaggerUi, swaggerUiOptions); app.register(fastifyStatic, { root: path.join(__dirname, 'assets'), prefix: '/', // optional: default '/' constraints: {} // optional: default {} }) app.register(fastifyFormbody) // app.register(fastifyMultipart, { // limits: { // fileSize: 30 * 1024 * 1024 // 30MB // } // }) app.register(fastifySecureSession, { // the name of the attribute decorated on the request-object, defaults to 'session' sessionName: 'session', cookieName: 'fp-session', // adapt this to point to the directory where secret-key is located key: Buffer.from(env.COOKIE_SECRET, 'hex'), // the amount of time the session is considered valid; this is different from the cookie options // and based on value within the session. expiry: 24 * 60 * 60, // Default 1 day cookie: { path: '/' // options for setCookie, see https://github.com/fastify/fastify-cookie } }) app.register(fastifyFlash) app.register(fastifyView, { engine: { handlebars: Handlebars, }, templates: join(__dirname, '..', 'src', 'views'), viewExt: 'hbs', options: { partials: { navbar: 'partials/navbar.hbs', footer: 'partials/footer.hbs', commentForm: 'partials/commentForm.hbs' } } }) // @todo we are going to need to use redis or similar https://github.com/fastify/fastify-caching // app.register(fastifyCaching, { // expiresIn: 300, // privacy: 'private', // serverExpiresIn: 300, // }) app.register(graphileWorker) app.register(prismaPlugin) app.register(hls) app.register(vodsRoutes) app.register(streamsRoutes) app.register(vtubersRoutes) app.register(uploadsRoutes) app.register(usersRoutes) app.register(indexRoutes) app.register(adminRoutes) app.register(authRoutes) return app }