Valentyne Stigloher b25afefc49 (fmt)
2024-10-29 10:56:32 +01:00

197 lines
6.2 KiB
TypeScript

import * as Sentry from '@sentry/node';
import { Router } from 'express';
import type { Response } from 'express';
import { createEvents } from 'ics';
import SQL from 'sql-template-strings';
import type { LocaleDescription } from '../../locale/locales.ts';
import type { Translations } from '../../locale/translations.ts';
import { buildCalendar } from '../../src/calendar/calendar.ts';
import { Calendar, Day } from '../../src/calendar/helpers.ts';
import type { Event } from '../../src/calendar/helpers.ts';
import { handleErrorAsync, clearLinkedText } from '../../src/helpers.ts';
import { config } from '../globals.ts';
import { loadSuml, loadSumlFromBase } from '../loader.ts';
import { normalise } from './user.ts';
// TODO caching? // import { caches } from "../../src/cache";
const translations = loadSuml('translations') as Translations;
const fallbackTranslations = loadSumlFromBase('locale/_base/translations') as Translations;
const calendar = buildCalendar(process.env.BASE_URL!);
const renderEvents = (yearEvents: Record<number, Event[]>, 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(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[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[config.locale],
));
}));
const routeBase = `/queer-calendar-${config.locale}`;
router.get(`${routeBase}.ics`, handleErrorAsync(async (req, res) => {
const events: Record<number, Event[]> = {};
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<Pick<ProfileRaw, 'events' | 'customEvents'>>(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 = ${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<number, Event[]> = {};
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;