Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspell.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@
"pubdate",
"radokristof",
"rajniszp",
"RDATE",
"rebuilded",
"Reis",
"rejas",
Expand Down
8 changes: 7 additions & 1 deletion defaultmodules/calendar/calendarfetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ class CalendarFetcher {
}

const responseData = await response.text();
const parsed = await ical.async.parseICS(responseData);

const filteredData = await CalendarFetcherUtils.preFilterICS(responseData, {
includePastEvents: this.includePastEvents,
maximumNumberOfDays: this.maximumNumberOfDays
});

const parsed = await ical.async.parseICS(filteredData);

Log.debug(`Parsed iCal data from ${this.url} with ${Object.keys(parsed).length} entries.`);

Expand Down
30 changes: 30 additions & 0 deletions defaultmodules/calendar/calendarfetcherutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,36 @@ const CalendarFetcherUtils = {
return moment.tz.guess();
},

/**
* Calculate the time window of events to keep, as [start, end].
* Without includePastEvents the window starts now; otherwise it also
* reaches maximumNumberOfDays into the past.
* @param {object} config Needs includePastEvents (boolean) and maximumNumberOfDays (number).
* @returns {[Date, Date]} The start and end of the window.
*/
calculateFilterWindow (config) {
const today = moment().startOf("day");
const start = config.includePastEvents
? today.clone().subtract(config.maximumNumberOfDays, "days").toDate()
: new Date();
const end = today.clone().add(config.maximumNumberOfDays, "days").toDate();
return [start, end];
},

/**
* Drop ICS data outside the configured time window before it is parsed,
* so that node-ical only has to process events we might actually show.
* @param {string} rawICS The raw ICS text.
* @param {object} config Needs includePastEvents (boolean) and maximumNumberOfDays (number).
* @returns {Promise<string>} The filtered ICS text.
*/
async preFilterICS (rawICS, config) {
// ics-filter is ESM-only, so we import it dynamically from this CommonJS file.
const { icsFilter } = await import("ics-filter");
const [start, end] = CalendarFetcherUtils.calculateFilterWindow(config);
return icsFilter(rawICS, start, end);
},

/**
* Filter the events from ical according to the given config
* @param {object} data the calendar data from ical
Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"helmet": "^8.2.0",
"html-to-text": "^10.0.0",
"iconv-lite": "^0.7.2",
"ics-filter": "^1.0.2",
"ipaddr.js": "^2.4.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.2",
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,29 @@ END:VCALENDAR`);
});
});

describe("calculateFilterWindow", () => {
it("ends maximumNumberOfDays after today's midnight", () => {
const [, end] = CalendarFetcherUtils.calculateFilterWindow({ includePastEvents: false, maximumNumberOfDays: 30 });

expect(end).toEqual(moment().startOf("day").add(30, "days").toDate());
});

it("starts now when includePastEvents is false", () => {
const before = Date.now();
const [start] = CalendarFetcherUtils.calculateFilterWindow({ includePastEvents: false, maximumNumberOfDays: 30 });
const after = Date.now();

expect(start.getTime()).toBeGreaterThanOrEqual(before);
expect(start.getTime()).toBeLessThanOrEqual(after);
});

it("starts maximumNumberOfDays before today's midnight when includePastEvents is true", () => {
const [start] = CalendarFetcherUtils.calculateFilterWindow({ includePastEvents: true, maximumNumberOfDays: 30 });

expect(start).toEqual(moment().startOf("day").subtract(30, "days").toDate());
});
});

describe("expandRecurringEvent", () => {
it("should extend end to end-of-day when event has no DTEND", () => {
// node-ical sets end === start when DTEND is absent; our code extends to endOf("day")
Expand Down