diff --git a/lib/utils/calendar.ts b/lib/utils/calendar.ts index fd20b1a..c4888b8 100644 --- a/lib/utils/calendar.ts +++ b/lib/utils/calendar.ts @@ -23,6 +23,29 @@ export const CALENDAR_COLOR_OPTIONS = [ 'bg-rose-700', ] as const; +const CALENDAR_COLOR_OPTIONS_HEX = [ + '#B51E1F', + '#450A0A', + '#BF4310', + '#431407', + '#B0550F', + '#834F13', + '#4D7D16', + '#1A2E05', + '#148041', + '#066048', + '#107873', + '#0E7490', + '#075985', + '#1E3A89', + '#423BCA', + '#6A2BD9', + '#6923A9', + '#9D21B1', + '#9C174D', + '#BC133D', +] as const; + // NOTE: This variable isn't really used, _but_ it allows for tailwind to include the classes without having to move this into the tailwind.config.ts file export const CALENDAR_BORDER_COLOR_OPTIONS = [ 'border-red-700', @@ -47,6 +70,12 @@ export const CALENDAR_BORDER_COLOR_OPTIONS = [ 'border-rose-700', ] as const; +export function getColorAsHex(calendarColor: string) { + const colorIndex = CALENDAR_COLOR_OPTIONS.findIndex((color) => color === calendarColor); + + return CALENDAR_COLOR_OPTIONS_HEX[colorIndex]; +} + function getVCalendarAttendeeStatus(status: CalendarEventAttendee['status']) { if (status === 'accepted' || status === 'rejected') { return status.toUpperCase(); @@ -77,7 +106,6 @@ function getSafelyUnescapedTextFromVCalendar(text: string) { return text.replaceAll('\\n', '\n').replaceAll('\\,', ','); } -// TODO: Build this export function formatCalendarEventsToVCalendar( calendarEvents: CalendarEvent[], calendars: Pick[], @@ -448,8 +476,6 @@ export function parseVCalendarFromTextContents(text: string): Partial = async (request, context 'xmlns:oc': 'http://owncloud.org/ns', 'xmlns:nc': 'http://nextcloud.org/ns', 'xmlns:cs': 'http://calendarserver.org/ns/', + 'xmlns:ic': 'http://apple.com/ns/ical/', }, }; const calendars = await getCalendars(context.state.user.id); + const requestBody = (await request.clone().text()).toLowerCase(); + + const includePrivileges = requestBody.includes('current-user-privilege-set'); + const includeColor = requestBody.includes('calendar-color'); + + if (includePrivileges) { + responseBody['d:multistatus']['d:response'][0]['d:propstat'][0]['d:prop']['d:current-user-privilege-set'] = { + 'd:privilege': [ + { 'd:write-properties': {} }, + { 'd:write': {} }, + { 'd:write-content': {} }, + { 'd:unlock': {} }, + { 'd:bind': {} }, + { 'd:unbind': {} }, + { 'd:write-acl': {} }, + { 'd:read': {} }, + { 'd:read-acl': {} }, + { 'd:read-current-user-privilege-set': {} }, + ], + }; + } + for (const calendar of calendars) { - responseBody['d:multistatus']['d:response'].push({ + const parsedCalendar: DavResponse = { 'd:href': `/dav/calendars/${calendar.id}`, 'd:propstat': [{ 'd:prop': { @@ -114,7 +154,30 @@ export const handler: Handler = async (request, context 'd:prop': { 'd:principal-URL': {} }, 'd:status': 'HTTP/1.1 404 Not Found', }], - }); + }; + + if (includePrivileges) { + parsedCalendar['d:propstat'][0]['d:prop']['d:current-user-privilege-set'] = { + 'd:privilege': [ + { 'd:write-properties': {} }, + { 'd:write': {} }, + { 'd:write-content': {} }, + { 'd:unlock': {} }, + { 'd:bind': {} }, + { 'd:unbind': {} }, + { 'd:write-acl': {} }, + { 'd:read': {} }, + { 'd:read-acl': {} }, + { 'd:read-current-user-privilege-set': {} }, + ], + }; + } + + if (includeColor) { + parsedCalendar['d:propstat'][0]['d:prop']['ic:calendar-color'] = getColorAsHex(calendar.color); + } + + responseBody['d:multistatus']['d:response'].push(parsedCalendar); } const response = new Response(convertObjectToDavXml(responseBody, true), { diff --git a/routes/dav/calendars/[calendarId].tsx b/routes/dav/calendars/[calendarId].tsx index 0fe14d0..3d8185f 100644 --- a/routes/dav/calendars/[calendarId].tsx +++ b/routes/dav/calendars/[calendarId].tsx @@ -1,9 +1,11 @@ import { Handler } from 'fresh/server.ts'; +import { parse } from 'xml/mod.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 { + createCalendar, createCalendarEvent, getCalendar, getCalendarEvent, @@ -76,8 +78,27 @@ export const handler: Handler = async (request, context console.error(error); } + if (!calendar && request.method === 'MKCALENDAR') { + const requestBody = await request.clone().text(); + + try { + const parsedDocument = parse(requestBody); + + const makeCalendarRequest = (parsedDocument['c:mkcalendar'] || parsedDocument['cal:mkcalendar']) as + | Record + | undefined; + + const name: string = makeCalendarRequest!['d:set']['d:prop']['d:displayname']!; + + const calendar = await createCalendar(context.state.user.id, name); + + return new Response('Created', { status: 201, headers: { 'etag': `"${calendar.revision}"` } }); + } catch (error) { + console.error(`Failed to parse XML`, error); + } + } + if (!calendar) { - // TODO: Support MKCALENDAR return new Response('Not found', { status: 404 }); } diff --git a/routes/dav/calendars/[calendarId]/[calendarEventId].ics.tsx b/routes/dav/calendars/[calendarId]/[calendarEventId].ics.tsx index f6f0bc6..1026c4d 100644 --- a/routes/dav/calendars/[calendarId]/[calendarEventId].ics.tsx +++ b/routes/dav/calendars/[calendarId]/[calendarEventId].ics.tsx @@ -2,8 +2,14 @@ import { Handler } from 'fresh/server.ts'; import { Calendar, CalendarEvent, FreshContextState } from '/lib/types.ts'; import { buildRFC822Date, convertObjectToDavXml, DAV_RESPONSE_HEADER, escapeHtml, escapeXml } from '/lib/utils/misc.ts'; -import { formatCalendarEventsToVCalendar } from '/lib/utils/calendar.ts'; -import { getCalendar, getCalendarEvent, updateCalendarEvent } from '/lib/data/calendar.ts'; +import { formatCalendarEventsToVCalendar, parseVCalendarFromTextContents } from '/lib/utils/calendar.ts'; +import { + createCalendarEvent, + deleteCalendarEvent, + getCalendar, + getCalendarEvent, + updateCalendarEvent, +} from '/lib/data/calendar.ts'; import { createSessionCookie } from '/lib/auth.ts'; interface Data {} @@ -83,71 +89,88 @@ export const handler: Handler = async (request, context if (!calendarEvent) { if (request.method === 'PUT') { - // TODO: Build this - // const newCalendarEvent = await createCalendarEvent( - // context.state.user.id, - // partialCalendarEvent.title, - // partialCalendarEvent.start_date, - // partialCalendarEvent.end_date, - // ); + const requestBody = await request.clone().text(); - // // Use the sent id for the UID - // if (!partialCalendarEvent.extra?.uid) { - // partialCalendarEvent.extra = { - // ...(partialCalendarEvent.extra! || {}), - // uid: calendarId, - // }; - // } + const [partialCalendarEvent] = parseVCalendarFromTextContents(requestBody); - // newCalendarEvent.extra = partialCalendarEvent.extra!; + 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, + ); - // await updateCalendarEvent(newCalendarEvent); + // Use the sent id for the UID + if (!partialCalendarEvent.extra?.uid) { + partialCalendarEvent.extra = { + ...(partialCalendarEvent.extra! || {}), + uid: calendarEventId, + }; + } - // const calendarEvent = await getCalendarEvent(newCalendarEvent.id, context.state.user.id); + const parsedExtra = JSON.stringify(partialCalendarEvent.extra || {}); - // return new Response('Created', { status: 201, headers: { 'etag': `"${calendarEvent.revision}"` } }); + if (parsedExtra !== '{}') { + newCalendarEvent.extra = partialCalendarEvent.extra!; + + if ( + newCalendarEvent.extra.is_recurring && newCalendarEvent.extra.recurring_sequence === 0 && + !newCalendarEvent.extra.recurring_id + ) { + newCalendarEvent.extra.recurring_id = newCalendarEvent.id; + } + + await updateCalendarEvent(newCalendarEvent); + } + + 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 }); } - // TODO: Build this - // if (request.method === 'DELETE') { - // const clientRevision = request.headers.get('if-match') || request.headers.get('etag'); + if (request.method === 'DELETE') { + const clientRevision = request.headers.get('if-match') || request.headers.get('etag'); - // // Don't update outdated data - // if (clientRevision && clientRevision !== `"${calendarEvent.revision}"`) { - // return new Response(null, { status: 204, headers: { 'etag': `"${calendarEvent.revision}"` } }); - // } + // Don't update outdated data + if (clientRevision && clientRevision !== `"${calendarEvent.revision}"`) { + return new Response(null, { status: 204, headers: { 'etag': `"${calendarEvent.revision}"` } }); + } - // await deleteContact(contactId, context.state.user.id); + await deleteCalendarEvent(calendarEventId, calendarId, context.state.user.id); - // return new Response(null, { status: 202 }); - // } + return new Response(null, { status: 202 }); + } - // if (request.method === 'PUT') { - // const clientRevision = request.headers.get('if-match') || request.headers.get('etag'); + if (request.method === 'PUT') { + const clientRevision = request.headers.get('if-match') || request.headers.get('etag'); - // // Don't update outdated data - // if (clientRevision && clientRevision !== `"${contact.revision}"`) { - // return new Response(null, { status: 204, headers: { 'etag': `"${contact.revision}"` } }); - // } + // Don't update outdated data + if (clientRevision && clientRevision !== `"${calendarEvent.revision}"`) { + return new Response(null, { status: 204, headers: { 'etag': `"${calendarEvent.revision}"` } }); + } - // const requestBody = await request.clone().text(); + const requestBody = await request.clone().text(); - // const [partialContact] = parseVCardFromTextContents(requestBody); + const [partialCalendarEvent] = parseVCalendarFromTextContents(requestBody); - // contact = { - // ...contact, - // ...partialContact, - // }; + calendarEvent = { + ...calendarEvent, + ...partialCalendarEvent, + }; - // await updateContact(contact); + await updateCalendarEvent(calendarEvent); - // contact = await getContact(contactId, context.state.user.id); + calendarEvent = await getCalendarEvent(calendarEventId, context.state.user.id); - // return new Response(null, { status: 204, headers: { 'etag': `"${contact.revision}"` } }); - // } + return new Response(null, { status: 204, headers: { 'etag': `"${calendarEvent.revision}"` } }); + } if (request.method === 'GET') { // Set a UID if there isn't one