mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-07 22:40:27 -04:00
227 lines
7.2 KiB
TypeScript
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;
|