Importing iCal feeds
Importing iCal feeds
Fetch the subscription feed, parse it with ical.js, and upload
one resource per UID. Install ical.js alongside tsdav. The parser and serializer preserve date
values, escaped text, recurrence rules, and recurrence exceptions. Include the feed’s timezone
components in each resource.
import ICAL from 'ical.js';import { createCalendarObject, DAVCalendar } from 'tsdav';
export async function importExternalFeed( feedUrl: string, targetCalendar: DAVCalendar, authHeader: string,) { const response = await fetch(feedUrl); if (!response.ok) throw new Error(`Feed download failed: ${response.status}`); const source = new ICAL.Component(ICAL.parse(await response.text())); const byUid = new Map<string, ICAL.Component[]>(); for (const event of source.getAllSubcomponents('vevent')) { const uid = event.getFirstPropertyValue('uid'); if (typeof uid !== 'string' || !uid) throw new Error('Feed event has no UID'); const group = byUid.get(uid) ?? []; group.push(event); byUid.set(uid, group); }
let imported = 0; for (const [uid, events] of byUid) { const resource = new ICAL.Component(['vcalendar', [], []]); resource.addPropertyWithValue('version', '2.0'); resource.addPropertyWithValue('prodid', '-//tsdav//Feed importer//EN'); for (const timezone of source.getAllSubcomponents('vtimezone')) { resource.addSubcomponent(new ICAL.Component(JSON.parse(JSON.stringify(timezone.toJSON())))); } for (const event of events) { resource.addSubcomponent(new ICAL.Component(JSON.parse(JSON.stringify(event.toJSON())))); } const result = await createCalendarObject({ calendar: targetCalendar, filename: `feed-${encodeURIComponent(uid)}.ics`, iCalString: resource.toString(), headers: { authorization: authHeader }, }); if (!result.ok) { throw new Error(`Import failed for ${uid}: ${result.status}`); } imported += 1; } return imported;}This is an import, not a feed synchronization policy. createCalendarObject uses If-None-Match: *;
a duplicate resource returns 412 and the example throws. For repeated polling, keep a UID-to-resource
mapping and fetched etags, use updateCalendarObject with the current etag for changes, and decide
explicitly whether a removed feed event should be deleted. A later failure can leave earlier uploads
completed, so persist successful imports individually in that workflow.