import { Router } from 'express'; import type { Response } from 'express'; import * as Sentry from '@sentry/node'; import { handleErrorAsync, clearLinkedText } from '../../src/helpers.ts'; import { createEvents } from 'ics'; import { calendar } from '../../src/calendar/calendar.ts'; import { loadSuml, loadSumlFromBase } from '../loader.ts'; import { Calendar, Day } from '../../src/calendar/helpers.ts'; import SQL from 'sql-template-strings'; import { normalise } from './user.ts'; import type { Translations } from '../../locale/translations.ts'; import type { Event } from '../../src/calendar/helpers.ts'; import type { LocaleDescription } from '../../locale/locales.ts'; // TODO caching? // import { caches } from "../../src/cache"; const translations = loadSuml('translations') as Translations; const fallbackTranslations = loadSumlFromBase('locale/_base/translations') as Translations; const renderEvents = (yearEvents: Record, res: Response, onlyFirstDays: boolean = false, calNameExtra: string = '') => { const events = []; let i = 1; for (const year in yearEvents) { if (!yearEvents.hasOwnProperty(year)) { continue; } for (const event of yearEvents[year]) { if (!event) { continue; } const ics = event.toIcs(parseInt(year), translations, fallbackTranslations, 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): string => { name = translations.calendar.events[name] || fallbackTranslations.calendar.events[name] || name; name = name.replace(/{.*?=(.*?)}/g, '$1'); return name; }; const buildMessage = (events: string[], locale: LocaleDescription, day: Day, link: string): string | null => { if (events.length === 0) { return null; } let message = `[${locale.name}] ${day.toString()}\n\n${translations.calendar.banner}:\n`; for (const event of events) { message += ` - ${event}\n`; } message += `\n${link}`; return message; }; const eventsSummary = (day: Day, locale: LocaleDescription) => { const eventsRaw = calendar.getCurrentYear()!.eventsByDate[day.toString()]; const link = `${locale.url}/${encodeURIComponent(global.config.calendar!.route!)}/${day}`; const image = `${locale.url}/calendar/${day}.png`; let message = null; const events = []; if (eventsRaw !== undefined && eventsRaw.length > 0) { for (const event of eventsRaw) { events.push(getEventName(event.name)); delete event.daysMemoise; } } let eventsForMessage = [...events]; while (true) { message = buildMessage(eventsForMessage, locale, 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) => { return res.json(eventsSummary( Day.today(), req.locales[global.config.locale], )); })); router.get('/calendar/:year-:month-:day', handleErrorAsync(async (req, res) => { return res.json(eventsSummary( new Day(parseInt(req.params.year), parseInt(req.params.month), parseInt(req.params.day)), req.locales[global.config.locale], )); })); const routeBase = `/queer-calendar-${global.config.locale}`; router.get(`${routeBase}.ics`, handleErrorAsync(async (req, res) => { const events: Record = {}; for (const year of calendar.getAllYears()) { events[year.year] = year.events; } renderEvents(events, res, req.query.only_first_days !== undefined); })); router.get(generateURIEncodedPathAlternative(`${routeBase}-@:username.ics`), handleErrorAsync(async (req, res) => { 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 = ${global.config.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, res, req.query.only_first_days !== undefined, ` (@${req.params.username})`); })); router.get(`${routeBase}-:year-:uuid.ics`, handleErrorAsync(async (req, res) => { 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]] }, res); })); router.get(`${routeBase}-:year.ics`, handleErrorAsync(async (req, res) => { const year = calendar.getYear(parseInt(req.params.year)); if (!year) { return res.status(404).json({ error: 'Not found' }); } renderEvents({ [year.year]: year.events }, res); })); export default router;