import * as Sentry from '@sentry/node'; import { Router } from 'express'; import type { Response } from 'express'; import { getH3Event } from 'h3-express'; import { createEvents } from 'ics'; import SQL from 'sql-template-strings'; import type { LocaleDescription } from '../../locale/locales.ts'; import { normalise } from './user.ts'; import { Calendar, Day } from '#shared/calendar/helpers.ts'; import type { Event } from '#shared/calendar/helpers.ts'; import { handleErrorAsync, clearLinkedText } from '#shared/helpers.ts'; import type { Translator } from '#shared/translator.ts'; import type { Config } from '~~/locale/config.ts'; import { getLocale, loadCalendar, loadConfig, loadTranslator } from '~~/server/data.ts'; // TODO caching? // import { caches } from "../../src/cache"; const renderEvents = ( yearEvents: Record, translator: Translator, res: Response, onlyFirstDays: boolean = false, calNameExtra: string = '', ) => { const events = []; let i = 1; for (const year in yearEvents) { if (!Object.hasOwn(yearEvents, year)) { continue; } for (const event of yearEvents[year]) { if (!event) { continue; } const ics = event.toIcs(parseInt(year), translator, clearLinkedText, i, onlyFirstDays, calNameExtra); if (ics !== null) { events.push(ics); } } i++; } if (events.length === 0) { return res.status(404).json({ error: 'Not found' }); } createEvents( events, (error, value) => { if (error) { Sentry.captureException(error); return res.status(500).json({ error: 'Unexpected server error' }); } res.header('Content-type', 'text/calendar'); res.send(value); }, ); }; const getEventName = (name: string, translator: Translator): string => { name = translator.get(`calendar.events.${name}`) ?? name; name = name.replace(/{.*?=(.*?)}/g, '$1'); return name; }; const buildMessage = ( events: string[], locale: LocaleDescription, translator: Translator, day: Day, link: string, ): string | null => { if (events.length === 0) { return null; } let message = `[${locale.name}] ${day.toString()}\n\n${translator.translate('calendar.banner')}:\n`; for (const event of events) { message += ` - ${event}\n`; } message += `\n${link}`; return message; }; const eventsSummary = async (config: Config, day: Day, locale: LocaleDescription) => { const [translator, calendar] = await Promise.all([loadTranslator(config.locale), loadCalendar(config.locale)]); const eventsRaw = calendar.getCurrentYear()!.eventsByDate[day.toString()]; const link = `${locale.url}/${encodeURIComponent(config.calendar!.route!)}/${day}`; const image = `${locale.url}/calendar/${locale.code}/${day}.png`; let message = null; const events = []; if (eventsRaw !== undefined && eventsRaw.length > 0) { for (const event of eventsRaw) { events.push(getEventName(event.name, translator)); delete event.daysMemoise; } } let eventsForMessage = [...events]; while (true) { message = buildMessage(eventsForMessage, locale, translator, day, link); if (message === null || message.length <= 280) { break; } else { eventsForMessage = eventsForMessage.slice(0, eventsForMessage.length - 1); } } return { day: day.toString(), link, image, message, events, eventsRaw: eventsRaw || [], }; }; const generateURIEncodedPathAlternative = (path: string): string[] => { return [path, path.replace('@', encodeURIComponent('@'))]; }; interface ProfileRaw { events: string; customEvents: string; } const router = Router(); router.get('/calendar/today', handleErrorAsync(async (req, res) => { const locale = getLocale(getH3Event(req)); const config = await loadConfig(locale); return res.json(await eventsSummary( config, Day.today(), req.locales[locale], )); })); router.get('/calendar/:year-:month-:day', handleErrorAsync(async (req, res) => { const locale = getLocale(getH3Event(req)); const config = await loadConfig(locale); return res.json(await eventsSummary( config, new Day(parseInt(req.params.year), parseInt(req.params.month), parseInt(req.params.day)), req.locales[locale], )); })); router.get( generateURIEncodedPathAlternative(`/queer-calendar-:locale-@:username.ics`), handleErrorAsync(async (req, res) => { const [translator, calendar] = await Promise.all([loadTranslator(req.params.locale), loadCalendar(req.params.locale)]); const profiles = await req.db.all>(SQL` SELECT events, customEvents FROM profiles p LEFT JOIN users u ON p.userId = u.id WHERE u.usernameNorm = ${normalise(req.params.username)} AND p.locale = ${req.params.locale} `); if (!profiles.length) { return res.status(404).json({ error: 'Not found' }); } const events = Calendar.generatePersonalCalendarEvents([ ...JSON.parse(profiles[0].events), ...JSON.parse(profiles[0].customEvents), ], calendar.getCurrentYear()!); const groupedEvents: Record = {}; for (const year of calendar.getAllYears()) { groupedEvents[year.year] = events; } renderEvents( groupedEvents, translator, res, req.query.only_first_days !== undefined, ` (@${req.params.username})`, ); }), ); router.get(`/queer-calendar-:locale.ics`, handleErrorAsync(async (req, res) => { const [translator, calendar] = await Promise.all([loadTranslator(req.params.locale), loadCalendar(req.params.locale)]); const events: Record = {}; for (const year of calendar.getAllYears()) { events[year.year] = year.events; } renderEvents(events, translator, res, req.query.only_first_days !== undefined); })); router.get(`/queer-calendar-:locale-:year-:uuid.ics`, handleErrorAsync(async (req, res) => { const [translator, calendar] = await Promise.all([loadTranslator(req.params.locale), loadCalendar(req.params.locale)]); const year = calendar.getYear(parseInt(req.params.year)); if (!year) { return res.status(404).json({ error: 'Not found' }); } renderEvents({ [year.year]: [year.eventsByUuid[req.params.uuid]] }, translator, res); })); router.get(`/queer-calendar-:locale-:year.ics`, handleErrorAsync(async (req, res) => { const [translator, calendar] = await Promise.all([loadTranslator(req.params.locale), loadCalendar(req.params.locale)]); const year = calendar.getYear(parseInt(req.params.year)); if (!year) { return res.status(404).json({ error: 'Not found' }); } renderEvents({ [year.year]: year.events }, translator, res); })); export default router;