This takes part of the work being done in #96 that was reverted but still useful. Note Tailwind and Fresh weren't upgraded because there's no security vulnerability in either, and I have found the new versions to be worse in performance. Thos will likely stay at those fixed versions going forward.
339 lines
8.6 KiB
TypeScript
339 lines
8.6 KiB
TypeScript
import { createDAVClient } from '/lib/models/dav.js';
|
|
import { AppConfig } from '/lib/config.ts';
|
|
import { getColorAsHex, parseVCalendar } from '/lib/utils/calendar.ts';
|
|
import { concurrentPromises } from '/lib/utils/misc.ts';
|
|
import { UserModel } from '/lib/models/user.ts';
|
|
|
|
interface DAVObject extends Record<string, any> {
|
|
data?: string;
|
|
displayName?: string;
|
|
ctag?: string;
|
|
url: string;
|
|
uid?: string;
|
|
}
|
|
|
|
export interface Calendar extends DAVObject {
|
|
calendarColor?: string;
|
|
isVisible: boolean;
|
|
}
|
|
|
|
export interface CalendarEvent extends DAVObject {
|
|
calendarId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
title: string;
|
|
isAllDay: boolean;
|
|
organizerEmail: string;
|
|
attendees?: CalendarEventAttendee[];
|
|
reminders?: CalendarEventReminder[];
|
|
transparency: 'opaque' | 'transparent';
|
|
description?: string;
|
|
location?: string;
|
|
eventUrl?: string;
|
|
sequence?: number;
|
|
isRecurring?: boolean;
|
|
recurringRrule?: string;
|
|
recurrenceId?: string;
|
|
recurrenceMasterUid?: string;
|
|
}
|
|
|
|
export interface CalendarEventAttendee {
|
|
email: string;
|
|
status: 'accepted' | 'rejected' | 'invited';
|
|
name?: string;
|
|
}
|
|
|
|
export interface CalendarEventReminder {
|
|
uid?: string;
|
|
startDate: string;
|
|
type: 'email' | 'sound' | 'display';
|
|
acknowledgedAt?: string;
|
|
description?: string;
|
|
}
|
|
|
|
const calendarConfig = await AppConfig.getCalendarConfig();
|
|
|
|
async function getClient(userId: string) {
|
|
const client = await createDAVClient({
|
|
serverUrl: calendarConfig.calDavUrl,
|
|
credentials: {},
|
|
authMethod: 'Custom',
|
|
// deno-lint-ignore require-await
|
|
authFunction: async () => {
|
|
return {
|
|
'X-Remote-User': userId,
|
|
};
|
|
},
|
|
fetchOptions: {
|
|
timeout: 15_000,
|
|
},
|
|
defaultAccountType: 'caldav',
|
|
rootUrl: `${calendarConfig.calDavUrl}/`,
|
|
principalUrl: `${calendarConfig.calDavUrl}/${userId}/`,
|
|
homeUrl: `${calendarConfig.calDavUrl}/${userId}/`,
|
|
});
|
|
|
|
return client;
|
|
}
|
|
|
|
export class CalendarModel {
|
|
static async list(
|
|
userId: string,
|
|
): Promise<Calendar[]> {
|
|
const client = await getClient(userId);
|
|
|
|
const calendarUrl = `${calendarConfig.calDavUrl}/${userId}/`;
|
|
|
|
const davCalendars: DAVObject[] = await client.fetchCalendars({
|
|
calendar: {
|
|
url: calendarUrl,
|
|
},
|
|
});
|
|
|
|
const user = await UserModel.getById(userId);
|
|
|
|
const calendars: Calendar[] = davCalendars.map((davCalendar) => {
|
|
const uid = davCalendar.url.split('/').filter(Boolean).pop()!;
|
|
|
|
return {
|
|
...davCalendar,
|
|
displayName: decodeURIComponent(davCalendar.displayName || '(empty)'),
|
|
calendarColor: decodeURIComponent(
|
|
typeof davCalendar.calendarColor === 'string' ? davCalendar.calendarColor : getColorAsHex('bg-gray-700'),
|
|
),
|
|
isVisible: !user.extra.hidden_calendar_ids?.includes(uid),
|
|
uid,
|
|
};
|
|
});
|
|
|
|
return calendars;
|
|
}
|
|
|
|
static async get(
|
|
userId: string,
|
|
calendarId: string,
|
|
): Promise<Calendar | undefined> {
|
|
const calendars = await this.list(userId);
|
|
|
|
return calendars.find((calendar) => calendar.uid === calendarId);
|
|
}
|
|
|
|
static async create(
|
|
userId: string,
|
|
name: string,
|
|
color: string,
|
|
): Promise<void> {
|
|
const calendarId = crypto.randomUUID();
|
|
const calendarUrl = `${calendarConfig.calDavUrl}/${userId}/${calendarId}/`;
|
|
|
|
const client = await getClient(userId);
|
|
|
|
await client.makeCalendar({
|
|
url: calendarUrl,
|
|
props: {
|
|
displayname: name,
|
|
},
|
|
});
|
|
|
|
// Cannot properly set color with makeCalendar, so we quickly update it instead
|
|
await this.update(userId, calendarUrl, name, color);
|
|
}
|
|
|
|
static async update(
|
|
userId: string,
|
|
calendarUrl: string,
|
|
displayName: string,
|
|
color?: string,
|
|
): Promise<void> {
|
|
// Make "manual" request (https://www.rfc-editor.org/rfc/rfc4791.html#page-20) because the dav client doesn't have PROPPATCH
|
|
const xmlBody = `<?xml version="1.0" encoding="utf-8"?>
|
|
<d:proppatch xmlns:d="DAV:" xmlns:a="http://apple.com/ns/ical/">
|
|
<d:set>
|
|
<d:prop>
|
|
<d:displayname>${encodeURIComponent(displayName)}</d:displayname>
|
|
${color ? `<a:calendar-color>${encodeURIComponent(color)}</a:calendar-color>` : ''}
|
|
</d:prop>
|
|
</d:set>
|
|
</d:proppatch>`;
|
|
|
|
await fetch(calendarUrl, {
|
|
method: 'PROPPATCH',
|
|
headers: {
|
|
'Content-Type': 'application/xml; charset=utf-8',
|
|
'X-Remote-User': userId,
|
|
},
|
|
body: xmlBody,
|
|
});
|
|
}
|
|
|
|
static async delete(
|
|
userId: string,
|
|
calendarId: string,
|
|
): Promise<void> {
|
|
const calendarUrl = `${calendarConfig.calDavUrl}/${userId}/${calendarId}/`;
|
|
|
|
const client = await getClient(userId);
|
|
|
|
await client.deleteObject({
|
|
url: calendarUrl,
|
|
});
|
|
}
|
|
}
|
|
|
|
export class CalendarEventModel {
|
|
private static async fetchByCalendarId(
|
|
userId: string,
|
|
calendarId: string,
|
|
dateRange?: { start: Date; end: Date },
|
|
): Promise<CalendarEvent[]> {
|
|
const client = await getClient(userId);
|
|
|
|
const fetchOptions: { calendar: { url: string }; timeRange?: { start: string; end: string }; expand?: boolean } = {
|
|
calendar: {
|
|
url: `${calendarConfig.calDavUrl}/${userId}/${calendarId}/`,
|
|
},
|
|
};
|
|
|
|
const davCalendarEvents: DAVObject[] = await client.fetchCalendarObjects(fetchOptions);
|
|
|
|
if (dateRange) {
|
|
fetchOptions.timeRange = {
|
|
start: dateRange.start.toISOString(),
|
|
end: dateRange.end.toISOString(),
|
|
};
|
|
fetchOptions.expand = true;
|
|
|
|
// Sometimes the expand option doesn't return anything, so we we fetch with and without it, when queried for a date range
|
|
const davCalendarEventsWithExpansion = await client.fetchCalendarObjects(fetchOptions);
|
|
|
|
for (const davCalendarEvent of davCalendarEventsWithExpansion) {
|
|
// Only add the events that are not already in the list
|
|
if (!davCalendarEvents.some((davCalendarEvent) => davCalendarEvent.url === davCalendarEvent.url)) {
|
|
davCalendarEvents.push(davCalendarEvent);
|
|
}
|
|
}
|
|
}
|
|
|
|
const calendarEvents: CalendarEvent[] = [];
|
|
|
|
for (const davCalendarEvent of davCalendarEvents) {
|
|
let uid = davCalendarEvent.url.split('/').filter(Boolean).pop()!;
|
|
|
|
const parsedEvents = parseVCalendar(davCalendarEvent.data || '');
|
|
|
|
for (const parsedEvent of parsedEvents) {
|
|
if (parsedEvent.uid) {
|
|
uid = parsedEvent.uid;
|
|
}
|
|
|
|
calendarEvents.push({
|
|
...davCalendarEvent,
|
|
...parsedEvent,
|
|
uid,
|
|
calendarId,
|
|
});
|
|
}
|
|
}
|
|
|
|
return calendarEvents;
|
|
}
|
|
|
|
static async list(
|
|
userId: string,
|
|
calendarIds: string[],
|
|
dateRange?: { start: Date; end: Date },
|
|
): Promise<CalendarEvent[]> {
|
|
const allCalendarEvents: CalendarEvent[] = [];
|
|
|
|
await concurrentPromises(
|
|
calendarIds.map((calendarId) => async () => {
|
|
const calendarEvents = await this.fetchByCalendarId(userId, calendarId, dateRange);
|
|
|
|
allCalendarEvents.push(...calendarEvents);
|
|
|
|
return calendarEvents;
|
|
}),
|
|
5,
|
|
);
|
|
|
|
return allCalendarEvents;
|
|
}
|
|
|
|
static async get(
|
|
userId: string,
|
|
calendarId: string,
|
|
eventId: string,
|
|
): Promise<CalendarEvent | undefined> {
|
|
const client = await getClient(userId);
|
|
|
|
const davCalendarEvents: DAVObject[] = await client.fetchCalendarObjects({
|
|
calendar: {
|
|
url: `${calendarConfig.calDavUrl}/${userId}/${calendarId}/`,
|
|
},
|
|
objectUrls: [`${calendarConfig.calDavUrl}/${userId}/${calendarId}/${eventId}.ics`],
|
|
});
|
|
|
|
if (davCalendarEvents.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const davCalendarEvent = davCalendarEvents[0];
|
|
|
|
const calendarEvent: CalendarEvent = {
|
|
...davCalendarEvent,
|
|
...parseVCalendar(davCalendarEvent.data || '')[0],
|
|
uid: eventId,
|
|
calendarId,
|
|
};
|
|
|
|
return calendarEvent;
|
|
}
|
|
|
|
static async create(
|
|
userId: string,
|
|
calendarId: string,
|
|
eventId: string,
|
|
vCalendar: string,
|
|
): Promise<void> {
|
|
const client = await getClient(userId);
|
|
|
|
const calendarUrl = `${calendarConfig.calDavUrl}/${userId}/${calendarId}/`;
|
|
|
|
await client.createCalendarObject({
|
|
calendar: {
|
|
url: calendarUrl,
|
|
},
|
|
iCalString: vCalendar,
|
|
filename: `${eventId}.ics`,
|
|
});
|
|
}
|
|
|
|
static async update(
|
|
userId: string,
|
|
eventUrl: string,
|
|
ics: string,
|
|
): Promise<void> {
|
|
const client = await getClient(userId);
|
|
|
|
await client.updateCalendarObject({
|
|
calendarObject: {
|
|
url: eventUrl,
|
|
data: ics,
|
|
},
|
|
});
|
|
}
|
|
|
|
static async delete(
|
|
userId: string,
|
|
eventUrl: string,
|
|
): Promise<void> {
|
|
const client = await getClient(userId);
|
|
|
|
await client.deleteCalendarObject({
|
|
calendarObject: {
|
|
url: eventUrl,
|
|
},
|
|
});
|
|
}
|
|
}
|