{"version":3,"file":"start.mjs","names":["Config","Config"],"sources":["../src/lib/server.ts","../src/bin/start.ts"],"sourcesContent":["import { generateName, generateNameWithNumber } from '@criblinc/docker-names';\nimport { readFile } from 'fs/promises';\nimport { ICalAlarmType, ICalCalendar, ICalEventStatus } from 'ical-generator';\nimport moment from 'moment-timezone';\nimport { randomUUID } from 'node:crypto';\n\nimport Config from './config.js';\nimport { type User } from './db.js';\nimport prisma from './db.js';\nimport { DEFAULT_EMOJI } from './emoji.js';\n\nexport default class ServerLib {\n    static async createUser() {\n        let user: undefined | User;\n\n        // eslint-disable-next-line no-constant-condition\n        for (let c = 0; true; c++) {\n            const prefix = this.generatePrefix(c);\n            try {\n                user = await prisma.user.create({\n                    data: {\n                        prefix,\n                    },\n                });\n                if (user) {\n                    break;\n                }\n            } catch (error) {\n                if (\n                    error &&\n                    typeof error === 'object' &&\n                    'code' in error &&\n                    error.code === 'P2002'\n                ) {\n                    continue;\n                }\n\n                throw error;\n            }\n        }\n        if (!user) {\n            throw new Error('User not created');\n        }\n\n        return user;\n    }\n\n    static async generateCalendar(userId: string) {\n        const user = await prisma.user.findUniqueOrThrow({\n            select: {\n                event: {\n                    select: {\n                        amount: true,\n                        canceledAt: true,\n                        createdAt: true,\n                        from: true,\n                        id: true,\n                        location: {\n                            select: {\n                                address: true,\n                                emoji: true,\n                                latitude: true,\n                                longitude: true,\n                                name: true,\n                            },\n                        },\n                        orderId: true,\n                        price: true,\n                        to: true,\n                    },\n                },\n                id: true,\n            },\n            where: {\n                id: userId,\n            },\n        });\n\n        const cal = new ICalCalendar({\n            events: user.event.map((event) => {\n                const price = new Intl.NumberFormat('de-DE', {\n                    currency: 'EUR',\n                    style: 'currency',\n                }).format(event.price / 100);\n\n                let status: ICalEventStatus = ICalEventStatus.CONFIRMED;\n                if (event.canceledAt) {\n                    status = ICalEventStatus.CANCELLED;\n                }\n\n                return {\n                    alarms: [{ trigger: 600, type: ICalAlarmType.display }],\n                    created: event.createdAt,\n                    description: `${event.amount}x\\n${price}`,\n                    end: event.to,\n                    id: event.id,\n                    location: {\n                        address: event.location.address,\n                        geo:\n                            event.location.latitude && event.location.longitude\n                                ? {\n                                      lat: event.location.latitude,\n                                      lon: event.location.longitude,\n                                  }\n                                : undefined,\n                        title: event.location.name,\n                    },\n                    start: event.from,\n                    status,\n                    summary: `${event.location.emoji || DEFAULT_EMOJI} ${event.location.name}`,\n                    timestamp: event.createdAt,\n                    url: `https://share.toogoodtogo.com/receipts/details/${event.orderId}`,\n                };\n            }),\n            name: 'TGTG',\n            ttl: 60 * 60,\n        });\n\n        this.updateUserLastSeen(userId);\n        return cal.toString();\n    }\n\n    static generatePrefix(c = 0) {\n        if (c > 100) {\n            return randomUUID();\n        }\n\n        if (c < 10) {\n            return generateName();\n        }\n\n        return generateNameWithNumber();\n    }\n\n    static async generateUserPage(userId: string) {\n        const [user, html] = await Promise.all([\n            this.getUser(userId),\n            readFile(Config.src('templates/user.html'), 'utf-8'),\n        ]);\n\n        return html\n            .replace(\n                /\\${CALENDAR_URL}/g,\n                `${Config.baseUrl}/${user.id}/calendar.ical`,\n            )\n            .replace(/\\${EMAIL_ADDRESS}/g, `${user.prefix}${Config.baseMail}`);\n    }\n\n    static async getUser(userId: string) {\n        const user = await prisma.user.findUniqueOrThrow({\n            where: {\n                id: userId,\n            },\n        });\n\n        this.updateUserLastSeen(user.id);\n        return user;\n    }\n\n    static async isHealthy() {\n        const c = await prisma.mail.count({\n            where: {\n                createdAt: {\n                    lt: moment().subtract(30, 'minutes').toDate(),\n                },\n            },\n        });\n        if (c > 0) {\n            throw new Error(`There are ${c} unhandled mails in the queue!`);\n        }\n    }\n\n    static updateUserLastSeen(userId: string) {\n        prisma.user\n            .update({\n                data: { lastSeenAt: new Date() },\n                where: { id: userId },\n            })\n            .catch((error) => {\n                console.log(error);\n            });\n    }\n}\n","#!/usr/bin/env node\n'use strict';\n\nimport cookieParser from 'cookie-parser';\nimport express, { type Express, type Response } from 'express';\nimport { Server } from 'http';\n\nimport Config from '../lib/config.js';\nimport prisma from '../lib/db.js';\nimport Parser from '../lib/parser.js';\nimport ServerLib from '../lib/server.js';\n\nclass AppServer {\n    private app: Express;\n\n    private server: Server;\n    constructor() {\n        this.app = express();\n        this.app.use(cookieParser());\n\n        this.setupRoutes();\n        this.server = this.app.listen(process.env.PORT || 8080);\n        console.log(\n            `tgtg-ical v${Config.version} listening on port ${process.env.PORT || 8080}`,\n        );\n\n        process.on('SIGINT', () => this.stop());\n        process.on('SIGTERM', () => this.stop());\n\n        Parser.runCleanup()\n            .then(() => console.log('Initial cleanup succeeded.'))\n            .catch((error) => {\n                console.log('Initial cleanup failed:');\n                console.error(error);\n                process.exit(1);\n            });\n    }\n\n    static run() {\n        new AppServer();\n    }\n\n    handleError(error: unknown, res: Response) {\n        if (\n            error &&\n            typeof error === 'object' &&\n            'code' in error &&\n            error.code === 'P2025'\n        ) {\n            res.sendStatus(404);\n            return;\n        }\n\n        console.log(error);\n        res.sendStatus(500);\n    }\n\n    setupRoutes() {\n        this.app.get('/ping', (req, res) => {\n            res.send('pong');\n        });\n\n        this.app.get('/', (req, res) => {\n            if ('userId' in req.cookies && req.cookies.userId) {\n                res.redirect('/' + req.cookies.userId);\n                return;\n            }\n\n            ServerLib.createUser()\n                .then((user) => {\n                    res.cookie('userId', user.id);\n                    res.redirect('/' + user.id);\n                })\n                .catch((error) => this.handleError(error, res));\n        });\n\n        this.app.get('/_health', (req, res) => {\n            ServerLib.isHealthy()\n                .then(() => res.sendStatus(204))\n                .catch((error) => this.handleError(error, res));\n        });\n\n        this.app.use(express.static(Config.src('./assets')));\n\n        this.app.get('/:userId', (req, res) => {\n            res.format({\n                'application/json': () => {\n                    ServerLib.getUser(req.params.userId)\n                        .then((json) => res.send(json))\n                        .catch((error) => this.handleError(error, res));\n                },\n                'text/html': () => {\n                    ServerLib.generateUserPage(req.params.userId)\n                        .then((html) => {\n                            res.cookie('userId', req.params.userId);\n                            res.send(html);\n                        })\n                        .catch((error) => this.handleError(error, res));\n                },\n            });\n        });\n\n        this.app.get('/:userId/calendar.ical', (req, res) => {\n            ServerLib.generateCalendar(req.params.userId)\n                .then((ical) => {\n                    res.set('Content-Type', 'text/calendar');\n                    res.send(ical);\n                })\n                .catch((error) => this.handleError(error, res));\n        });\n    }\n\n    async stop() {\n        await new Promise((cb) => this.server.close(cb));\n        await prisma.$disconnect();\n\n        process.exit();\n    }\n}\n\nAppServer.run();\n"],"mappings":"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"}