Added the Event Information to the ICS Event Body
This commit is contained in:
1 parent
120e25d50d
commit
a2814cc327
4 files changed
+44
-35
No files matched your search
@@ -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}`] : []),
|
||||||
|
];
|
||||||
|
}
|
||||||
+6
-3
@@ -2,6 +2,7 @@
|
|||||||
import { getEvents, getPollStatus, refresh, startPolling } from './poller.js';
|
import { getEvents, getPollStatus, refresh, startPolling } from './poller.js';
|
||||||
import type { StoredCalendarEvent } from './types.js';
|
import type { StoredCalendarEvent } from './types.js';
|
||||||
import { getCalendarEntries } from './storage.js';
|
import { getCalendarEntries } from './storage.js';
|
||||||
|
import { formatEventDetails } from './eventText.js';
|
||||||
|
|
||||||
const port = Number(process.env.PORT ?? 3000);
|
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;VALUE=DATE:${icsDay(event.start)}`, `DTEND;VALUE=DATE:${icsDay(event.end)}`]
|
||||||
: [`DTSTART:${icsDate(event.start)}`, `DTEND:${icsDate(event.end)}`]),
|
: [`DTSTART:${icsDate(event.start)}`, `DTEND:${icsDate(event.end)}`]),
|
||||||
`SUMMARY:${escapeIcs(event.title)}`,
|
`SUMMARY:${escapeIcs(event.title)}`,
|
||||||
...(event.description ? [`DESCRIPTION:${escapeIcs(event.description)}`] : []),
|
`DESCRIPTION:${escapeIcs(formatEventDetails(event).join('\n'))}`,
|
||||||
...(event.location ? [`LOCATION:${escapeIcs(event.location)}`] : []),
|
...(event.location ? [`LOCATION:${escapeIcs(event.location)}`] : []),
|
||||||
...(event.url ? [`URL:${escapeIcs(event.url)}`] : []),
|
...(event.url ? [`URL:${escapeIcs(event.url)}`] : []),
|
||||||
'END:VEVENT',
|
'END:VEVENT',
|
||||||
@@ -70,13 +71,15 @@ async function handle(request: IncomingMessage, response: ServerResponse): Promi
|
|||||||
sendJson(response, status.lastError ? 502 : 200, status);
|
sendJson(response, status.lastError ? 502 : 200, status);
|
||||||
return;
|
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, {
|
response.writeHead(200, {
|
||||||
'content-type': 'text/calendar; charset=utf-8',
|
'content-type': 'text/calendar; charset=utf-8',
|
||||||
'content-disposition': 'inline; filename="calendar.ics"',
|
'content-disposition': 'inline; filename="calendar.ics"',
|
||||||
'cache-control': 'public, max-age=300',
|
'cache-control': 'public, max-age=300',
|
||||||
|
'content-length': String(Buffer.byteLength(body)),
|
||||||
});
|
});
|
||||||
response.end(calendarBody());
|
response.end(request.method === 'HEAD' ? undefined : body);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendJson(response, 404, { error: 'Not found' });
|
sendJson(response, 404, { error: 'Not found' });
|
||||||
|
|||||||
+10
-7
@@ -18,7 +18,7 @@ interface SourceEvent {
|
|||||||
description?: string;
|
description?: string;
|
||||||
date_at: string;
|
date_at: string;
|
||||||
time_start: string;
|
time_start: string;
|
||||||
time_end: string;
|
time_end?: string | null;
|
||||||
timezone?: string | number;
|
timezone?: string | number;
|
||||||
location?: string;
|
location?: string;
|
||||||
link?: 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') {
|
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`);
|
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') {
|
if (typeof source.time_start !== 'string') {
|
||||||
throw new Error(`Event at index ${index} is missing its start or end time`);
|
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 start: string;
|
||||||
let end: string;
|
let end: string;
|
||||||
if (allDay) {
|
if (allDay) {
|
||||||
@@ -73,10 +74,12 @@ function mapEvent(value: unknown, index: number, sourceMonth: string): StoredCal
|
|||||||
const offset = Number(source.timezone ?? timezoneOffsetMinutes);
|
const offset = Number(source.timezone ?? timezoneOffsetMinutes);
|
||||||
if (!Number.isFinite(offset)) throw new Error(`Event at index ${index} has an invalid timezone offset`);
|
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);
|
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);
|
const startMs = Date.parse(start);
|
||||||
if (endMs < startMs) endMs += 24 * 60 * 60 * 1000;
|
let endMs = endTime
|
||||||
if (endMs === startMs) endMs += 60 * 60 * 1000;
|
? 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();
|
end = new Date(endMs).toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-25
@@ -2,6 +2,7 @@
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import { dirname, resolve } from 'node:path';
|
import { dirname, resolve } from 'node:path';
|
||||||
import type { CalendarEvent, PendingNotification, StoredCalendarEvent } from './types.js';
|
import type { CalendarEvent, PendingNotification, StoredCalendarEvent } from './types.js';
|
||||||
|
import { formatEventDetails } from './eventText.js';
|
||||||
|
|
||||||
const databasePath = resolve(process.env.DB_PATH ?? './data/events.sqlite');
|
const databasePath = resolve(process.env.DB_PATH ?? './data/events.sqlite');
|
||||||
mkdirSync(dirname(databasePath), { recursive: true });
|
mkdirSync(dirname(databasePath), { recursive: true });
|
||||||
@@ -122,30 +123,6 @@ function mapRow(row: Record<string, unknown>): 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(
|
function queueNotification(
|
||||||
kind: 'added' | 'changed' | 'removed',
|
kind: 'added' | 'changed' | 'removed',
|
||||||
event: StoredCalendarEvent,
|
event: StoredCalendarEvent,
|
||||||
@@ -156,7 +133,7 @@ function queueNotification(
|
|||||||
const body = [
|
const body = [
|
||||||
metaTitle,
|
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');
|
].join('\n');
|
||||||
database.prepare(`
|
database.prepare(`
|
||||||
INSERT OR IGNORE INTO notification_outbox (uid, sequence, change_type, title, body, link, created_at)
|
INSERT OR IGNORE INTO notification_outbox (uid, sequence, change_type, title, body, link, created_at)
|
||||||
|
|||||||
Reference in new issue
Block a user