import { Injectable, Logger } from '@nestjs/common'; import { DAVClient } from 'tsdav'; import * as ical from 'node-ical'; import { CalendarEvent, CalendarProvider } from '../calendar.service'; /** * CalDAV calendar provider — uses tsdav for protocol handling. * * Connects via Basic auth (username + decrypted password), fetches calendars, * then fetchCalendarObjects with time-range filter (A2). * Parses returned iCal data with node-ical and maps to CalendarEvent. */ @Injectable() export class CalDAVProvider implements CalendarProvider { private readonly logger = new Logger(CalDAVProvider.name); /** * Fetches events from a CalDAV server within the specified date range. * Uses tsdav's time-range filter to only fetch relevant events (A2). */ async fetchEvents( source: { url: string; username?: string; password?: string; id: string; color?: string | null }, from: Date, to: Date, ): Promise { const events: CalendarEvent[] = []; try { const client = await this.createClient(source); const calendars = await client.fetchCalendars(); for (const calendar of calendars) { const objects = await client.fetchCalendarObjects({ calendar, timeRange: { start: from.toISOString(), end: to.toISOString(), }, expand: true, }); for (const obj of objects) { if (!obj.data) continue; try { const parsed = ical.sync.parseICS(obj.data); for (const key of Object.keys(parsed)) { const component = parsed[key]; if (component?.type !== 'VEVENT') continue; const vevent = component as ical.VEvent; const start = new Date(vevent.start); const end = vevent.end ? new Date(vevent.end) : new Date(start.getTime() + 3600000); events.push({ id: `${source.id}-${vevent.uid || key}`, sourceId: source.id, title: extractString(vevent.summary), start, end, allDay: vevent.datetype === 'date', location: vevent.location ? extractString(vevent.location) : undefined, description: vevent.description ? extractString(vevent.description) : undefined, color: source.color ?? undefined, }); } } catch (parseErr) { this.logger.warn( `Failed to parse CalDAV object: ${(parseErr as Error).message}`, ); } } } } catch (error) { this.logger.error( `Failed to fetch CalDAV events from ${source.url}: ${(error as Error).message}`, ); throw error; } return events; } /** * Tests connection by attempting login and listing calendars. */ async testConnection( source: { url: string; username?: string; password?: string; id: string }, ): Promise { try { const client = await this.createClient(source); const calendars = await client.fetchCalendars(); return calendars.length > 0; } catch { return false; } } /** * Creates a tsdav DAVClient with Basic auth and logs in. */ private async createClient( source: { url: string; username?: string; password?: string }, ): Promise { const client = new DAVClient({ serverUrl: source.url, credentials: { username: source.username || '', password: source.password || '', }, authMethod: 'Basic', defaultAccountType: 'caldav', }); await client.login(); return client; } } /** * Extracts a plain string from a node-ical ParameterValue. */ function extractString( value: ical.ParameterValue | string | undefined, ): string { if (!value) return ''; if (typeof value === 'string') return value; if (typeof value === 'object' && 'val' in value) return String(value.val); return String(value); }