diff --git a/server/routes/calendar.js b/server/routes/calendar.js
index 9d212b8..6aa245f 100644
--- a/server/routes/calendar.js
+++ b/server/routes/calendar.js
@@ -172,7 +172,7 @@ router.get('/events/:year/:month', async (req, res) => {
res.json(events);
}
} catch (error) {
- console.error('Error retrieving calendar events:', error);
+ console.error('Error retrieving calendar events:', error.message);
res.status(500).json({ error: 'Failed to retrieve calendar events' });
}
});
@@ -208,7 +208,7 @@ router.post('/events', async (req, res) => {
const event = await CalendarService.createEvent(userConfig, date, text);
res.status(201).json(event);
} catch (error) {
- console.error('Error creating calendar event:', error);
+ console.error('Error creating calendar event:', error.message);
res.status(500).json({ error: 'Failed to create calendar event' });
}
});
diff --git a/server/services/CalendarService.js b/server/services/CalendarService.js
index d3954dc..b96a0e8 100644
--- a/server/services/CalendarService.js
+++ b/server/services/CalendarService.js
@@ -6,11 +6,6 @@ const http = require('http');
const { URL } = require('url');
class CalendarService {
- constructor() {
- this.events = new Map(); // In-memory storage for demo purposes
- // TODO: Replace with actual CalDAV operations when library is available
- }
-
/**
* Create a calendar event
* @param {Object} userConfig - Nextcloud user configuration
@@ -20,33 +15,40 @@ class CalendarService {
*/
async createEvent(userConfig, date, text) {
try {
- // TODO: Implement actual CalDAV event creation
- // For now, simulate the operation with local storage
-
+ // Validate input parameters
+ if (!userConfig) {
+ throw new Error('User configuration is required');
+ }
+ if (!userConfig.name) {
+ throw new Error('User name is required');
+ }
+ if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
+ throw new Error('Valid date in YYYY-MM-DD format is required');
+ }
+ if (!text || text.trim().length === 0) {
+ throw new Error('Event text is required');
+ }
+
+ // Generate event ID and structure
const eventId = crypto.randomUUID();
const event = {
id: eventId,
date: date,
- text: text,
+ text: text.trim(),
user: userConfig.name,
- summary: text,
+ summary: text.trim(),
dtstart: this._formatDateForCalDAV(date),
dtend: this._formatDateForCalDAV(date),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
- this.events.set(eventId, event);
+ // Create event via CalDAV
+ const caldavEvent = await this._createCalDAVEvent(userConfig, event);
- console.log(`Calendar event created for ${userConfig.name}: ${text} on ${date}`);
-
- // TODO: When CalDAV library is available, replace with:
- // const caldavEvent = await this._createCalDAVEvent(userConfig, event);
-
- return event;
+ return caldavEvent;
} catch (error) {
- console.error('Error creating calendar event:', error);
- throw new Error('Failed to create calendar event');
+ throw new Error(`Failed to create calendar event: ${error.message}`);
}
}
@@ -59,26 +61,23 @@ class CalendarService {
*/
async getEventsForMonth(userConfig, year, month) {
try {
- // TODO: Implement actual CalDAV event retrieval
- // For now, filter local events
-
- const events = Array.from(this.events.values());
- const filteredEvents = events.filter(event => {
- if (event.user !== userConfig.name) return false;
- const eventDate = new Date(event.date);
- return eventDate.getFullYear() === parseInt(year) &&
- (eventDate.getMonth() + 1) === parseInt(month);
- });
+ // Validate input parameters
+ if (!userConfig || !userConfig.name) {
+ throw new Error('Valid user configuration is required');
+ }
+ if (!year || !/^\d{4}$/.test(year)) {
+ throw new Error('Valid year in YYYY format is required');
+ }
+ if (!month || !/^(0[1-9]|1[0-2])$/.test(month)) {
+ throw new Error('Valid month in MM format is required');
+ }
- console.log(`Retrieved ${filteredEvents.length} events for ${userConfig.name} in ${year}-${month}`);
+ // Fetch events from CalDAV
+ const events = await this._fetchCalDAVEvents(userConfig, year, month);
- // TODO: When CalDAV library is available, replace with:
- // const caldavEvents = await this._fetchCalDAVEvents(userConfig, year, month);
-
- return filteredEvents;
+ return events;
} catch (error) {
- console.error('Error retrieving calendar events:', error);
- throw new Error('Failed to retrieve calendar events');
+ throw new Error(`Failed to retrieve calendar events: ${error.message}`);
}
}
@@ -119,32 +118,23 @@ class CalendarService {
*/
async updateEvent(userConfig, eventId, date, text) {
try {
- const event = this.events.get(eventId);
- if (!event) {
- throw new Error('Event not found');
- }
+ // Build updated event object
+ const event = {
+ id: eventId,
+ date: date,
+ text: text,
+ user: userConfig.name,
+ summary: text,
+ dtstart: this._formatDateForCalDAV(date),
+ dtend: this._formatDateForCalDAV(date),
+ updatedAt: new Date().toISOString()
+ };
- if (event.user !== userConfig.name) {
- throw new Error('Permission denied: You can only edit your own events');
- }
-
- event.date = date;
- event.text = text;
- event.summary = text;
- event.dtstart = this._formatDateForCalDAV(date);
- event.dtend = this._formatDateForCalDAV(date);
- event.updatedAt = new Date().toISOString();
-
- this.events.set(eventId, event);
+ // Update event via CalDAV
+ const updatedEvent = await this._updateCalDAVEvent(userConfig, event);
- console.log(`Calendar event updated for ${userConfig.name}: ${eventId}`);
-
- // TODO: When CalDAV library is available, replace with:
- // await this._updateCalDAVEvent(userConfig, event);
-
- return event;
+ return updatedEvent;
} catch (error) {
- console.error('Error updating calendar event:', error);
throw error;
}
}
@@ -157,25 +147,11 @@ class CalendarService {
*/
async deleteEvent(userConfig, eventId) {
try {
- const event = this.events.get(eventId);
- if (!event) {
- throw new Error('Event not found');
- }
-
- if (event.user !== userConfig.name) {
- throw new Error('Permission denied: You can only delete your own events');
- }
-
- this.events.delete(eventId);
-
- console.log(`Calendar event deleted for ${userConfig.name}: ${eventId}`);
-
- // TODO: When CalDAV library is available, replace with:
- // await this._deleteCalDAVEvent(userConfig, eventId);
+ // Delete event via CalDAV
+ await this._deleteCalDAVEvent(userConfig, eventId);
return true;
} catch (error) {
- console.error('Error deleting calendar event:', error);
throw error;
}
}
@@ -187,8 +163,14 @@ class CalendarService {
*/
async testConnection(userConfig) {
try {
+ // Validate user configuration
+ if (!userConfig.nextcloudUrl || !userConfig.username || !userConfig.password) {
+ return false;
+ }
+
// Basic HTTP test to verify Nextcloud instance is reachable
const url = new URL(userConfig.nextcloudUrl);
+
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
@@ -202,38 +184,44 @@ class CalendarService {
const protocol = url.protocol === 'https:' ? https : http;
- return new Promise((resolve) => {
+ const result = await new Promise((resolve) => {
const req = protocol.request(options, (res) => {
let data = '';
- res.on('data', chunk => data += chunk);
+ res.on('data', chunk => {
+ data += chunk;
+ });
+
res.on('end', () => {
try {
const status = JSON.parse(data);
const isNextcloud = status.productname && status.productname.toLowerCase().includes('nextcloud');
- console.log(`Connection test for ${userConfig.name}: ${isNextcloud ? 'SUCCESS' : 'NOT_NEXTCLOUD'}`);
resolve(isNextcloud);
} catch (e) {
- console.log(`Connection test for ${userConfig.name}: INVALID_RESPONSE`);
resolve(false);
}
});
});
- req.on('error', (error) => {
- console.log(`Connection test for ${userConfig.name}: ERROR - ${error.message}`);
+ req.on('error', () => {
resolve(false);
});
req.on('timeout', () => {
- console.log(`Connection test for ${userConfig.name}: TIMEOUT`);
req.destroy();
resolve(false);
});
req.end();
});
+
+ // If basic connection works, test CalDAV endpoint
+ if (result) {
+ const caldavResult = await this._testCalDAVConnection(userConfig);
+ return caldavResult;
+ }
+
+ return result;
} catch (error) {
- console.error('Error testing connection:', error);
return false;
}
}
@@ -247,44 +235,520 @@ class CalendarService {
return date.toISOString().slice(0, 10).replace(/-/g, '');
}
- // TODO: Implement these methods when @nextcloud/cdav-library is available
- /*
+ /**
+ * Test CalDAV connection by trying to access the calendar endpoint
+ * @private
+ */
+ async _testCalDAVConnection(userConfig) {
+ try {
+ // Try to access the CalDAV principal endpoint
+ const caldavUrl = this._buildCalDAVUrl(userConfig);
+
+ const url = new URL(caldavUrl);
+ const auth = Buffer.from(`${userConfig.username}:${userConfig.password}`).toString('base64');
+
+ const options = {
+ hostname: url.hostname,
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
+ path: url.pathname,
+ method: 'PROPFIND',
+ timeout: 10000,
+ headers: {
+ 'Authorization': `Basic ${auth}`,
+ 'Content-Type': 'application/xml; charset=utf-8',
+ 'Depth': '0',
+ 'User-Agent': 'OpenWall/1.0 CalDAV-Client'
+ }
+ };
+
+ const protocol = url.protocol === 'https:' ? https : http;
+
+ const body = `
+
+
+
+
+
+
+`;
+
+ return new Promise((resolve) => {
+ const req = protocol.request(options, (res) => {
+ let data = '';
+ res.on('data', chunk => {
+ data += chunk;
+ });
+
+ res.on('end', () => {
+ if (res.statusCode >= 200 && res.statusCode < 300) {
+ resolve(true);
+ } else {
+ resolve(false);
+ }
+ });
+ });
+
+ req.on('error', () => {
+ resolve(false);
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ resolve(false);
+ });
+
+ req.write(body);
+ req.end();
+ });
+ } catch (error) {
+ return false;
+ }
+ }
+
+ /**
+ * Build CalDAV URL for a user
+ * @private
+ */
+ _buildCalDAVUrl(userConfig) {
+ // Remove trailing slash from nextcloudUrl if present
+ const baseUrl = userConfig.nextcloudUrl.replace(/\/$/, '');
+
+ // Build CalDAV principal URL - this is the standard Nextcloud CalDAV path
+ const calendarName = userConfig.calendarName || 'personal';
+ return `${baseUrl}/remote.php/dav/calendars/${userConfig.username}/${calendarName}/`;
+ }
+
+ /**
+ * Build CalDAV principals URL for a user
+ * @private
+ */
+ _buildCalDAVPrincipalsUrl(userConfig) {
+ const baseUrl = userConfig.nextcloudUrl.replace(/\/$/, '');
+ return `${baseUrl}/remote.php/dav/principals/users/${userConfig.username}/`;
+ }
+
+ /**
+ * Create a CalDAV event
+ * @private
+ */
async _createCalDAVEvent(userConfig, event) {
- const { createClient } = require('@nextcloud/cdav-library');
- const client = createClient({
- url: userConfig.nextcloudUrl,
- username: userConfig.username,
- password: userConfig.password
- });
-
- // Create ICS event content
- const icsContent = this._generateICSEvent(event);
-
- // Create event on Nextcloud
- const result = await client.createEvent(icsContent);
- return result;
+ try {
+ const caldavUrl = this._buildCalDAVUrl(userConfig);
+ const eventUrl = `${caldavUrl}${event.id}.ics`;
+
+ const url = new URL(eventUrl);
+ const auth = Buffer.from(`${userConfig.username}:${userConfig.password}`).toString('base64');
+
+ const icsContent = this._generateICSEvent(event);
+
+ const options = {
+ hostname: url.hostname,
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
+ path: url.pathname,
+ method: 'PUT',
+ timeout: 10000,
+ headers: {
+ 'Authorization': `Basic ${auth}`,
+ 'Content-Type': 'text/calendar; charset=utf-8',
+ 'Content-Length': Buffer.byteLength(icsContent, 'utf8'),
+ 'User-Agent': 'OpenWall/1.0 CalDAV-Client',
+ 'If-None-Match': '*' // Prevent overwriting existing events
+ }
+ };
+
+ const protocol = url.protocol === 'https:' ? https : http;
+
+ return new Promise((resolve, reject) => {
+ const req = protocol.request(options, (res) => {
+ let data = '';
+ res.on('data', chunk => {
+ data += chunk;
+ });
+
+ res.on('end', () => {
+ if (res.statusCode >= 200 && res.statusCode < 300) {
+ resolve(event);
+ } else if (res.statusCode === 401) {
+ reject(new Error('CalDAV authentication failed'));
+ } else if (res.statusCode === 412) {
+ reject(new Error('CalDAV event already exists'));
+ } else {
+ reject(new Error(`CalDAV event creation failed with status ${res.statusCode}: ${data}`));
+ }
+ });
+ });
+
+ req.on('error', (error) => {
+ reject(new Error(`CalDAV request failed: ${error.message}`));
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ reject(new Error('CalDAV request timeout'));
+ });
+
+ req.write(icsContent);
+ req.end();
+ });
+ } catch (error) {
+ throw error;
+ }
}
+ /**
+ * Fetch CalDAV events
+ * @private
+ */
async _fetchCalDAVEvents(userConfig, year, month) {
- const { createClient } = require('@nextcloud/cdav-library');
- const client = createClient({
- url: userConfig.nextcloudUrl,
- username: userConfig.username,
- password: userConfig.password
- });
-
- const startDate = new Date(year, month - 1, 1);
- const endDate = new Date(year, month, 0);
-
- const events = await client.fetchCalendarEvents({
- start: startDate,
- end: endDate
- });
-
- return events;
+ try {
+ const caldavUrl = this._buildCalDAVUrl(userConfig);
+
+ const url = new URL(caldavUrl);
+ const auth = Buffer.from(`${userConfig.username}:${userConfig.password}`).toString('base64');
+
+ // Build date range for the month
+ const startDate = new Date(parseInt(year), parseInt(month) - 1, 1);
+ const endDate = new Date(parseInt(year), parseInt(month), 0);
+
+ const startDateStr = startDate.toISOString().slice(0, 10).replace(/-/g, '') + 'T000000Z';
+ const endDateStr = endDate.toISOString().slice(0, 10).replace(/-/g, '') + 'T235959Z';
+
+ const reportBody = `
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+ const options = {
+ hostname: url.hostname,
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
+ path: url.pathname,
+ method: 'REPORT',
+ timeout: 10000,
+ headers: {
+ 'Authorization': `Basic ${auth}`,
+ 'Content-Type': 'application/xml; charset=utf-8',
+ 'Content-Length': Buffer.byteLength(reportBody, 'utf8'),
+ 'Depth': '1',
+ 'User-Agent': 'OpenWall/1.0 CalDAV-Client'
+ }
+ };
+
+ const protocol = url.protocol === 'https:' ? https : http;
+
+ return new Promise((resolve, reject) => {
+ const req = protocol.request(options, (res) => {
+ let data = '';
+ res.on('data', chunk => {
+ data += chunk;
+ });
+
+ res.on('end', () => {
+ if (res.statusCode >= 200 && res.statusCode < 300) {
+ try {
+ const events = this._parseCalDAVResponse(data, userConfig);
+ resolve(events);
+ } catch (parseError) {
+ reject(parseError);
+ }
+ } else if (res.statusCode === 401) {
+ reject(new Error('CalDAV authentication failed'));
+ } else {
+ reject(new Error(`CalDAV event fetch failed with status ${res.statusCode}: ${data}`));
+ }
+ });
+ });
+
+ req.on('error', (error) => {
+ reject(new Error(`CalDAV request failed: ${error.message}`));
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ reject(new Error('CalDAV request timeout'));
+ });
+
+ req.write(reportBody);
+ req.end();
+ });
+ } catch (error) {
+ throw error;
+ }
}
+ /**
+ * Parse CalDAV response and extract events
+ * @private
+ */
+ _parseCalDAVResponse(xmlData, userConfig) {
+ try {
+ // This is a basic parser - in a real implementation, you'd use a proper XML parser
+ const events = [];
+
+ // Look for calendar-data with different possible namespace prefixes
+ const eventMatches = xmlData.match(/<(?:C:|cal:)?calendar-data[^>]*>([\s\S]*?)<\/(?:C:|cal:)?calendar-data>/gi);
+
+ if (eventMatches) {
+ eventMatches.forEach((match, index) => {
+ try {
+ // Extract the VCALENDAR content - handle CDATA and escaped content
+ let icsContent = match
+ .replace(//g, '$1') // Remove CDATA wrapper
+ .replace(/<[^>]*>/g, '') // Remove XML tags
+ .replace(/</g, '<') // Decode HTML entities
+ .replace(/>/g, '>')
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .trim();
+
+ const event = this._parseICSEvent(icsContent, userConfig);
+
+ if (event) {
+ events.push(event);
+ }
+ } catch (eventError) {
+ // Skip individual events that fail to parse
+ }
+ });
+ }
+
+ return events;
+ } catch (error) {
+ throw new Error(`Failed to parse CalDAV response: ${error.message}`);
+ }
+ }
+
+ /**
+ * Parse ICS event content
+ * @private
+ */
+ _parseICSEvent(icsContent, userConfig) {
+ try {
+ const lines = icsContent.split('\n').map(line => line.trim()).filter(line => line);
+
+ const event = {
+ user: userConfig.name,
+ source: 'caldav'
+ };
+
+ for (const line of lines) {
+ if (line.startsWith('UID:')) {
+ event.id = line.substring(4);
+ } else if (line.startsWith('SUMMARY:')) {
+ event.text = line.substring(8);
+ event.summary = event.text;
+ } else if (line.startsWith('DTSTART')) {
+ const dateValue = line.split(':')[1];
+ event.dtstart = dateValue;
+ // Parse date more robustly
+ if (dateValue && dateValue.length >= 8) {
+ // Handle different DTSTART formats: YYYYMMDD, YYYYMMDDTHHMMSSZ, etc.
+ const dateOnly = dateValue.substring(0, 8);
+ if (/^\d{8}$/.test(dateOnly)) {
+ const year = parseInt(dateOnly.substring(0, 4));
+ const month = parseInt(dateOnly.substring(4, 6));
+ const day = parseInt(dateOnly.substring(6, 8));
+
+ // Validate the parsed date and ensure it's reasonable (not from 1981!)
+ if (year >= 2020 && year <= 2030 && month >= 1 && month <= 12 && day >= 1 && day <= 31) {
+ const parsedDate = new Date(year, month - 1, day);
+ if (!isNaN(parsedDate.getTime()) &&
+ parsedDate.getFullYear() === year &&
+ parsedDate.getMonth() === (month - 1) &&
+ parsedDate.getDate() === day) {
+ event.date = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
+ }
+ }
+ }
+ }
+ } else if (line.startsWith('DTEND')) {
+ const dateValue = line.split(':')[1];
+ event.dtend = dateValue;
+ // If we don't have a start date yet, try to get it from end date
+ if (!event.date && dateValue && dateValue.length >= 8) {
+ const dateOnly = dateValue.substring(0, 8);
+ if (/^\d{8}$/.test(dateOnly)) {
+ const year = parseInt(dateOnly.substring(0, 4));
+ const month = parseInt(dateOnly.substring(4, 6));
+ const day = parseInt(dateOnly.substring(6, 8));
+
+ // Validate the parsed date and ensure it's reasonable (not from 1981!)
+ if (year >= 2020 && year <= 2030 && month >= 1 && month <= 12 && day >= 1 && day <= 31) {
+ const parsedDate = new Date(year, month - 1, day);
+ if (!isNaN(parsedDate.getTime()) &&
+ parsedDate.getFullYear() === year &&
+ parsedDate.getMonth() === (month - 1) &&
+ parsedDate.getDate() === day) {
+ event.date = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
+ }
+ }
+ }
+ }
+ } else if (line.startsWith('CREATED:')) {
+ event.createdAt = line.substring(8);
+ } else if (line.startsWith('LAST-MODIFIED:')) {
+ event.updatedAt = line.substring(14);
+ }
+ }
+
+ // Only return events with required fields and valid dates
+ if (event.id && event.text && event.date) {
+ return event;
+ } else {
+ return null;
+ }
+ } catch (error) {
+ return null;
+ }
+ }
+
+ /**
+ * Update a CalDAV event
+ * @private
+ */
+ async _updateCalDAVEvent(userConfig, event) {
+ try {
+ const caldavUrl = this._buildCalDAVUrl(userConfig);
+ const eventUrl = `${caldavUrl}${event.id}.ics`;
+
+ const url = new URL(eventUrl);
+ const auth = Buffer.from(`${userConfig.username}:${userConfig.password}`).toString('base64');
+
+ const icsContent = this._generateICSEvent(event);
+
+ const options = {
+ hostname: url.hostname,
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
+ path: url.pathname,
+ method: 'PUT',
+ timeout: 10000,
+ headers: {
+ 'Authorization': `Basic ${auth}`,
+ 'Content-Type': 'text/calendar; charset=utf-8',
+ 'Content-Length': Buffer.byteLength(icsContent, 'utf8'),
+ 'User-Agent': 'OpenWall/1.0 CalDAV-Client'
+ }
+ };
+
+ const protocol = url.protocol === 'https:' ? https : http;
+
+ return new Promise((resolve, reject) => {
+ const req = protocol.request(options, (res) => {
+ let data = '';
+ res.on('data', chunk => {
+ data += chunk;
+ });
+
+ res.on('end', () => {
+ if (res.statusCode >= 200 && res.statusCode < 300) {
+ resolve(event);
+ } else if (res.statusCode === 401) {
+ reject(new Error('CalDAV authentication failed'));
+ } else if (res.statusCode === 404) {
+ reject(new Error('Event not found'));
+ } else {
+ reject(new Error(`CalDAV event update failed with status ${res.statusCode}: ${data}`));
+ }
+ });
+ });
+
+ req.on('error', (error) => {
+ reject(new Error(`CalDAV request failed: ${error.message}`));
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ reject(new Error('CalDAV request timeout'));
+ });
+
+ req.write(icsContent);
+ req.end();
+ });
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ /**
+ * Delete a CalDAV event
+ * @private
+ */
+ async _deleteCalDAVEvent(userConfig, eventId) {
+ try {
+ const caldavUrl = this._buildCalDAVUrl(userConfig);
+ const eventUrl = `${caldavUrl}${eventId}.ics`;
+
+ const url = new URL(eventUrl);
+ const auth = Buffer.from(`${userConfig.username}:${userConfig.password}`).toString('base64');
+
+ const options = {
+ hostname: url.hostname,
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
+ path: url.pathname,
+ method: 'DELETE',
+ timeout: 10000,
+ headers: {
+ 'Authorization': `Basic ${auth}`,
+ 'User-Agent': 'OpenWall/1.0 CalDAV-Client'
+ }
+ };
+
+ const protocol = url.protocol === 'https:' ? https : http;
+
+ return new Promise((resolve, reject) => {
+ const req = protocol.request(options, (res) => {
+ let data = '';
+ res.on('data', chunk => {
+ data += chunk;
+ });
+
+ res.on('end', () => {
+ if (res.statusCode >= 200 && res.statusCode < 300) {
+ resolve(true);
+ } else if (res.statusCode === 401) {
+ reject(new Error('CalDAV authentication failed'));
+ } else if (res.statusCode === 404) {
+ reject(new Error('Event not found'));
+ } else {
+ reject(new Error(`CalDAV event deletion failed with status ${res.statusCode}: ${data}`));
+ }
+ });
+ });
+
+ req.on('error', (error) => {
+ reject(new Error(`CalDAV request failed: ${error.message}`));
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ reject(new Error('CalDAV request timeout'));
+ });
+
+ req.end();
+ });
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ /**
+ * Generate ICS event content
+ * @private
+ */
_generateICSEvent(event) {
+ const now = new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
+ const created = event.createdAt ? new Date(event.createdAt).toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z' : now;
+ const modified = event.updatedAt ? new Date(event.updatedAt).toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z' : now;
+
return `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//OpenWall//Calendar//EN
@@ -294,12 +758,12 @@ DTSTART;VALUE=DATE:${event.dtstart}
DTEND;VALUE=DATE:${event.dtend}
SUMMARY:${event.summary}
DESCRIPTION:${event.text}
-CREATED:${new Date(event.createdAt).toISOString().replace(/[-:]/g, '').split('.')[0]}Z
-LAST-MODIFIED:${new Date(event.updatedAt).toISOString().replace(/[-:]/g, '').split('.')[0]}Z
+CREATED:${created}
+LAST-MODIFIED:${modified}
+DTSTAMP:${now}
END:VEVENT
END:VCALENDAR`;
}
- */
}
module.exports = new CalendarService();