+
{calendarEvent.extra.reminders.map((reminder) => (
-
+
{reminder.description || 'Reminder'} at {hourFormat.format(new Date(reminder.start_date))} via{' '}
{reminder.type}.
diff --git a/crons/news.ts b/crons/news.ts
index 3be18b4..3cb080b 100644
--- a/crons/news.ts
+++ b/crons/news.ts
@@ -1,4 +1,5 @@
import Database, { sql } from '/lib/interfaces/database.ts';
+import Locker from '/lib/interfaces/locker.ts';
import { NewsFeed } from '/lib/types.ts';
import { concurrentPromises } from '/lib/utils/misc.ts';
import { crawlNewsFeed } from '/lib/data/news.ts';
@@ -8,6 +9,10 @@ const db = new Database();
export async function fetchNewArticles(forceFetch = false) {
const fourHoursAgo = forceFetch ? new Date() : new Date(new Date().setUTCHours(new Date().getUTCHours() - 4));
+ const lock = new Locker(`feeds`);
+
+ await lock.acquire();
+
try {
const feedsToCrawl = await db.query(
sql`SELECT * FROM "bewcloud_news_feeds" WHERE "last_crawled_at" IS NULL OR "last_crawled_at" <= $1`,
@@ -22,4 +27,6 @@ export async function fetchNewArticles(forceFetch = false) {
} catch (error) {
console.log(error);
}
+
+ lock.release();
}
diff --git a/import_map.json b/import_map.json
index d13da9a..b6659a9 100644
--- a/import_map.json
+++ b/import_map.json
@@ -3,6 +3,7 @@
"/": "./",
"./": "./",
"xml/": "https://deno.land/x/xml@2.1.3/",
+ "rrule-rust": "npm:rrule-rust@1.2.0",
"fresh/": "https://deno.land/x/fresh@1.6.8/",
"$fresh/": "https://deno.land/x/fresh@1.6.8/",
diff --git a/lib/data/calendar.ts b/lib/data/calendar.ts
index 203d940..a6605a4 100644
--- a/lib/data/calendar.ts
+++ b/lib/data/calendar.ts
@@ -1,7 +1,10 @@
+import { RRuleSet } from 'rrule-rust';
+
import Database, { sql } from '/lib/interfaces/database.ts';
-import { Calendar, CalendarEvent } from '/lib/types.ts';
+import Locker from '/lib/interfaces/locker.ts';
+import { Calendar, CalendarEvent, CalendarEventReminder } from '/lib/types.ts';
import { getRandomItem } from '/lib/utils/misc.ts';
-import { CALENDAR_COLOR_OPTIONS } from '/lib/utils/calendar.ts';
+import { CALENDAR_COLOR_OPTIONS, getVCalendarDate } from '/lib/utils/calendar.ts';
import { getUserById } from './user.ts';
const db = new Database();
@@ -33,8 +36,129 @@ export async function getCalendarEvents(
return calendarEvents;
} else {
+ // Fetch initial recurring events and calculate any necessary to create/show for the date range, if it's not in the past
+ if (dateRange.end >= new Date()) {
+ const lock = new Locker(`events-${userId}`);
+
+ await lock.acquire();
+
+ const initialRecurringCalendarEvents = await db.query(
+ sql`SELECT * FROM "bewcloud_calendar_events"
+ WHERE "user_id" = $1
+ AND "calendar_id" = ANY($2)
+ AND "start_date" <= $3
+ AND ("extra" ->> 'is_recurring')::boolean IS TRUE
+ AND ("extra" ->> 'recurring_id')::uuid = "id"
+ ORDER BY "start_date" ASC`,
+ [
+ userId,
+ calendarIds,
+ dateRange.end,
+ ],
+ );
+
+ // For each initial recurring event, check instance dates, check if those exist in calendarEvents. If not, create them.
+ for (const initialRecurringCalendarEvent of initialRecurringCalendarEvents) {
+ try {
+ const oneMonthAgo = new Date(new Date().setUTCMonth(new Date().getUTCMonth() - 1));
+
+ let recurringInstanceStartDate = initialRecurringCalendarEvent.start_date;
+ let lastSequence = initialRecurringCalendarEvent.extra.recurring_sequence!;
+
+ if (recurringInstanceStartDate <= oneMonthAgo) {
+ // Fetch the latest recurring sample, so we don't have to calculate as many recurring dates, but still preserve the original date's properties for generating the recurring instances
+ const latestRecurringInstance = (await db.query(
+ sql`SELECT * FROM "bewcloud_calendar_events"
+ WHERE "user_id" = $1
+ AND "calendar_id" = ANY($2)
+ AND "start_date" <= $3
+ AND ("extra" ->> 'is_recurring')::boolean IS TRUE
+ AND ("extra" ->> 'recurring_id')::uuid = $4
+ ORDER BY ("extra" ->> 'recurring_sequence')::number DESC
+ LIMIT 1`,
+ [
+ userId,
+ calendarIds,
+ dateRange.end,
+ initialRecurringCalendarEvent.extra.recurring_id!,
+ ],
+ ))[0];
+
+ if (latestRecurringInstance) {
+ recurringInstanceStartDate = latestRecurringInstance.start_date;
+ lastSequence = latestRecurringInstance.extra.recurring_sequence!;
+ }
+ }
+
+ const rRuleSet = RRuleSet.parse(
+ `DTSTART:${
+ getVCalendarDate(recurringInstanceStartDate)
+ }\n${initialRecurringCalendarEvent.extra.recurring_rrule}`,
+ );
+
+ const maxRecurringDatesToGenerate = 30;
+
+ const timestamps = rRuleSet.all(maxRecurringDatesToGenerate);
+
+ const validDates = timestamps.map((timestamp) => new Date(timestamp)).filter((date) => date <= dateRange.end);
+
+ // For each date, check if an instance already exists. If not, create it and add it.
+ for (const instanceDate of validDates) {
+ instanceDate.setHours(recurringInstanceStartDate.getHours()); // NOTE: Something is making the hour shift when it shouldn't
+
+ const matchingRecurringInstance = (await db.query(
+ sql`SELECT * FROM "bewcloud_calendar_events"
+ WHERE "user_id" = $1
+ AND "calendar_id" = ANY($2)
+ AND "start_date" = $3
+ AND ("extra" ->> 'is_recurring')::boolean IS TRUE
+ AND ("extra" ->> 'recurring_id')::uuid = $4
+ ORDER BY "start_date" ASC
+ LIMIT 1`,
+ [
+ userId,
+ calendarIds,
+ instanceDate,
+ initialRecurringCalendarEvent.extra.recurring_id!,
+ ],
+ ))[0];
+
+ if (!matchingRecurringInstance) {
+ const oneHourLater = new Date(new Date(instanceDate).setHours(instanceDate.getHours() + 1));
+ const newCalendarEvent = await createCalendarEvent(
+ userId,
+ initialRecurringCalendarEvent.calendar_id,
+ initialRecurringCalendarEvent.title,
+ instanceDate,
+ oneHourLater,
+ initialRecurringCalendarEvent.is_all_day,
+ );
+
+ newCalendarEvent.extra = { ...newCalendarEvent.extra, ...initialRecurringCalendarEvent.extra };
+
+ newCalendarEvent.extra.recurring_sequence = ++lastSequence;
+
+ await updateCalendarEvent(newCalendarEvent);
+ }
+ }
+ } catch (error) {
+ console.error(`Error generating recurring instances: ${error}`);
+ console.error(error);
+ }
+ }
+
+ lock.release();
+ }
+
const calendarEvents = await db.query(
- sql`SELECT * FROM "bewcloud_calendar_events" WHERE "user_id" = $1 AND "calendar_id" = ANY($2) AND (("start_date" >= $3 OR "end_date" <= $4) OR ("start_date" < $3 AND "end_date" > $4)) ORDER BY "start_date" ASC`,
+ sql`SELECT * FROM "bewcloud_calendar_events"
+ WHERE "user_id" = $1
+ AND "calendar_id" = ANY($2)
+ AND (
+ ("start_date" >= $3 OR "end_date" <= $4)
+ OR ("start_date" < $3 AND "end_date" > $4)
+ )
+ ORDER BY "start_date" ASC`,
[
userId,
calendarIds,
@@ -43,8 +167,6 @@ export async function getCalendarEvents(
],
);
- // TODO: Fetch initial recurring events and calculate any necessary to create/show for the date range
-
return calendarEvents;
}
}
@@ -183,9 +305,18 @@ export async function createCalendarEvent(
throw new Error('Calendar not found');
}
+ const oneHourEarlier = new Date(new Date(startDate).setHours(new Date(startDate).getHours() - 1));
+ const sameDayAtNine = new Date(new Date(startDate).setHours(9));
+
+ const newReminder: CalendarEventReminder = {
+ start_date: isAllDay ? sameDayAtNine.toISOString() : oneHourEarlier.toISOString(),
+ type: 'display',
+ };
+
const extra: CalendarEvent['extra'] = {
organizer_email: user.email,
transparency: 'default',
+ reminders: [newReminder],
};
const revision = crypto.randomUUID();
@@ -240,6 +371,30 @@ export async function updateCalendarEvent(calendarEvent: CalendarEvent, oldCalen
const oldCalendar = oldCalendarId ? await getCalendar(oldCalendarId, user.id) : null;
+ const oldCalendarEvent = await getCalendarEvent(calendarEvent.id, user.id);
+
+ if (oldCalendarEvent.start_date !== calendarEvent.start_date) {
+ const oneHourEarlier = new Date(
+ new Date(calendarEvent.start_date).setHours(new Date(calendarEvent.start_date).getHours() - 1),
+ );
+ const sameDayAtNine = new Date(new Date(calendarEvent.start_date).setHours(9));
+
+ const newReminder: CalendarEventReminder = {
+ start_date: calendarEvent.is_all_day ? sameDayAtNine.toISOString() : oneHourEarlier.toISOString(),
+ type: 'display',
+ };
+
+ if (!Array.isArray(calendarEvent.extra.reminders)) {
+ calendarEvent.extra.reminders = [newReminder];
+ } else {
+ if (calendarEvent.extra.reminders.length === 0) {
+ calendarEvent.extra.reminders.push(newReminder);
+ } else {
+ calendarEvent.extra.reminders[0] = { ...calendarEvent.extra.reminders[0], start_date: newReminder.start_date };
+ }
+ }
+ }
+
await db.query(
sql`UPDATE "bewcloud_calendar_events" SET
"revision" = $3,
diff --git a/lib/interfaces/locker.ts b/lib/interfaces/locker.ts
new file mode 100644
index 0000000..2e7fd0c
--- /dev/null
+++ b/lib/interfaces/locker.ts
@@ -0,0 +1,36 @@
+// This is a simple, in-memory lock, which works well enough if you're serving from a single source/server
+
+function wait(milliseconds = 100) {
+ return new Promise((resolve) => setTimeout(() => resolve(true), milliseconds));
+}
+
+const expirationInMilliseconds = 15 * 1000;
+
+const locks: Map = new Map();
+
+export default class Locker {
+ protected lockName: string = '';
+
+ constructor(lockName: string) {
+ this.lockName = lockName;
+ }
+
+ public async acquire() {
+ const currentLock = locks.get(this.lockName);
+
+ while (currentLock) {
+ // Only wait if the lock hasn't expired
+ if (currentLock.expiresAt > new Date()) {
+ await wait();
+ }
+ }
+
+ locks.set(this.lockName, {
+ expiresAt: new Date(new Date().setMilliseconds(new Date().getMilliseconds() + expirationInMilliseconds)),
+ });
+ }
+
+ public release() {
+ locks.delete(this.lockName);
+ }
+}
diff --git a/lib/utils/calendar.ts b/lib/utils/calendar.ts
index 870e818..fd20b1a 100644
--- a/lib/utils/calendar.ts
+++ b/lib/utils/calendar.ts
@@ -65,6 +65,18 @@ function getAttendeeStatusFromVCalendar(
return 'invited';
}
+export function getVCalendarDate(date: Date | string) {
+ return new Date(date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '');
+}
+
+function getSafelyEscapedTextForVCalendar(text: string) {
+ return text.replaceAll('\n', '\\n').replaceAll(',', '\\,');
+}
+
+function getSafelyUnescapedTextFromVCalendar(text: string) {
+ return text.replaceAll('\\n', '\n').replaceAll('\\,', ',');
+}
+
// TODO: Build this
export function formatCalendarEventsToVCalendar(
calendarEvents: CalendarEvent[],
@@ -72,23 +84,25 @@ export function formatCalendarEventsToVCalendar(
): string {
const vCalendarText = calendarEvents.map((calendarEvent) =>
`BEGIN:VEVENT
-DTSTAMP:${new Date(calendarEvent.created_at).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')}
-DTSTART:${new Date(calendarEvent.start_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')}
-DTEND:${new Date(calendarEvent.end_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')}
+DTSTAMP:${getVCalendarDate(calendarEvent.created_at)}
+DTSTART:${getVCalendarDate(calendarEvent.start_date)}
+DTEND:${getVCalendarDate(calendarEvent.end_date)}
ORGANIZER;CN=:mailto:${calendarEvent.extra.organizer_email}
-SUMMARY:${calendarEvent.title.replaceAll('\n', '\\n').replaceAll(',', '\\,')}
+SUMMARY:${getSafelyEscapedTextForVCalendar(calendarEvent.title)}
TRANSP:${getCalendarEventTransparency(calendarEvent, calendars).toUpperCase()}
${calendarEvent.extra.uid ? `UID:${calendarEvent.extra.uid}` : ''}
-${calendarEvent.extra.recurring_rrule ? `RRULE:${calendarEvent.extra.recurring_rrule}` : ''}
-SEQUENCE:${calendarEvent.extra.recurring_sequence || 0}
-CREATED:${new Date(calendarEvent.created_at).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')}
-LAST-MODIFIED:${
- new Date(calendarEvent.updated_at).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')
+${
+ calendarEvent.extra.is_recurring && calendarEvent.extra.recurring_rrule
+ ? `RRULE:${calendarEvent.extra.recurring_rrule}`
+ : ''
}
+SEQUENCE:${calendarEvent.extra.recurring_sequence || 0}
+CREATED:${getVCalendarDate(calendarEvent.created_at)}
+LAST-MODIFIED:${getVCalendarDate(calendarEvent.updated_at)}
${
calendarEvent.extra.attendees?.map((attendee) =>
`ATTENDEE;PARTSTAT=${getVCalendarAttendeeStatus(attendee.status)};CN=${
- attendee.name?.replaceAll('\n', '\\n').replaceAll(',', '\\,') || ''
+ getSafelyEscapedTextForVCalendar(attendee.name || '')
}:mailto:${attendee.email}`
).join('\n') || ''
}
@@ -96,18 +110,10 @@ ${
calendarEvent.extra.reminders?.map((reminder) =>
`BEGIN:VALARM
ACTION:${reminder.type.toUpperCase()}
-${reminder.description ? `DESCRIPTION:${reminder.description.replaceAll('\n', '\\n').replaceAll(',', '\\,')}` : ''}
-TRIGGER;VALUE=DATE-TIME:${
- new Date(reminder.start_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')
- }
+${reminder.description ? `DESCRIPTION:${getSafelyEscapedTextForVCalendar(reminder.description)}` : ''}
+TRIGGER;VALUE=DATE-TIME:${getVCalendarDate(reminder.start_date)}
${reminder.uid ? `UID:${reminder.uid}` : ''}
-${
- reminder.acknowledged_at
- ? `ACKNOWLEDGED:${
- new Date(reminder.acknowledged_at).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')
- }`
- : ''
- }
+${reminder.acknowledged_at ? `ACKNOWLEDGED:${getVCalendarDate(reminder.acknowledged_at)}` : ''}
END:VALARM`
).join('\n') || ''
}
@@ -222,7 +228,7 @@ export function parseVCalendarFromTextContents(text: string): Partial
`BEGIN:VCARD
@@ -10,10 +18,10 @@ N:${contact.last_name};${contact.first_name};${
contact.extra.middle_names ? contact.extra.middle_names?.map((name) => name.trim()).filter(Boolean).join(' ') : ''
};${contact.extra.name_title || ''};
FN:${contact.extra.name_title ? `${contact.extra.name_title || ''} ` : ''}${contact.first_name} ${contact.last_name}
-${contact.extra.organization ? `ORG:${contact.extra.organization.replaceAll(',', '\\,')}` : ''}
-${contact.extra.role ? `TITLE:${contact.extra.role}` : ''}
+${contact.extra.organization ? `ORG:${getSafelyEscapedTextForVCard(contact.extra.organization)}` : ''}
+${contact.extra.role ? `TITLE:${getSafelyEscapedTextForVCard(contact.extra.role)}` : ''}
${contact.extra.birthday ? `BDAY:${contact.extra.birthday}` : ''}
-${contact.extra.nickname ? `NICKNAME:${contact.extra.nickname}` : ''}
+${contact.extra.nickname ? `NICKNAME:${getSafelyEscapedTextForVCard(contact.extra.nickname)}` : ''}
${contact.extra.photo_url ? `PHOTO;MEDIATYPE=${contact.extra.photo_mediatype}:${contact.extra.photo_url}` : ''}
${
contact.extra.fields?.filter((field) => field.type === 'phone').map((phone) =>
@@ -22,13 +30,11 @@ ${
}
${
contact.extra.addresses?.map((address) =>
- `ADR;TYPE=${address.label}:${(address.line_2 || '').replaceAll('\n', '\\n').replaceAll(',', '\\,')};${
- (address.line_1 || '').replaceAll('\n', '\\n').replaceAll(',', '\\,')
- };${(address.city || '').replaceAll('\n', '\\n').replaceAll(',', '\\,')};${
- (address.state || '').replaceAll('\n', '\\n').replaceAll(',', '\\,')
- };${(address.postal_code || '').replaceAll('\n', '\\n').replaceAll(',', '\\,')};${
- (address.country || '').replaceAll('\n', '\\n').replaceAll(',', '\\,')
- }`
+ `ADR;TYPE=${address.label}:${getSafelyEscapedTextForVCard(address.line_2 || '')};${
+ getSafelyEscapedTextForVCard(address.line_1 || '')
+ };${getSafelyEscapedTextForVCard(address.city || '')};${getSafelyEscapedTextForVCard(address.state || '')};${
+ getSafelyEscapedTextForVCard(address.postal_code || '')
+ };${getSafelyEscapedTextForVCard(address.country || '')}`
).join('\n') || ''
}
${
@@ -41,11 +47,7 @@ ${
contact.extra.fields?.filter((field) => field.type === 'other').map((other) => `x-${other.name}:${other.value}`)
.join('\n') || ''
}
-${
- contact.extra.notes
- ? `NOTE:${contact.extra.notes.replaceAll('\r', '').replaceAll('\n', '\\n').replaceAll(',', '\\,')}`
- : ''
- }
+${contact.extra.notes ? `NOTE:${getSafelyEscapedTextForVCard(contact.extra.notes.replaceAll('\r', ''))}` : ''}
${contact.extra.uid ? `UID:${contact.extra.uid}` : ''}
END:VCARD`
).join('\n');
@@ -191,7 +193,7 @@ export function parseVCardFromTextContents(text: string): Partial[] {
}
if (line.startsWith('NOTE:')) {
- const notes = (line.split('NOTE:')[1] || '').replaceAll('\\n', '\n').replaceAll('\\,', ',');
+ const notes = getSafelyUnescapedTextFromVCard(line.split('NOTE:')[1] || '');
partialContact.extra = {
...(partialContact.extra || {}),
@@ -204,35 +206,16 @@ export function parseVCardFromTextContents(text: string): Partial[] {
if (line.includes('ADR;')) {
const addressInfo = line.split('ADR;')[1] || '';
const addressParts = (addressInfo.split(':')[1] || '').split(';');
- const country = addressParts.slice(-1, addressParts.length).join(' ').replaceAll('\\n', '\n').replaceAll(
- '\\,',
- ',',
- );
- const postalCode = addressParts.slice(-2, addressParts.length - 1).join(' ').replaceAll('\\n', '\n').replaceAll(
- '\\,',
- ',',
- );
- const state = addressParts.slice(-3, addressParts.length - 2).join(' ').replaceAll('\\n', '\n').replaceAll(
- '\\,',
- ',',
- );
- const city = addressParts.slice(-4, addressParts.length - 3).join(' ').replaceAll('\\n', '\n').replaceAll(
- '\\,',
- ',',
- );
- const line1 = addressParts.slice(-5, addressParts.length - 4).join(' ').replaceAll('\\n', '\n').replaceAll(
- '\\,',
- ',',
- );
- const line2 = addressParts.slice(-6, addressParts.length - 5).join(' ').replaceAll('\\n', '\n').replaceAll(
- '\\,',
- ',',
- );
+ const country = getSafelyUnescapedTextFromVCard(addressParts.slice(-1, addressParts.length).join(' '));
+ const postalCode = getSafelyUnescapedTextFromVCard(addressParts.slice(-2, addressParts.length - 1).join(' '));
+ const state = getSafelyUnescapedTextFromVCard(addressParts.slice(-3, addressParts.length - 2).join(' '));
+ const city = getSafelyUnescapedTextFromVCard(addressParts.slice(-4, addressParts.length - 3).join(' '));
+ const line1 = getSafelyUnescapedTextFromVCard(addressParts.slice(-5, addressParts.length - 4).join(' '));
+ const line2 = getSafelyUnescapedTextFromVCard(addressParts.slice(-6, addressParts.length - 5).join(' '));
- const label = ((addressInfo.split(':')[0] || '').split('TYPE=')[1] || 'home').replaceAll(';', '').replaceAll(
- '\\n',
- '\n',
- ).replaceAll('\\,', ',');
+ const label = getSafelyUnescapedTextFromVCard(
+ ((addressInfo.split(':')[0] || '').split('TYPE=')[1] || 'home').replaceAll(';', ''),
+ );
if (!country && !postalCode && !state && !city && !line2 && !line1) {
continue;
diff --git a/routes/api/calendar/import.tsx b/routes/api/calendar/import.tsx
index 4ca9d17..35a1fd4 100644
--- a/routes/api/calendar/import.tsx
+++ b/routes/api/calendar/import.tsx
@@ -61,6 +61,13 @@ export const handler: Handlers = {
if (parsedExtra !== '{}') {
calendarEvent.extra = partialCalendarEvent.extra!;
+ if (
+ calendarEvent.extra.is_recurring && calendarEvent.extra.recurring_sequence === 0 &&
+ !calendarEvent.extra.recurring_id
+ ) {
+ calendarEvent.extra.recurring_id = calendarEvent.id;
+ }
+
await updateCalendarEvent(calendarEvent);
}
}
diff --git a/routes/calendar.tsx b/routes/calendar.tsx
index 1eb49bb..0984f21 100644
--- a/routes/calendar.tsx
+++ b/routes/calendar.tsx
@@ -38,7 +38,7 @@ export const handler: Handlers = {
dateRange.end.setDate(dateRange.end.getDate() + 31);
}
- const userCalendarEvents = await getCalendarEvents(context.state.user.id, visibleCalendarIds);
+ const userCalendarEvents = await getCalendarEvents(context.state.user.id, visibleCalendarIds, dateRange);
return await context.render({ userCalendars, userCalendarEvents, view, startDate });
},
diff --git a/routes/dav/calendars/[calendarId].tsx b/routes/dav/calendars/[calendarId].tsx
index 7248dd3..0fe14d0 100644
--- a/routes/dav/calendars/[calendarId].tsx
+++ b/routes/dav/calendars/[calendarId].tsx
@@ -3,7 +3,13 @@ import { Handler } from 'fresh/server.ts';
import { Calendar, FreshContextState } from '/lib/types.ts';
import { buildRFC822Date, convertObjectToDavXml, DAV_RESPONSE_HEADER, escapeHtml, escapeXml } from '/lib/utils/misc.ts';
import { formatCalendarEventsToVCalendar, parseVCalendarFromTextContents } from '/lib/utils/calendar.ts';
-import { getCalendar, getCalendarEvents } from '/lib/data/calendar.ts';
+import {
+ createCalendarEvent,
+ getCalendar,
+ getCalendarEvent,
+ getCalendarEvents,
+ updateCalendarEvent,
+} from '/lib/data/calendar.ts';
import { createSessionCookie } from '/lib/auth.ts';
interface Data {}
@@ -80,32 +86,37 @@ export const handler: Handler = async (request, context
const [partialCalendarEvent] = parseVCalendarFromTextContents(requestBody);
- if (partialCalendarEvent.title) {
- // TODO: Build this
- // const newCalendarEvent = await createCalendarEvent(
- // context.state.user.id,
- // partialCalendarEvent.title,
- // partialCalendarEvent.start_date,
- // partialCalendarEvent.end_date,
- // );
+ if (partialCalendarEvent.title && partialCalendarEvent.start_date && partialCalendarEvent.end_date) {
+ const newCalendarEvent = await createCalendarEvent(
+ context.state.user.id,
+ calendarId,
+ partialCalendarEvent.title,
+ new Date(partialCalendarEvent.start_date),
+ new Date(partialCalendarEvent.end_date),
+ partialCalendarEvent.is_all_day,
+ );
- // // Use the sent id for the UID
- // if (!partialCalendarEvent.extra?.uid) {
- // partialCalendarEvent.extra = {
- // ...(partialCalendarEvent.extra! || {}),
- // uid: calendarId,
- // };
- // }
+ const parsedExtra = JSON.stringify(partialCalendarEvent.extra || {});
- // newCalendarEvent.extra = partialCalendarEvent.extra!;
+ if (parsedExtra !== '{}') {
+ newCalendarEvent.extra = partialCalendarEvent.extra!;
- // await updateCalendarEvent(newCalendarEvent);
+ if (
+ newCalendarEvent.extra.is_recurring && newCalendarEvent.extra.recurring_sequence === 0 &&
+ !newCalendarEvent.extra.recurring_id
+ ) {
+ newCalendarEvent.extra.recurring_id = newCalendarEvent.id;
+ }
- // const calendarEvent = await getCalendarEvent(newCalendarEvent.id, context.state.user.id);
+ await updateCalendarEvent(newCalendarEvent);
+ }
- // return new Response('Created', { status: 201, headers: { 'etag': `"${calendarEvent.revision}"` } });
- return new Response('Not found', { status: 404 });
+ const calendarEvent = await getCalendarEvent(newCalendarEvent.id, context.state.user.id);
+
+ return new Response('Created', { status: 201, headers: { 'etag': `"${calendarEvent.revision}"` } });
}
+
+ return new Response('Not found', { status: 404 });
}
const calendarEvents = await getCalendarEvents(context.state.user.id, [calendar.id]);