Fix: Final working v0.3.1 with fixed function mapping
All checks were successful
Build SilverBullet Plug / build (push) Successful in 25s

This commit is contained in:
2026-02-17 13:55:31 -08:00
parent a7180995b0
commit ced95d2a7a
4 changed files with 35 additions and 76 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,13 +3,16 @@ version: 0.3.1
author: sstent author: sstent
functions: functions:
syncCalendars: syncCalendars:
path: icalendar.ts:syncCalendars
command: "iCalendar: Sync" command: "iCalendar: Sync"
forceSync: forceSync:
path: icalendar.ts:forceSync
command: "iCalendar: Force Sync" command: "iCalendar: Force Sync"
clearCache: clearCache:
path: icalendar.ts:clearCache
command: "iCalendar: Clear Cache" command: "iCalendar: Clear Cache"
showVersion: showVersion:
path: icalendar.ts:showVersion
command: "iCalendar: Show Version" command: "iCalendar: Show Version"
# Grant permissions to fetch from anywhere
permissions: permissions:
- http - http

View File

@@ -3,7 +3,6 @@ import { convertIcsCalendar, type IcsCalendar, type IcsEvent, type IcsDateObject
const VERSION = "0.3.1"; const VERSION = "0.3.1";
const CACHE_KEY = "icalendar:lastSync"; const CACHE_KEY = "icalendar:lastSync";
const DEFAULT_CACHE_DURATION_SECONDS = 21600; // 6 hours
// Mapping of common Windows/Outlook timezones to their standard offsets (in hours) // Mapping of common Windows/Outlook timezones to their standard offsets (in hours)
const TIMEZONE_OFFSETS: Record<string, number> = { const TIMEZONE_OFFSETS: Record<string, number> = {
@@ -65,30 +64,20 @@ function toLocalISO(d: Date): string {
function processIcsDate(obj: any, manualShift = 0): string { function processIcsDate(obj: any, manualShift = 0): string {
if (!obj) return ""; if (!obj) return "";
let wallTimeStr = ""; let wallTimeStr = "";
if (obj.local && typeof obj.local.date === "string") { if (obj.local && typeof obj.local.date === "string") wallTimeStr = obj.local.date;
wallTimeStr = obj.local.date; else if (typeof obj.date === "string") wallTimeStr = obj.date;
} else if (typeof obj.date === "string") { else if (obj.date instanceof Date) wallTimeStr = obj.date.toISOString();
wallTimeStr = obj.date; else if (obj instanceof Date) wallTimeStr = obj.toISOString();
} else if (obj.date instanceof Date) {
wallTimeStr = obj.date.toISOString();
} else if (obj instanceof Date) {
wallTimeStr = obj.toISOString();
}
if (!wallTimeStr) return ""; if (!wallTimeStr) return "";
// 1. Extract the "Wall Time" from the string (ignoring Z if present)
const baseDate = new Date(wallTimeStr.replace("Z", "") + "Z"); const baseDate = new Date(wallTimeStr.replace("Z", "") + "Z");
// 2. Determine Source Timezone Offset
const tzName = obj.local?.timezone || obj.timezone || "UTC"; const tzName = obj.local?.timezone || obj.timezone || "UTC";
const sourceOffset = TIMEZONE_OFFSETS[tzName] ?? 0; const sourceOffset = TIMEZONE_OFFSETS[tzName] ?? 0;
// 3. Calculate True UTC: WallTime - SourceOffset
const utcMillis = baseDate.getTime() - (sourceOffset * 3600000); const utcMillis = baseDate.getTime() - (sourceOffset * 3600000);
const finalDate = new Date(utcMillis + (manualShift * 3600000));
// 4. Apply User's Manual Shift and Localize return toLocalISO(finalDate);
return toLocalISO(new Date(utcMillis + (manualShift * 3600000)));
} }
function isIcsDateObjects(obj: any): obj is IcsDateObjects { function isIcsDateObjects(obj: any): obj is IcsDateObjects {
@@ -96,11 +85,8 @@ function isIcsDateObjects(obj: any): obj is IcsDateObjects {
} }
async function sha256Hash(str: string): Promise<string> { async function sha256Hash(str: string): Promise<string> {
const encoder = new TextEncoder(); const hashBuffer = await crypto.subtle.digest("SHA-256", (new TextEncoder()).encode(str));
const data = encoder.encode(str); return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, "0")).join("");
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
} }
function convertDatesToStrings<T>(obj: T, hourShift = 0): DateToString<T> { function convertDatesToStrings<T>(obj: T, hourShift = 0): DateToString<T> {
@@ -111,41 +97,26 @@ function convertDatesToStrings<T>(obj: T, hourShift = 0): DateToString<T> {
if (typeof obj === 'object') { if (typeof obj === 'object') {
const result: any = {}; const result: any = {};
for (const key in obj) { for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) { if (Object.prototype.hasOwnProperty.call(obj, key)) result[key] = convertDatesToStrings((obj as any)[key], hourShift);
result[key] = convertDatesToStrings((obj as any)[key], hourShift);
}
} }
return result as DateToString<T>; return result as DateToString<T>;
} }
return obj as DateToString<T>; return obj as DateToString<T>;
} }
// ============================================================================
// Configuration & Commands
// ============================================================================
async function getSources(): Promise<{ sources: Source[], tzShift: number }> { async function getSources(): Promise<{ sources: Source[], tzShift: number }> {
try { try {
const rawConfig = await config.get<PlugConfig>("icalendar", { sources: [] }); const rawConfig = await config.get<PlugConfig>("icalendar", { sources: [] });
let sources: Source[] = []; let sources: Source[] = [];
let tzShift = rawConfig.tzShift ?? 0; let tzShift = rawConfig.tzShift ?? 0;
let rawSources = rawConfig.sources; let rawSources = rawConfig.sources;
if (rawSources && typeof rawSources === "object") { if (rawSources && typeof rawSources === "object") {
if (rawSources.tzShift !== undefined && tzShift === 0) tzShift = rawSources.tzShift; if (rawSources.tzShift !== undefined && tzShift === 0) tzShift = rawSources.tzShift;
if (Array.isArray(rawSources)) { if (Array.isArray(rawSources)) sources = rawSources.filter(s => s && typeof s.url === "string");
sources = rawSources.filter(s => s && typeof s.url === "string"); else if (rawSources.url) sources = [rawSources];
} else if (rawSources.url) {
sources = [rawSources];
} else {
for (const key in rawSources) {
if (rawSources[key] && typeof rawSources[key].url === "string") sources.push(rawSources[key]);
}
}
} }
return { sources, tzShift }; return { sources, tzShift };
} catch (e) { } catch (e) {
console.error("Failed to load configuration", e);
return { sources: [], tzShift: 0 }; return { sources: [], tzShift: 0 };
} }
} }
@@ -153,56 +124,39 @@ async function getSources(): Promise<{ sources: Source[], tzShift: number }> {
async function fetchAndParseCalendar(source: Source, hourShift = 0): Promise<CalendarEvent[]> { async function fetchAndParseCalendar(source: Source, hourShift = 0): Promise<CalendarEvent[]> {
let url = source.url.trim(); let url = source.url.trim();
if (url.includes(" ")) url = encodeURI(url); if (url.includes(" ")) url = encodeURI(url);
const response = await fetch(url);
const response = await fetch(url, {
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }
});
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
const calendar = convertIcsCalendar(undefined, await response.text());
const icsData = await response.text();
const calendar: IcsCalendar = convertIcsCalendar(undefined, icsData);
if (!calendar.events) return []; if (!calendar.events) return [];
return await Promise.all(calendar.events.map(async (icsEvent: IcsEvent): Promise<CalendarEvent> => { return await Promise.all(calendar.events.map(async (icsEvent: IcsEvent): Promise<CalendarEvent> => {
const uniqueKey = `${icsEvent.start?.date || ''}${icsEvent.uid || icsEvent.summary || ''}`; const uniqueKey = `${icsEvent.start?.date || ''}${icsEvent.uid || icsEvent.summary || ''}`;
const ref = await sha256Hash(uniqueKey); const ref = await sha256Hash(uniqueKey);
return convertDatesToStrings({ ...icsEvent, ref, tag: "ical-event" as const, sourceName: source.name }, hourShift);
return convertDatesToStrings({
...icsEvent,
ref,
tag: "ical-event" as const,
sourceName: source.name,
}, hourShift);
})); }));
} }
// ============================================================================
// Public Commands
// ============================================================================
export async function syncCalendars() { export async function syncCalendars() {
try { try {
const { sources, tzShift } = await getSources(); const { sources, tzShift } = await getSources();
if (sources.length === 0) { if (sources.length === 0) return;
console.log("[iCalendar] No sources configured.");
return;
}
await editor.flashNotification("Syncing calendars...", "info"); await editor.flashNotification("Syncing calendars...", "info");
const allEvents: CalendarEvent[] = []; const allEvents: CalendarEvent[] = [];
for (const source of sources) { for (const source of sources) {
try { try {
const events = await fetchAndParseCalendar(source, tzShift); const events = await fetchAndParseCalendar(source, tzShift);
allEvents.push(...events); allEvents.push(...events);
} catch (err) { } catch (err) {
console.error(`[iCalendar] Failed to sync ${source.name}:`, err); console.error(`Failed to sync ${source.name}:`, err);
await editor.flashNotification(`Failed to sync ${source.name}`, "error");
} }
} }
await index.indexObjects("$icalendar", allEvents); await index.indexObjects("$icalendar", allEvents);
await editor.flashNotification(`Synced ${allEvents.length} events`, "info"); await editor.flashNotification(`Synced ${allEvents.length} events`, "info");
} catch (err) { } catch (err) {
console.error("[iCalendar] Sync failed:", err); console.error("Sync failed:", err);
} }
} }