mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-30 16:44:29 -04:00

the #shared alias used by Nuxt cannot be easily disabled and to prevent breackage with jiti, we make use of it
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 { 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<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;
|