227 lines
7.2 KiB
TypeScript

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 { Calendar, Day } from '../../src/calendar/helpers.ts';
import type { Event } from '../../src/calendar/helpers.ts';
import { handleErrorAsync, clearLinkedText } from '../../src/helpers.ts';
import { normalise } from './user.ts';
import type { Config } from '~/locale/config.ts';
import { getLocale, loadCalendar, loadConfig, loadTranslator } from '~/server/data.ts';
import type { Translator } from '~/src/translator.ts';
// TODO caching? // import { caches } from "../../src/cache";
const renderEvents = (
yearEvents: Record<number, Event[]>,
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<string | undefined>(`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<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 = ${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<number, Event[]> = {};
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<number, Event[]> = {};
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;