Implement CALDAV for nextcloud
This commit is contained in:
@@ -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' });
|
||||
}
|
||||
});
|
||||
|
@@ -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 = `<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
<D:resourcetype/>
|
||||
<D:displayname/>
|
||||
<C:calendar-description/>
|
||||
</D:prop>
|
||||
</D:propfind>`;
|
||||
|
||||
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 = `<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
<D:getetag/>
|
||||
<C:calendar-data/>
|
||||
</D:prop>
|
||||
<C:filter>
|
||||
<C:comp-filter name="VCALENDAR">
|
||||
<C:comp-filter name="VEVENT">
|
||||
<C:time-range start="${startDateStr}" end="${endDateStr}"/>
|
||||
</C:comp-filter>
|
||||
</C:comp-filter>
|
||||
</C:filter>
|
||||
</C:calendar-query>`;
|
||||
|
||||
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(/<!\[CDATA\[([\s\S]*?)\]\]>/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();
|
||||
|
Reference in New Issue
Block a user