92 lines
2.6 KiB
TypeScript
92 lines
2.6 KiB
TypeScript
import ical from "ical-generator";
|
|
import { NextResponse } from "next/server";
|
|
import { prisma } from "./prisma";
|
|
|
|
const DEFAULT_APP_NAME = "Vereinskalender";
|
|
|
|
const toFilename = (value: string) =>
|
|
value
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/(^-|-$)/g, "") || "kalender";
|
|
|
|
export async function getIcalResponse(request: Request, token: string) {
|
|
const view = await prisma.userView.findUnique({
|
|
where: { token },
|
|
include: {
|
|
items: { include: { event: true } },
|
|
categories: true,
|
|
exclusions: true,
|
|
user: true
|
|
}
|
|
});
|
|
|
|
if (!view) {
|
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
}
|
|
|
|
const appNameSetting = await prisma.setting.findUnique({
|
|
where: { key: "app_name" }
|
|
});
|
|
const appName = appNameSetting?.value || DEFAULT_APP_NAME;
|
|
const url = new URL(request.url);
|
|
const pastDaysParam = Number(url.searchParams.get("pastDays"));
|
|
const rawPastDays =
|
|
Number.isFinite(pastDaysParam) && pastDaysParam >= 0
|
|
? pastDaysParam
|
|
: Number.isFinite(view.icalPastDays) && view.icalPastDays >= 0
|
|
? view.icalPastDays
|
|
: 14;
|
|
const pastDays = Math.min(365, Math.floor(rawPastDays));
|
|
const cutoff = new Date(Date.now() - pastDays * 24 * 60 * 60 * 1000);
|
|
|
|
const calendar = ical({
|
|
name: appName,
|
|
timezone: "Europe/Berlin"
|
|
});
|
|
|
|
const excludedIds = new Set(view.exclusions.map((item) => item.eventId));
|
|
const explicitEvents = view.items
|
|
.map((item) => item.event)
|
|
.filter((event) => event.status === "APPROVED");
|
|
|
|
const categoryIds = view.categories.map((item) => item.categoryId);
|
|
const categoryEvents =
|
|
categoryIds.length > 0
|
|
? await prisma.event.findMany({
|
|
where: { categoryId: { in: categoryIds }, status: "APPROVED" }
|
|
})
|
|
: [];
|
|
|
|
const combined = [...explicitEvents, ...categoryEvents].filter(
|
|
(event, index, all) =>
|
|
all.findIndex((item) => item.id === event.id) === index &&
|
|
!excludedIds.has(event.id) &&
|
|
event.startAt >= cutoff
|
|
);
|
|
|
|
combined.forEach((event) => {
|
|
const start = event.startAt;
|
|
const end =
|
|
event.endAt || new Date(event.startAt.getTime() + 3 * 60 * 60 * 1000);
|
|
calendar.createEvent({
|
|
id: event.id,
|
|
summary: event.title,
|
|
description: event.description || undefined,
|
|
location: event.location || undefined,
|
|
start,
|
|
end
|
|
});
|
|
});
|
|
|
|
const filename = `${toFilename(appName)}.ical`;
|
|
|
|
return new NextResponse(calendar.toString(), {
|
|
headers: {
|
|
"Content-Type": "text/calendar; charset=utf-8",
|
|
"Content-Disposition": `attachment; filename="${filename}"`
|
|
}
|
|
});
|
|
}
|