mirror of
https://github.com/gethomepage/homepage.git
synced 2025-05-24 02:02:35 -04:00
Chore: change to ical.js for ical parsing (#5241)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
parent
b28cc0b7f6
commit
3c6f99d5ae
@ -13,13 +13,13 @@
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@kubernetes/client-node": "^1.0.0",
|
||||
"cal-parser": "^1.0.2",
|
||||
"classnames": "^2.5.1",
|
||||
"compare-versions": "^6.1.1",
|
||||
"dockerode": "^4.0.4",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"gamedig": "^5.2.0",
|
||||
"i18next": "^24.2.3",
|
||||
"ical.js": "^2.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-rpc-2.0": "^1.7.0",
|
||||
"luxon": "^3.5.0",
|
||||
@ -35,7 +35,6 @@
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-icons": "^5.4.0",
|
||||
"recharts": "^2.15.3",
|
||||
"rrule": "^2.8.1",
|
||||
"swr": "^2.3.3",
|
||||
"systeminformation": "^5.25.11",
|
||||
"tough-cookie": "^5.1.2",
|
||||
|
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@ -14,9 +14,6 @@ importers:
|
||||
'@kubernetes/client-node':
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
cal-parser:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
classnames:
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
@ -35,6 +32,9 @@ importers:
|
||||
i18next:
|
||||
specifier: ^24.2.3
|
||||
version: 24.2.3(typescript@5.7.3)
|
||||
ical.js:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
@ -80,9 +80,6 @@ importers:
|
||||
recharts:
|
||||
specifier: ^2.15.3
|
||||
version: 2.15.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
rrule:
|
||||
specifier: ^2.8.1
|
||||
version: 2.8.1
|
||||
swr:
|
||||
specifier: ^2.3.3
|
||||
version: 2.3.3(react@18.3.1)
|
||||
@ -970,9 +967,6 @@ packages:
|
||||
resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
cal-parser@1.0.2:
|
||||
resolution: {integrity: sha512-wlQwcF0fl4eLclyGdncF9rcNNq0ipRYZGagG6h3LVgRXvCWE1fdMUaCLXwfC9YWoz9jKKbjQAq7TpO2Y3yrvmA==}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1670,8 +1664,8 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
ical-date-parser@4.0.0:
|
||||
resolution: {integrity: sha512-XRCK/FU1akC2ZaJOdKIeZI6BLLgzWUuE0pegSrrkEva89GOan5mNkLVqCU4EMhCJ9nkG5TLWdMXrVX1fNAkFzw==}
|
||||
ical.js@2.1.0:
|
||||
resolution: {integrity: sha512-BOVfrH55xQ6kpS3muGvIXIg2l7p+eoe12/oS7R5yrO3TL/j/bLsR0PR+tYQESFbyTbvGgPHn9zQ6tI4FWyuSaQ==}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
@ -2424,9 +2418,6 @@ packages:
|
||||
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
|
||||
hasBin: true
|
||||
|
||||
rrule@2.8.1:
|
||||
resolution: {integrity: sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
@ -3680,11 +3671,6 @@ snapshots:
|
||||
normalize-url: 8.0.1
|
||||
responselike: 3.0.0
|
||||
|
||||
cal-parser@1.0.2:
|
||||
dependencies:
|
||||
ical-date-parser: 4.0.0
|
||||
rrule: 2.8.1
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@ -4557,7 +4543,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 5.7.3
|
||||
|
||||
ical-date-parser@4.0.0: {}
|
||||
ical.js@2.1.0: {}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
@ -5306,10 +5292,6 @@ snapshots:
|
||||
dependencies:
|
||||
glob: 10.4.5
|
||||
|
||||
rrule@2.8.1:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
run-parallel@1.2.0:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
|
@ -856,7 +856,8 @@
|
||||
"physicalRelease": "Physical release",
|
||||
"digitalRelease": "Digital release",
|
||||
"noEventsToday": "No events for today!",
|
||||
"noEventsFound": "No events found"
|
||||
"noEventsFound": "No events found",
|
||||
"errorWhenLoadingData": "Error when loading calendar data"
|
||||
},
|
||||
"romm": {
|
||||
"platforms": "Platforms",
|
||||
|
@ -1,21 +1,20 @@
|
||||
import { parseString } from "cal-parser";
|
||||
import ICAL from "ical.js";
|
||||
import { DateTime } from "luxon";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useEffect } from "react";
|
||||
import { RRule } from "rrule";
|
||||
|
||||
import Error from "../../../components/services/widget/error";
|
||||
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
|
||||
|
||||
// https://gist.github.com/jlevy/c246006675becc446360a798e2b2d781
|
||||
function simpleHash(str) {
|
||||
/* eslint-disable no-plusplus, no-bitwise */
|
||||
let hash = 0;
|
||||
const prime = 31;
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
|
||||
hash = (hash * prime + str.charCodeAt(i)) % 2_147_483_647;
|
||||
}
|
||||
return (hash >>> 0).toString(36);
|
||||
/* eslint-disable no-plusplus, no-bitwise */
|
||||
|
||||
return Math.abs(hash).toString(36);
|
||||
}
|
||||
|
||||
export default function Integration({ config, params, setEvents, hideErrors, timezone }) {
|
||||
@ -25,11 +24,49 @@ export default function Integration({ config, params, setEvents, hideErrors, tim
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let parsedIcal;
|
||||
const { showName = false } = config?.params || {};
|
||||
let events = [];
|
||||
|
||||
if (!icalError && icalData && !icalData.error) {
|
||||
parsedIcal = parseString(icalData.data);
|
||||
if (parsedIcal.events.length === 0) {
|
||||
if (!icalData.data) {
|
||||
icalData.error = { message: `'${config.name}': ${t("calendar.errorWhenLoadingData")}` };
|
||||
return;
|
||||
}
|
||||
|
||||
const jCal = ICAL.parse(icalData.data);
|
||||
const vCalendar = new ICAL.Component(jCal);
|
||||
|
||||
const buildEvent = (event, type) => {
|
||||
return {
|
||||
id: event.getFirstPropertyValue("uid"),
|
||||
type,
|
||||
title: event.getFirstPropertyValue("summary"),
|
||||
rrule: event.getFirstPropertyValue("rrule"),
|
||||
dtstart:
|
||||
event.getFirstPropertyValue("dtstart") ||
|
||||
event.getFirstPropertyValue("due") ||
|
||||
event.getFirstPropertyValue("completed") ||
|
||||
ICAL.Time.now(), // handles events without a date
|
||||
dtend:
|
||||
event.getFirstPropertyValue("dtend") ||
|
||||
event.getFirstPropertyValue("due") ||
|
||||
event.getFirstPropertyValue("completed") ||
|
||||
ICAL.Time.now(), // handles events without a date
|
||||
location: event.getFirstPropertyValue("location"),
|
||||
status: event.getFirstPropertyValue("status"),
|
||||
};
|
||||
};
|
||||
|
||||
const getEvents = () => {
|
||||
const vEvents = vCalendar.getAllSubcomponents("vevent").map((event) => buildEvent(event, "vevent"));
|
||||
|
||||
const vTodos = vCalendar.getAllSubcomponents("vtodo").map((todo) => buildEvent(todo, "vtodo"));
|
||||
|
||||
return [...vEvents, ...vTodos];
|
||||
};
|
||||
|
||||
events = getEvents();
|
||||
if (events.length === 0) {
|
||||
icalData.error = { message: `'${config.name}': ${t("calendar.noEventsFound")}` };
|
||||
}
|
||||
}
|
||||
@ -37,72 +74,67 @@ export default function Integration({ config, params, setEvents, hideErrors, tim
|
||||
const startDate = DateTime.fromISO(params.start);
|
||||
const endDate = DateTime.fromISO(params.end);
|
||||
|
||||
if (icalError || !parsedIcal || !startDate.isValid || !endDate.isValid) {
|
||||
if (icalError || events.length === 0 || !startDate.isValid || !endDate.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventsToAdd = {};
|
||||
const events = parsedIcal?.getEventsBetweenDates(startDate.toJSDate(), endDate.toJSDate());
|
||||
const now = timezone ? DateTime.now().setZone(timezone) : DateTime.now();
|
||||
const rangeStart = ICAL.Time.fromJSDate(startDate.toJSDate());
|
||||
const rangeEnd = ICAL.Time.fromJSDate(endDate.toJSDate());
|
||||
|
||||
events?.forEach((event) => {
|
||||
let title = `${event?.summary?.value}`;
|
||||
if (config?.params?.showName) {
|
||||
title = `${config.name}: ${title}`;
|
||||
}
|
||||
|
||||
// 'dtend' is null for all-day events
|
||||
const { dtstart, dtend = { value: 0 } } = event;
|
||||
|
||||
const eventToAdd = (date, i, type) => {
|
||||
const days = dtend.value === 0 ? 1 : (dtend.value - dtstart.value) / (1000 * 60 * 60 * 24);
|
||||
const eventDate = timezone ? DateTime.fromJSDate(date, { zone: timezone }) : DateTime.fromJSDate(date);
|
||||
|
||||
for (let j = 0; j < days; j += 1) {
|
||||
// See https://github.com/gethomepage/homepage/issues/2753 uid is not stable
|
||||
// assumption is that the event is the same if the start, end and title are all the same
|
||||
const hash = simpleHash(`${dtstart?.value}${dtend?.value}${title}${i}${j}${type}}`);
|
||||
eventsToAdd[hash] = {
|
||||
title,
|
||||
date: eventDate.plus({ days: j }),
|
||||
color: config?.color ?? "zinc",
|
||||
isCompleted: eventDate < now,
|
||||
additional: event.location?.value,
|
||||
type: "ical",
|
||||
};
|
||||
const getOcurrencesFromRange = (event) => {
|
||||
if (!event.rrule) {
|
||||
if (event.dtstart.compare(rangeStart) >= 0 && event.dtend.compare(rangeEnd) <= 0) {
|
||||
return [event.dtstart];
|
||||
}
|
||||
};
|
||||
|
||||
let recurrenceOptions = event?.recurrenceRule?.origOptions;
|
||||
// RRuleSet does not have dtstart, add it manually
|
||||
if (event?.recurrenceRule && event.recurrenceRule.rrules && event.recurrenceRule.rrules()?.[0]?.origOptions) {
|
||||
recurrenceOptions = event.recurrenceRule.rrules()[0].origOptions;
|
||||
recurrenceOptions.dtstart = dtstart.value;
|
||||
return [];
|
||||
}
|
||||
|
||||
if (recurrenceOptions && Object.keys(recurrenceOptions).length !== 0) {
|
||||
try {
|
||||
const rule = new RRule(recurrenceOptions);
|
||||
const recurringEvents = rule.between(startDate.toJSDate(), endDate.toJSDate());
|
||||
const iterator = event.rrule.iterator(event.dtstart);
|
||||
|
||||
recurringEvents.forEach((date, i) => {
|
||||
let eventDate = date;
|
||||
if (event.dtstart?.params?.tzid) {
|
||||
// date is in UTC but parsed as if it is in current timezone, so we need to adjust it
|
||||
const dateInUTC = DateTime.fromJSDate(date).setZone("UTC");
|
||||
const offset = dateInUTC.offset - DateTime.fromJSDate(date, { zone: event.dtstart.params.tzid }).offset;
|
||||
eventDate = dateInUTC.plus({ minutes: offset }).toJSDate();
|
||||
}
|
||||
eventToAdd(eventDate, i, "recurring");
|
||||
});
|
||||
return;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Unable to parse recurring events from iCal: %s", e);
|
||||
const occurrences = [];
|
||||
for (let next = iterator.next(); next && next.compare(rangeEnd) < 0; next = iterator.next()) {
|
||||
if (next.compare(rangeStart) < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
occurrences.push(next.clone());
|
||||
}
|
||||
|
||||
event.matchingDates.forEach((date, i) => eventToAdd(date, i, "single"));
|
||||
return occurrences;
|
||||
};
|
||||
|
||||
const eventsToAdd = [];
|
||||
events.forEach((event, index) => {
|
||||
const occurrences = getOcurrencesFromRange(event);
|
||||
|
||||
occurrences.forEach((icalDate) => {
|
||||
const date = icalDate.toJSDate();
|
||||
|
||||
const hash = simpleHash(`${event.id}-${event.title}-${index}-${date.toString()}`);
|
||||
|
||||
let title = event.title;
|
||||
if (showName) {
|
||||
title = `${config.name}: ${title}`;
|
||||
}
|
||||
|
||||
const getIsCompleted = () => {
|
||||
if (event.type === "vtodo") {
|
||||
return event.status === "COMPLETED";
|
||||
}
|
||||
|
||||
return DateTime.fromJSDate(date) < DateTime.now();
|
||||
};
|
||||
|
||||
eventsToAdd[hash] = {
|
||||
title,
|
||||
date: DateTime.fromJSDate(date),
|
||||
color: config?.color ?? "zinc",
|
||||
isCompleted: getIsCompleted(),
|
||||
additional: event.location,
|
||||
type: "ical",
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
|
||||
|
Loading…
x
Reference in New Issue
Block a user