diff --git a/TOOLS.md b/TOOLS.md index 0739c6a..9cc5ad4 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -134,20 +134,20 @@ forgejo raw # Raw API call ## CalDAV Calendar Access -Credentials stored in `.credentials/nextcloud.env`: -- URL: `https://nextcloud.cloonar.com` -- User: `moltbot@cloonar.com` -- Calendar: `personal_shared_by_dominik.polakovics@cloonar.com` +Helper script: `~/.openclaw/workspace/bin/calendar` -To fetch today's events: ```bash -source .credentials/nextcloud.env -curl -s -X REPORT -u "$NEXTCLOUD_USER:$NEXTCLOUD_PASS" \ - -H "Content-Type: application/xml" -H "Depth: 1" \ - -d '' \ - "$NEXTCLOUD_URL/remote.php/dav/calendars/$NEXTCLOUD_USER/$CALDAV_CALENDAR/" +calendar today # Today's events (default) +calendar tomorrow # Tomorrow's events +calendar week # Next 7 days +calendar next # Next 14 days ``` +- Credentials: `.credentials/services.env` (NEXTCLOUD_URL, NEXTCLOUD_USER, NEXTCLOUD_PASS, CALDAV_CALENDAR) +- Calendar: `personal_shared_by_dominik.polakovics@cloonar.com` +- Output: tab-separated (time, summary, location) +- Times shown in Vienna timezone + ## AI News RSS (Hybrid Approach) Helper script: `~/.openclaw/workspace/bin/ainews` diff --git a/bin/calendar b/bin/calendar new file mode 100755 index 0000000..551aecd --- /dev/null +++ b/bin/calendar @@ -0,0 +1,188 @@ +#!/usr/bin/env node +// Calendar CLI — CalDAV client for Nextcloud calendar +// Credentials from .credentials/services.env: +// NEXTCLOUD_URL, NEXTCLOUD_USER, NEXTCLOUD_PASS, CALDAV_CALENDAR + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +// Load credentials +const credFile = path.join(__dirname, '..', '.credentials', 'services.env'); +const env = {}; +if (fs.existsSync(credFile)) { + fs.readFileSync(credFile, 'utf8').split('\n').forEach(l => { + const m = l.match(/^([A-Z_]+)=(.+)$/); + if (m) env[m[1]] = m[2].trim(); + }); +} + +const BASE_URL = env.NEXTCLOUD_URL; +const USER = env.NEXTCLOUD_USER; +const PASS = env.NEXTCLOUD_PASS; +const CALENDAR = env.CALDAV_CALENDAR; + +if (!BASE_URL || !USER || !PASS || !CALENDAR) { + console.error('Missing NEXTCLOUD_URL, NEXTCLOUD_USER, NEXTCLOUD_PASS, or CALDAV_CALENDAR in .credentials/services.env'); + process.exit(1); +} + +function caldavRequest(startDate, endDate) { + const url = new URL(`/remote.php/dav/calendars/${USER}/${CALENDAR}/`, BASE_URL); + const auth = Buffer.from(`${USER}:${PASS}`).toString('base64'); + + const body = ` + + + + + + + + + +`; + + return new Promise((resolve, reject) => { + const opts = { + method: 'REPORT', + hostname: url.hostname, + port: url.port || 443, + path: url.pathname, + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/xml', + 'Depth': '1', + }, + }; + const req = https.request(opts, res => { + let data = ''; + res.on('data', c => data += c); + res.on('end', () => resolve({ status: res.statusCode, data })); + }); + req.on('error', reject); + req.write(body); + req.end(); + }); +} + +function parseEvents(xmlData) { + const events = []; + const eventBlocks = xmlData.split('BEGIN:VEVENT'); + + for (let i = 1; i < eventBlocks.length; i++) { + const block = eventBlocks[i].split('END:VEVENT')[0]; + const get = (key) => { + // Handle both folded and unfolded lines + const regex = new RegExp(`${key}[^:]*:(.+?)(?:\\r?\\n(?! ))`, 's'); + const m = block.match(regex); + return m ? m[1].replace(/\r?\n\s/g, '').trim() : null; + }; + + const summary = get('SUMMARY'); + const dtstart = get('DTSTART'); + const dtend = get('DTEND'); + const location = get('LOCATION'); + const description = get('DESCRIPTION'); + + if (summary) { + events.push({ summary, dtstart, dtend, location, description }); + } + } + + // Sort by start time + events.sort((a, b) => (a.dtstart || '').localeCompare(b.dtstart || '')); + return events; +} + +function formatDateTime(dt) { + if (!dt) return ''; + // Handle YYYYMMDD (all-day) + if (/^\d{8}$/.test(dt)) { + return `${dt.slice(0,4)}-${dt.slice(4,6)}-${dt.slice(6,8)} (all day)`; + } + // Handle YYYYMMDDTHHMMSSZ or YYYYMMDDTHHMMSS + const m = dt.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z?$/); + if (m) { + const d = new Date(Date.UTC(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +m[6])); + // Convert to Vienna time + return d.toLocaleString('de-AT', { timeZone: 'Europe/Vienna', dateStyle: 'short', timeStyle: 'short' }); + } + return dt; +} + +function formatDateRange(start, end) { + if (!start) return 'unknown time'; + // All-day event + if (/^\d{8}$/.test(start)) return 'all day'; + const startStr = formatDateTime(start); + if (!end) return startStr; + // Same day — only show end time + const endStr = formatDateTime(end); + return `${startStr} – ${endStr.split(', ').pop() || endStr}`; +} + +function getDateRange(offset, days) { + const start = new Date(); + start.setUTCHours(0, 0, 0, 0); + start.setUTCDate(start.getUTCDate() + offset); + const end = new Date(start); + end.setUTCDate(end.getUTCDate() + days); + + const fmt = d => d.toISOString().replace(/[-:]/g, '').replace(/\.\d+/, ''); + return { start: fmt(start), end: fmt(end) }; +} + +async function main() { + const args = process.argv.slice(2); + const cmd = args[0] || 'today'; + + let range; + switch (cmd) { + case 'today': + range = getDateRange(0, 1); + break; + case 'tomorrow': + range = getDateRange(1, 1); + break; + case 'week': + range = getDateRange(0, 7); + break; + case 'next': + range = getDateRange(0, 14); + break; + default: + console.log('Usage: calendar [today|tomorrow|week|next]'); + console.log(' today — Events for today (default)'); + console.log(' tomorrow — Events for tomorrow'); + console.log(' week — Events for the next 7 days'); + console.log(' next — Events for the next 14 days'); + process.exit(0); + } + + try { + const res = await caldavRequest(range.start, range.end); + if (res.status !== 207 && res.status !== 200) { + console.error(`CalDAV error: HTTP ${res.status}`); + process.exit(1); + } + + const events = parseEvents(res.data); + if (events.length === 0) { + console.log(`No events (${cmd}).`); + return; + } + + for (const e of events) { + const time = formatDateRange(e.dtstart, e.dtend); + const loc = e.location ? ` 📍 ${e.location}` : ''; + console.log(`${time}\t${e.summary}${loc}`); + } + console.log(`\n${events.length} event(s).`); + } catch (err) { + console.error('Error:', err.message); + process.exit(1); + } +} + +main();