diff --git a/src/calendarUtil.js b/src/calendarUtil.js index 68f9e03..db044cf 100644 --- a/src/calendarUtil.js +++ b/src/calendarUtil.js @@ -1,7 +1,7 @@ -import ICAL from './lib/ical.timezones.js'; -import fs from 'fs'; -import path from 'path'; -import axios from 'axios'; +import ICAL from './lib/ical.timezones.js' +import fs from 'fs' +import path from 'path' +import axios from 'axios' export const MERGED_CALENDARS_DIR = path.join(process.cwd(), 'calendar'); @@ -26,81 +26,6 @@ export async function fetchCalendarData(calendar) { } } -// Helper function to check if TZID exists in the raw property string -function hasTZID(rawProperty) { - return rawProperty.includes('TZID='); -} - -// Function to process DTSTART/DTEND -function processDateTimeProperty(event, propertyName, newEvent) { - const rawProperty = event.getFirstProperty(propertyName)?.toICALString(); - if (!rawProperty) { - console.log(`No raw property found for ${propertyName}`); - return; - } - - console.log(`Raw property: ${rawProperty}`); // Log the raw property - - // Check if it's a date-based event (VALUE=DATE) - if (rawProperty.includes('VALUE=DATE')) { - console.log(`Date-based event detected for ${propertyName}: ${rawProperty}`); - - // Split to get the date part (should be in the format YYYYMMDD) - const dateOnly = rawProperty.split(':')[1]; // e.g., "20231225" - console.log(`Extracted date string: ${dateOnly}`); - - if (!dateOnly) { - console.error(`Error: Could not extract date from ${rawProperty}`); - return; - } - - // Ensure the date string is valid (no dashes, just YYYYMMDD) - const year = dateOnly.slice(0, 4); - const month = dateOnly.slice(4, 6); - const day = dateOnly.slice(6, 8); - - console.log(`Parsed date: ${year}-${month}-${day}`); - - // Check if the date is valid - if (!year || !month || !day || isNaN(new Date(`${year}-${month}-${day}`))) { - console.error(`Invalid date parsed from raw property: ${rawProperty}`); - return; - } - - const formattedDate = dateOnly; // Use the date string as is (YYYYMMDD format) - console.log(`Formatted date: ${formattedDate}`); - - // Log before adding the property to ensure it's correct - console.log(`Adding date-based property with value: ${propertyName};VALUE=DATE:${formattedDate}`); - - // Correct property name usage (DTSTART not dtstart) - const property = new ICAL.Property(propertyName.toUpperCase(), newEvent); // Use uppercase "DTSTART" - property.setValue(`VALUE=DATE:${formattedDate}`); - - // Log the property object before adding it to ensure everything is correct - console.log(`Property to add:`, property); - - newEvent.addProperty(property); - } else { - console.log(`Time-based event detected for ${propertyName}: ${rawProperty}`); - - // Time-based event processing (existing logic) - const dateTime = event.getFirstPropertyValue(propertyName); - const dateTimeString = dateTime.toString(); - - const property = new ICAL.Property(propertyName, newEvent); - property.setValue(dateTimeString); - - if (hasTZID(rawProperty)) { - // If raw property includes TZID, add it - property.setParameter('TZID', dateTime.zone.tzid); - } - - newEvent.addProperty(property); - } -} - - // Create a top-level VCALENDAR component export function createCalendarComponent(name) { const calendarComponent = new ICAL.Component(['vcalendar', [], []]); @@ -112,94 +37,71 @@ export function createCalendarComponent(name) { } // Add events to the calendar component -export function addEventsToCalendar(calendarComponent, results, overrideFlag = false) { +export function addEventsToCalendar(newCalendar, calendars, overrideFlag = false) { let defaultTimeZone = null; // To store the first found X-WR-TIMEZONE - results.forEach((result) => { + calendars.forEach((calendarRaw) => { try { - const parsed = ICAL.parse(result.data); - const component = new ICAL.Component(parsed); + const calendar = new ICAL.Component(ICAL.parse(calendarRaw.data)); // Extract METHOD from the parsed data (if available) - const method = component.getFirstPropertyValue('method'); + const method = calendar.getFirstPropertyValue('method'); if (method) { console.log(`Extracted METHOD: ${method}`); // Only add the METHOD property once - if (!calendarComponent.getFirstPropertyValue('method')) { - calendarComponent.updatePropertyWithValue('method', method.toUpperCase()); + if (!newCalendar.getFirstPropertyValue('method')) { + newCalendar.updatePropertyWithValue('method', method.toUpperCase()); } } // Extract X-WR-TIMEZONE if available - const wrTimeZone = component.getFirstPropertyValue('x-wr-timezone'); + const wrTimeZone = calendar.getFirstPropertyValue('x-wr-timezone'); if (wrTimeZone) { console.log(`Extracted X-WR-TIMEZONE: ${wrTimeZone}`); // Set it as the default if not already set if (!defaultTimeZone) { defaultTimeZone = wrTimeZone; - if (!calendarComponent.getFirstPropertyValue('x-wr-timezone')) { - calendarComponent.updatePropertyWithValue('x-wr-timezone', defaultTimeZone); + if (!newCalendar.getFirstPropertyValue('x-wr-timezone')) { + newCalendar.updatePropertyWithValue('x-wr-timezone', defaultTimeZone); } } } // Extract and add VTIMEZONE components - const timezones = component.getAllSubcomponents('vtimezone'); + const timezones = calendar.getAllSubcomponents('vtimezone'); timezones.forEach((timezone) => { const tzid = timezone.getFirstPropertyValue('tzid'); - if (!calendarComponent.getFirstSubcomponent((comp) => comp.name === 'vtimezone' && comp.getFirstPropertyValue('tzid') === tzid)) { - calendarComponent.addSubcomponent(timezone); + if (!newCalendar.getFirstSubcomponent((comp) => comp.name === 'vtimezone' && comp.getFirstPropertyValue('tzid') === tzid)) { + newCalendar.addSubcomponent(timezone); } }); + // Process VEVENT components - component.getAllSubcomponents('vevent').forEach((event) => { - const vevent = new ICAL.Event(event); - const newEvent = new ICAL.Component('vevent'); + calendar.getAllSubcomponents('vevent').forEach((vevent) => { + const event = new ICAL.Event(vevent); + const newEvent = new ICAL.Event(); - // 1. Add DTSTART - processDateTimeProperty(event, 'dtstart', newEvent); - - // 2. Add DTEND - processDateTimeProperty(event, 'dtend', newEvent); + newEvent.uid = event.uid; + newEvent.startDate = event.startDate + newEvent.endDate = event.endDate - // 3. Copy DTSTAMP - const dtstamp = event.getFirstPropertyValue('dtstamp'); - if (dtstamp) newEvent.updatePropertyWithValue('dtstamp', dtstamp); + const dtstamp = vevent.getFirstPropertyValue('dtstamp'); + if (dtstamp) newEvent.component.updatePropertyWithValue('dtstamp', dtstamp); - // 4. Copy UID - newEvent.updatePropertyWithValue('uid', vevent.uid); - - // 5. Add LOCATION (conditionally included) - if (!overrideFlag && vevent.location) { - newEvent.updatePropertyWithValue('location', vevent.location); - } else if (overrideFlag && vevent.location) { - // Modify SUMMARY if override is set - const modifiedSummary = `${vevent.summary.trim()} (Location omitted)`; - newEvent.updatePropertyWithValue('summary', modifiedSummary); + if (overrideFlag) { + newEvent.summary = 'Busy' } else { - newEvent.updatePropertyWithValue('summary', vevent.summary.trim()); + newEvent.summary = event.summary; + if (event.location) newEvent.location = event.location; } - // 6. Copy Recurrence Rules (RRULE) and Recurrence ID - const rrule = event.getFirstPropertyValue('rrule'); - if (rrule) newEvent.updatePropertyWithValue('rrule', rrule); + const rrule = vevent.getFirstPropertyValue('rrule'); + if (rrule) newEvent.component.updatePropertyWithValue('rrule', rrule); - const recurrenceId = event.getFirstPropertyValue('recurrence-id'); - if (recurrenceId) newEvent.updatePropertyWithValue('recurrence-id', recurrenceId); - - // 7. Copy SUMMARY - newEvent.updatePropertyWithValue('summary', vevent.summary.trim()); - - // 8. Add SEQUENCE (if available or default to 0) - const sequence = event.getFirstPropertyValue('sequence') || 0; - newEvent.updatePropertyWithValue('sequence', sequence); - // Add the VEVENT to the calendar - calendarComponent.addSubcomponent(newEvent); + newCalendar.addSubcomponent(newEvent.component); }); - - console.log(`Processed VEVENT components for calendar: ${result.name}`); - } catch (error) { - console.error('Error processing calendar data:', error.message); + } catch (error) { + console.error(`Error processing calendar:`, calendarRaw, error); } }); } diff --git a/src/routes.js b/src/routes.js index 5293e25..a0f4546 100644 --- a/src/routes.js +++ b/src/routes.js @@ -47,9 +47,9 @@ async function refreshCalendarData(calendarName) { // Read the JSON file to get the source URL and other details const { calendars } = JSON.parse(fs.readFileSync(jsonFilePath, 'utf-8')); - const results = await Promise.all(calendars.map(fetchCalendarData)); + const calendarResults = await Promise.all(calendars.map(fetchCalendarData)); const calendarComponent = createCalendarComponent(calendarName); - addEventsToCalendar(calendarComponent, results); + addEventsToCalendar(calendarComponent, calendarResults); saveCalendarFile(`${calendarName}.ics`, calendarComponent.toString()); console.log('Calendar data refreshed and saved.'); diff --git a/src/server.js b/src/server.js index 851fcc2..b86cca0 100644 --- a/src/server.js +++ b/src/server.js @@ -2,13 +2,13 @@ import express from 'express'; import path from 'path'; import routes from './routes.js'; +console.log(`Starting server in ${process.cwd()}`); + const app = express(); app.use(express.json()); - // Serve static files from the 'public' directory app.use(express.static(path.join(process.cwd(), 'public'))); app.use('/', routes); - -export default app; +export default app; \ No newline at end of file diff --git a/test/calendar.test.js b/test/calendar.test.js index edea6d5..ce4b017 100644 --- a/test/calendar.test.js +++ b/test/calendar.test.js @@ -1,8 +1,6 @@ import request from 'supertest'; -import express from 'express'; import fs from 'fs'; import path from 'path'; -import { jest } from '@jest/globals'; import { fileURLToPath } from 'url'; // ESM equivalent of __dirname @@ -15,20 +13,18 @@ const EXPECTED_OUTPUTS_DIR = path.join(__dirname, 'expected_outputs'); let server; process.chdir(__dirname) -console.log(process.cwd()); -import app from '../src/server.js'; - -const normalizeLineEndings = (str) => str.replace(/\r\n/g, '\r\n').trimEnd(); // Normalize to CRLF +const app = await import('../src/server'); describe('Calendar Merging API', () => { beforeAll(async () => { - // Start the server - server = app.listen(0); + // Clean up the merged calendars directory before tests fs.rmdirSync(CALENDARS_DIR, { recursive: true }); if (fs.existsSync(CALENDARS_DIR)) { fs.rmdirSync(CALENDARS_DIR, { recursive: true }); } + // Start the server + server = app.default.listen(0); }); afterAll(async () => { @@ -71,12 +67,8 @@ describe('Calendar Merging API', () => { const expectedOutput = fs.readFileSync(input, 'utf8'); const actualOutput = fs.readFileSync(filePath, 'utf8'); - // Normalize line endings - const normalizedActual = normalizeLineEndings(actualOutput); - const normalizedExpected = normalizeLineEndings(expectedOutput); - //compare - expect(normalizedActual).toBe(normalizedExpected); + expect(actualOutput).toBe(expectedOutput); }); test('Preserve google calendar', async () => { @@ -103,12 +95,8 @@ describe('Calendar Merging API', () => { const expectedOutput = fs.readFileSync(input, 'utf8'); const actualOutput = fs.readFileSync(filePath, 'utf8'); - // Normalize line endings - const normalizedActual = normalizeLineEndings(actualOutput); - const normalizedExpected = normalizeLineEndings(expectedOutput); - //compare - expect(normalizedActual).toBe(normalizedExpected); + expect(actualOutput).toBe(expectedOutput); }); diff --git a/test/test_calendars/US_Holidays.ics b/test/test_calendars/US_Holidays.ics index 842dd79..8d07404 100644 --- a/test/test_calendars/US_Holidays.ics +++ b/test/test_calendars/US_Holidays.ics @@ -1,11 +1,13 @@ BEGIN:VCALENDAR NAME:US Holidays +PRODID:-//CalMerge//Calendar Merger 1.0//EN VERSION:2.0 +CALSCALE:GREGORIAN BEGIN:VEVENT UID:20231225T000000-001@example.com -DTSTAMP:20231225T000000Z DTSTART;VALUE=DATE:20231225 DTEND;VALUE=DATE:20231226 +DTSTAMP:20231225T000000Z SUMMARY:Christmas Day END:VEVENT END:VCALENDAR \ No newline at end of file diff --git a/test/test_calendars/google-calendar-minimal.ics b/test/test_calendars/google-calendar-minimal.ics index 9c34571..5eba20d 100644 --- a/test/test_calendars/google-calendar-minimal.ics +++ b/test/test_calendars/google-calendar-minimal.ics @@ -6,11 +6,10 @@ CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-TIMEZONE:Africa/Nairobi BEGIN:VEVENT +UID:6tbrvsitniuu72li7kk15gou2b@google.com DTSTART:20241003T190000Z DTEND:20241003T200000Z DTSTAMP:20241119T115316Z -UID:6tbrvsitniuu72li7kk15gou2b@google.com SUMMARY:progodessey -SEQUENCE:0 END:VEVENT -END:VCALENDAR +END:VCALENDAR \ No newline at end of file diff --git a/test/test_calendars/google-calendar.ics b/test/test_calendars/google-calendar.ics index ca0a7b7..c008591 100644 --- a/test/test_calendars/google-calendar.ics +++ b/test/test_calendars/google-calendar.ics @@ -30,10 +30,10 @@ SUMMARY:progodessey TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT +UID:7l7n9nltrudluv65gfgll2q930@google.com DTSTART:20241009T173000Z DTEND:20241009T183000Z DTSTAMP:20241119T115316Z -UID:7l7n9nltrudluv65gfgll2q930@google.com CREATED:20241010T123337Z LAST-MODIFIED:20241010T123337Z SEQUENCE:0 @@ -41,4 +41,4 @@ STATUS:CONFIRMED SUMMARY:do TRANSP:OPAQUE END:VEVENT -END:VCALENDAR +END:VCALENDAR \ No newline at end of file diff --git a/test/test_calendars/nextcloud-minimal.ics b/test/test_calendars/nextcloud-minimal.ics index 23684ab..f8bcd87 100644 --- a/test/test_calendars/nextcloud-minimal.ics +++ b/test/test_calendars/nextcloud-minimal.ics @@ -21,13 +21,12 @@ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT +UID:5f4ad965-16a8-48eb-8233-78bf93a8b35e DTSTART;TZID=Europe/Berlin:20241120T211500 DTEND;TZID=Europe/Berlin:20241120T215000 DTSTAMP:20241113T212909Z -UID:5f4ad965-16a8-48eb-8233-78bf93a8b35e +SUMMARY:JR Weekly Check-In LOCATION:FaceTime RRULE:FREQ=WEEKLY;BYDAY=WE -SUMMARY:JR Weekly Check-In -SEQUENCE:0 END:VEVENT -END:VCALENDAR +END:VCALENDAR \ No newline at end of file