From a2814cc327061eb56d29748bc7188d50dc8b4598 Mon Sep 17 00:00:00 2001 From: chiko Date: Fri, 9 Oct 2026 01:21:15 +0200 Subject: [PATCH] Added the Event Information to the ICS Event Body --- src/eventText.ts | 26 ++++++++++++++++++++++++++ src/index.ts | 9 ++++++--- src/poller.ts | 17 ++++++++++------- src/storage.ts | 27 ++------------------------- 4 files changed, 44 insertions(+), 35 deletions(-) create mode 100644 src/eventText.ts diff --git a/src/eventText.ts b/src/eventText.ts new file mode 100644 index 0000000..3c29225 --- /dev/null +++ b/src/eventText.ts @@ -0,0 +1,26 @@ +import type { StoredCalendarEvent } from './types.js'; + +export function formatEventDetails(event: StoredCalendarEvent): string[] { + // Source event times are wall-clock values in fixed GMT+2 throughout the year. + const start = event.allDay ? undefined : new Date(Date.parse(event.start) + 120 * 60_000); + const date = start ? start.toISOString().slice(0, 10) : event.start.slice(0, 10); + let time = 'All day'; + if (start) { + const hour = start.getUTCHours(); + const minute = start.getUTCMinutes(); + const delta = hour * 60 + minute - 21 * 60; + const offset = delta === 0 ? ' (OP Time)' : ` (OP Time ${delta > 0 ? '+' : '-'}${Math.floor(Math.abs(delta) / 60)}:${String(Math.abs(delta) % 60).padStart(2, '0')})`; + time = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}${offset}`; + } + + return [ + `Title: ${event.title}`, + `Date: ${date}`, + `Time: ${time}`, + `Type: ${event.eventType}`, + ...(event.location ? [`Location: ${event.location}`] : []), + ...(event.postedBy ? [`By: ${event.postedBy}`] : []), + ...(event.url ? [`Link: ${event.url}`] : []), + ...(event.description ? [`Description: ${event.description}`] : []), + ]; +} diff --git a/src/index.ts b/src/index.ts index 8e6a9f7..1b3cd12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { getEvents, getPollStatus, refresh, startPolling } from './poller.js'; import type { StoredCalendarEvent } from './types.js'; import { getCalendarEntries } from './storage.js'; +import { formatEventDetails } from './eventText.js'; const port = Number(process.env.PORT ?? 3000); @@ -33,7 +34,7 @@ function toIcsEvent(event: StoredCalendarEvent): string { ? [`DTSTART;VALUE=DATE:${icsDay(event.start)}`, `DTEND;VALUE=DATE:${icsDay(event.end)}`] : [`DTSTART:${icsDate(event.start)}`, `DTEND:${icsDate(event.end)}`]), `SUMMARY:${escapeIcs(event.title)}`, - ...(event.description ? [`DESCRIPTION:${escapeIcs(event.description)}`] : []), + `DESCRIPTION:${escapeIcs(formatEventDetails(event).join('\n'))}`, ...(event.location ? [`LOCATION:${escapeIcs(event.location)}`] : []), ...(event.url ? [`URL:${escapeIcs(event.url)}`] : []), 'END:VEVENT', @@ -70,13 +71,15 @@ async function handle(request: IncomingMessage, response: ServerResponse): Promi sendJson(response, status.lastError ? 502 : 200, status); return; } - if (request.method === 'GET' && url.pathname === '/calendar.ics') { + if ((request.method === 'GET' || request.method === 'HEAD') && url.pathname === '/calendar.ics') { + const body = calendarBody(); response.writeHead(200, { 'content-type': 'text/calendar; charset=utf-8', 'content-disposition': 'inline; filename="calendar.ics"', 'cache-control': 'public, max-age=300', + 'content-length': String(Buffer.byteLength(body)), }); - response.end(calendarBody()); + response.end(request.method === 'HEAD' ? undefined : body); return; } sendJson(response, 404, { error: 'Not found' }); diff --git a/src/poller.ts b/src/poller.ts index 14b5dee..45f0648 100644 --- a/src/poller.ts +++ b/src/poller.ts @@ -18,7 +18,7 @@ interface SourceEvent { description?: string; date_at: string; time_start: string; - time_end: string; + time_end?: string | null; timezone?: string | number; location?: string; link?: string; @@ -59,11 +59,12 @@ function mapEvent(value: unknown, index: number, sourceMonth: string): StoredCal if (typeof source.title !== 'string' || !source.title.trim() || typeof source.date_at !== 'string') { throw new Error(`Event at index ${index} is missing its title or date_at`); } - if (typeof source.time_start !== 'string' || typeof source.time_end !== 'string') { - throw new Error(`Event at index ${index} is missing its start or end time`); + if (typeof source.time_start !== 'string') { + throw new Error(`Event at index ${index} is missing its start time`); } - const allDay = source.time_start.toLowerCase() === 'all' || source.time_end.toLowerCase() === 'all'; + const endTime = typeof source.time_end === 'string' ? source.time_end.trim() : ''; + const allDay = source.time_start.toLowerCase() === 'all' || endTime.toLowerCase() === 'all'; let start: string; let end: string; if (allDay) { @@ -73,10 +74,12 @@ function mapEvent(value: unknown, index: number, sourceMonth: string): StoredCal const offset = Number(source.timezone ?? timezoneOffsetMinutes); if (!Number.isFinite(offset)) throw new Error(`Event at index ${index} has an invalid timezone offset`); start = eventDateTime(source.date_at, source.time_start, offset); - let endMs = Date.parse(eventDateTime(source.date_at, source.time_end, offset)); const startMs = Date.parse(start); - if (endMs < startMs) endMs += 24 * 60 * 60 * 1000; - if (endMs === startMs) endMs += 60 * 60 * 1000; + let endMs = endTime + ? Date.parse(eventDateTime(source.date_at, endTime, offset)) + : startMs + 2 * 60 * 60 * 1000; + if (endTime && endMs < startMs) endMs += 24 * 60 * 60 * 1000; + if (endTime && endMs === startMs) endMs += 60 * 60 * 1000; end = new Date(endMs).toISOString(); } diff --git a/src/storage.ts b/src/storage.ts index c8f18df..ca90a06 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -2,6 +2,7 @@ import { mkdirSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import type { CalendarEvent, PendingNotification, StoredCalendarEvent } from './types.js'; +import { formatEventDetails } from './eventText.js'; const databasePath = resolve(process.env.DB_PATH ?? './data/events.sqlite'); mkdirSync(dirname(databasePath), { recursive: true }); @@ -122,30 +123,6 @@ function mapRow(row: Record): StoredCalendarEvent { }; } -function eventDetails(event: StoredCalendarEvent): string[] { - // Source event times are wall-clock values in fixed GMT+2 throughout the year. - const start = event.allDay ? undefined : new Date(Date.parse(event.start) + 120 * 60_000); - const date = start ? start.toISOString().slice(0, 10) : event.start.slice(0, 10); - let time = 'All day'; - if (start) { - const hour = start.getUTCHours(); - const minute = start.getUTCMinutes(); - const delta = hour * 60 + minute - 21 * 60; - const offset = delta === 0 ? ' (OP Time)' : ` (OP Time ${delta > 0 ? '+' : '-'}${Math.floor(Math.abs(delta) / 60)}:${String(Math.abs(delta) % 60).padStart(2, '0')})`; - time = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}${offset}`; - } - return [ - `Title: ${event.title}`, - `Date: ${date}`, - `Time: ${time}`, - `Type: ${event.eventType}`, - ...(event.location ? [`Location: ${event.location}`] : []), - ...(event.postedBy ? [`By: ${event.postedBy}`] : []), - ...(event.url ? [`Link: ${event.url}`] : []), - ...(event.description ? [`Description: ${event.description}`] : []), - ]; -} - function queueNotification( kind: 'added' | 'changed' | 'removed', event: StoredCalendarEvent, @@ -156,7 +133,7 @@ function queueNotification( const body = [ metaTitle, '', - ...(previous && kind === 'changed' ? ['Previous details:', ...eventDetails(previous), '', 'Current details:', ...eventDetails(event)] : eventDetails(event)), + ...(previous && kind === 'changed' ? ['Previous details:', ...formatEventDetails(previous), '', 'Current details:', ...formatEventDetails(event)] : formatEventDetails(event)), ].join('\n'); database.prepare(` INSERT OR IGNORE INTO notification_outbox (uid, sequence, change_type, title, body, link, created_at)