Release 2.26.0 (#3319)

## [2.26.0] - 01-01-2024

Thanks to: @bnitkin, @bugsounet, @dependabot, @jkriegshauser,
@kaennchenstruggle, @KristjanESPERANTO and @Ybbet.

Special thanks to @khassel, @rejas and @sdetweil for taking over most
(if not all) of the work on this release as project collaborators. This
version would not be there without their effort. Thank you guys! You are
awesome!

This release also marks the latest release by Michael Teeuw. For more
info, please read the following post: [A New Chapter for MagicMirror:
The Community Takes the
Lead](https://forum.magicmirror.builders/topic/18329/a-new-chapter-for-magicmirror-the-community-takes-the-lead).

### Added

- Added update notification updater (for 3rd party modules)
- Added node 21 to the test matrix
- Added transform object to calendar:customEvents
- Added ESLint rules for jest (including jest/expect-expect and
jest/no-done-callback)

### Removed

- Removed Codecov workflow (not working anymore, other workflow
required) (#3107)
- Removed titleReplace from calendar, replaced + extended by
customEvents (backward compatibility included) (#3249)
- Removed failing unit test (#3254)
- Removed some unused variables

### Updated

- Update electron to v27 and update other dependencies as well as github
actions
- Update newsfeed: Use `html-to-text` instead of regex for transform
description
- Review ESLint config (#3269)
- Updated dependencies
- Clock module: optionally display current moon phase in addition to
rise/set times
- electron is now per default started without gpu, if needed it must be
enabled with new env var `ELECTRON_ENABLE_GPU=1` on startup (#3226)
- Replace prettier by stylistic in ESLint config to lint JavaScript (and
disable some rules for `config/config.js*` files)
- Update node-ical to v0.17.1 and fix tests

### Fixed

- Avoid fade out/in on updateDom when many calendars are used
- Fix the option eventClass on customEvents.
- Fix yr API version in locationforecast and sunrise call (#3227)
- Fix cloneObject() function to respect RegExp (#3237)
- Fix newsfeed module for feeds using "a10:updated" tag (#3238)
- Fix issue template (#3167)
- Fix #3256 filter out bad results from rrule.between
- Fix calendar events sometimes not respecting deleted events (#3250)
- Fix electron loadurl locally on Windows when address "0.0.0.0" (#2550)
- Fix updatanotification (update_helper.js): catch error if reponse is
not an JSON format (check PM2)
- Fix missing typeof in calendar module
- Fix style issues after prettier update
- Fix calendar test (#3291) by moving "Exdate check" from e2e to
electron to run on a Thursday
- Fix calendar config params `fetchInterval` and `excludedEvents` were
never used from single calendar config (#3297)
- Fix MM_PORT variable not used in electron and allow full path for
MM_CONFIG_FILE variable (#3302)

---------

Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
Co-authored-by: Malte Hallström <46646495+SkySails@users.noreply.github.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: veeck <michael@veeck.de>
Co-authored-by: dWoolridge <dwoolridge@charter.net>
Co-authored-by: Johan <jojjepersson@yahoo.se>
Co-authored-by: Dario Mratovich <dario_mratovich@hotmail.com>
Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com>
Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com>
Co-authored-by: Naveen <172697+naveensrinivasan@users.noreply.github.com>
Co-authored-by: buxxi <buxxi@omfilm.net>
Co-authored-by: Thomas Hirschberger <47733292+Tom-Hirschberger@users.noreply.github.com>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com>
Co-authored-by: Dave Child <dave@addedbytes.com>
Co-authored-by: grenagit <46225780+grenagit@users.noreply.github.com>
Co-authored-by: Grena <grena@grenabox.fr>
Co-authored-by: Magnus Marthinsen <magmar@online.no>
Co-authored-by: Patrick <psieg@users.noreply.github.com>
Co-authored-by: Piotr Rajnisz <56397164+rajniszp@users.noreply.github.com>
Co-authored-by: Suthep Yonphimai <tomzt@users.noreply.github.com>
Co-authored-by: CarJem Generations (Carter Wallace) <cwallacecs@gmail.com>
Co-authored-by: Nicholas Fogal <nfogal.misc@gmail.com>
Co-authored-by: JakeBinney <126349119+JakeBinney@users.noreply.github.com>
Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com>
Co-authored-by: Oscar Björkman <17575446+oscarb@users.noreply.github.com>
Co-authored-by: Ismar Slomic <ismar@slomic.no>
Co-authored-by: Jørgen Veum-Wahlberg <jorgen.wahlberg@amedia.no>
Co-authored-by: Eddie Hung <6740044+eddiehung@users.noreply.github.com>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: bugsounet <bugsounet@bugsounet.fr>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Knapoc <Knapoc@users.noreply.github.com>
Co-authored-by: sam detweiler <sdetweil@gmail.com>
Co-authored-by: veeck <michael.veeck@nebenan.de>
Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
Co-authored-by: NolanKingdon <27908974+NolanKingdon@users.noreply.github.com>
Co-authored-by: J. Kenzal Hunter <kenzal.hunter@gmail.com>
Co-authored-by: Teddy <teddy.payet@gmail.com>
Co-authored-by: TeddyStarinvest <teddy.payet@starinvest.com>
Co-authored-by: martingron <61826403+martingron@users.noreply.github.com>
Co-authored-by: dgoth <132394363+dgoth@users.noreply.github.com>
Co-authored-by: kaennchenstruggle <54073894+kaennchenstruggle@users.noreply.github.com>
Co-authored-by: jkriegshauser <jkriegshauser@gmail.com>
Co-authored-by: Ben Nitkin <ben@nitkin.net>
This commit is contained in:
Michael Teeuw
2024-01-01 15:38:08 +01:00
committed by GitHub
parent 343e7de7bd
commit 8c0e7db494
110 changed files with 3433 additions and 2915 deletions

View File

@@ -1,4 +1,4 @@
/* global CalendarUtils, cloneObject */
/* global CalendarUtils */
/* MagicMirror²
* Module: Calendar
@@ -28,6 +28,7 @@ Module.register("calendar", {
fetchInterval: 60 * 60 * 1000, // Update every hour
animationSpeed: 2000,
fade: true,
fadePoint: 0.25, // Start on 1/4th of the list.
urgency: 7,
timeFormat: "relative",
dateFormat: "MMM Do",
@@ -35,14 +36,12 @@ Module.register("calendar", {
fullDayEventDateFormat: "MMM Do",
showEnd: false,
getRelative: 6,
fadePoint: 0.25, // Start on 1/4th of the list.
hidePrivate: false,
hideOngoing: false,
hideTime: false,
hideDuplicates: true,
showTimeToday: false,
colored: false,
customEvents: [], // Array of {keyword: "", symbol: "", color: "", eventClass: ""} where Keyword is a regexp and symbol/color/eventClass are to be applied for matched
tableClass: "small",
calendars: [
{
@@ -50,10 +49,11 @@ Module.register("calendar", {
url: "https://www.calendarlabs.com/templates/ical/US-Holidays.ics"
}
],
titleReplace: {
"De verjaardag van ": "",
"'s birthday": ""
},
customEvents: [
// Array of {keyword: "", symbol: "", color: "", eventClass: ""} where Keyword is a regexp and symbol/color/eventClass are to be applied for matched
{ keyword: ".*", transform: { search: "De verjaardag van ", replace: "" } },
{ keyword: ".*", transform: { search: "'s birthday", replace: "" } }
],
locationTitleReplace: {
"street ": ""
},
@@ -68,23 +68,24 @@ Module.register("calendar", {
coloredSymbol: false,
coloredBackground: false,
limitDaysNeverSkip: false,
flipDateHeaderTitle: false
flipDateHeaderTitle: false,
updateOnFetch: true
},
requiresVersion: "2.1.0",
// Define required scripts.
getStyles: function () {
getStyles () {
return ["calendar.css", "font-awesome.css"];
},
// Define required scripts.
getScripts: function () {
getScripts () {
return ["calendarutils.js", "moment.js"];
},
// Define required translations.
getTranslations: function () {
getTranslations () {
// The translations for the default modules are defined in the core translation files.
// Therefore we can just return false. Otherwise we should have returned a dictionary.
// If you're trying to build your own module including translations, check out the documentation.
@@ -92,9 +93,7 @@ Module.register("calendar", {
},
// Override start method.
start: function () {
const ONE_MINUTE = 60 * 1000;
start () {
Log.info(`Starting module: ${this.name}`);
if (this.config.colored) {
@@ -117,6 +116,9 @@ Module.register("calendar", {
// indicate no data available yet
this.loaded = false;
// data holder of calendar url. Avoid fade out/in on updateDom (one for each calendar update)
this.calendarDisplayer = {};
this.config.calendars.forEach((calendar) => {
calendar.url = calendar.url.replace("webcal://", "http://");
@@ -125,23 +127,25 @@ Module.register("calendar", {
maximumNumberOfDays: calendar.maximumNumberOfDays,
pastDaysCount: calendar.pastDaysCount,
broadcastPastEvents: calendar.broadcastPastEvents,
selfSignedCert: calendar.selfSignedCert
selfSignedCert: calendar.selfSignedCert,
excludedEvents: calendar.excludedEvents,
fetchInterval: calendar.fetchInterval
};
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
if (typeof calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
calendarConfig.symbolClass = "";
}
if (calendar.titleClass === "undefined" || calendar.titleClass === null) {
if (typeof calendar.titleClass === "undefined" || calendar.titleClass === null) {
calendarConfig.titleClass = "";
}
if (calendar.timeClass === "undefined" || calendar.timeClass === null) {
if (typeof calendar.timeClass === "undefined" || calendar.timeClass === null) {
calendarConfig.timeClass = "";
}
// we check user and password here for backwards compatibility with old configs
if (calendar.user && calendar.pass) {
Log.warn("Deprecation warning: Please update your calendar authentication configuration.");
Log.warn("https://github.com/MichMich/MagicMirror/tree/v2.1.2/modules/default/calendar#calendar-authentication-options");
Log.warn("https://docs.magicmirror.builders/modules/calendar.html#configuration-options");
calendar.auth = {
user: calendar.user,
pass: calendar.pass
@@ -153,20 +157,19 @@ Module.register("calendar", {
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
});
// Refresh the DOM every minute if needed: When using relative date format for events that start
// or end in less than an hour, the date shows minute granularity and we want to keep that accurate.
setTimeout(
() => {
setInterval(() => {
this.updateDom(1);
}, ONE_MINUTE);
},
ONE_MINUTE - (new Date() % ONE_MINUTE)
);
// for backward compatibility titleReplace
if (typeof this.config.titleReplace !== "undefined") {
Log.warn("Deprecation warning: Please consider upgrading your calendar titleReplace configuration to customEvents.");
for (const [titlesearchstr, titlereplacestr] of Object.entries(this.config.titleReplace)) {
this.config.customEvents.push({ keyword: ".*", transform: { search: titlesearchstr, replace: titlereplacestr } });
}
}
this.selfUpdate();
},
// Override socket notification handler.
socketNotificationReceived: function (notification, payload) {
socketNotificationReceived (notification, payload) {
if (notification === "FETCH_CALENDAR") {
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
}
@@ -184,6 +187,18 @@ Module.register("calendar", {
if (this.config.broadcastEvents) {
this.broadcastEvents();
}
if (!this.config.updateOnFetch) {
if (this.calendarDisplayer[payload.url] === undefined) {
// calendar will never displayed, so display it
this.updateDom(this.config.animationSpeed);
// set this calendar as displayed
this.calendarDisplayer[payload.url] = true;
} else {
Log.debug("[Calendar] DOM not updated waiting self update()");
}
return;
}
}
} else if (notification === "CALENDAR_ERROR") {
let error_message = this.translate(payload.error_type);
@@ -195,7 +210,7 @@ Module.register("calendar", {
},
// Override dom generator.
getDom: function () {
getDom () {
const ONE_SECOND = 1000; // 1,000 milliseconds
const ONE_MINUTE = ONE_SECOND * 60;
const ONE_HOUR = ONE_MINUTE * 60;
@@ -321,11 +336,16 @@ Module.register("calendar", {
}
}
// Color events if custom color or eventClass are specified
var transformedTitle = event.title;
// Color events if custom color or eventClass are specified, transform title if required
if (this.config.customEvents.length > 0) {
for (let ev in this.config.customEvents) {
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
if (needle.test(event.title)) {
if (typeof this.config.customEvents[ev].transform === "object") {
transformedTitle = CalendarUtils.titleTransform(transformedTitle, [this.config.customEvents[ev].transform]);
}
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
// Respect parameter ColoredSymbolOnly also for custom events
if (this.config.coloredText) {
@@ -335,7 +355,6 @@ Module.register("calendar", {
if (this.config.displaySymbol && this.config.coloredSymbol) {
symbolWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
}
break;
}
if (typeof this.config.customEvents[ev].eventClass !== "undefined" && this.config.customEvents[ev].eventClass !== "") {
eventWrapper.className += ` ${this.config.customEvents[ev].eventClass}`;
@@ -344,7 +363,6 @@ Module.register("calendar", {
}
}
const transformedTitle = CalendarUtils.titleTransform(event.title, this.config.titleReplace);
titleWrapper.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines) + repeatingCountTitle;
const titleClass = this.titleClassForUrl(event.url);
@@ -534,7 +552,7 @@ Module.register("calendar", {
* @param {string} url The calendar url
* @returns {boolean} True if the calendar config contains the url, False otherwise
*/
hasCalendarURL: function (url) {
hasCalendarURL (url) {
for (const calendar of this.config.calendars) {
if (calendar.url === url) {
return true;
@@ -549,7 +567,7 @@ Module.register("calendar", {
* @param {boolean} limitNumberOfEntries Whether to filter returned events for display.
* @returns {object[]} Array with events.
*/
createEventList: function (limitNumberOfEntries) {
createEventList (limitNumberOfEntries) {
const ONE_SECOND = 1000; // 1,000 milliseconds
const ONE_MINUTE = ONE_SECOND * 60;
const ONE_HOUR = ONE_MINUTE * 60;
@@ -599,7 +617,12 @@ Module.register("calendar", {
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / ONE_DAY) + 1;
if (this.config.sliceMultiDayEvents && maxCount > 1) {
const splitEvents = [];
let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
let midnight
= moment(event.startDate, "x")
.clone()
.startOf("day")
.add(1, "day")
.format("x");
let count = 1;
while (event.endDate > midnight) {
const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
@@ -668,7 +691,7 @@ Module.register("calendar", {
return events.slice(0, this.config.maximumEntries);
},
listContainsEvent: function (eventList, event) {
listContainsEvent (eventList, event) {
for (const evt of eventList) {
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate) && parseInt(evt.endDate) === parseInt(event.endDate)) {
return true;
@@ -683,7 +706,7 @@ Module.register("calendar", {
* @param {object} auth The authentication method and credentials
* @param {object} calendarConfig The config of the specific calendar
*/
addCalendar: function (url, auth, calendarConfig) {
addCalendar (url, auth, calendarConfig) {
this.sendSocketNotification("ADD_CALENDAR", {
id: this.identifier,
url: url,
@@ -706,7 +729,7 @@ Module.register("calendar", {
* @param {object} event Event to look for.
* @returns {string[]} The symbols
*/
symbolsForEvent: function (event) {
symbolsForEvent (event) {
let symbols = this.getCalendarPropertyAsArray(event.url, "symbol", this.config.defaultSymbol);
if (event.recurringEvent === true && this.hasCalendarProperty(event.url, "recurringSymbol")) {
@@ -733,7 +756,7 @@ Module.register("calendar", {
return symbols;
},
mergeUnique: function (arr1, arr2) {
mergeUnique (arr1, arr2) {
return arr1.concat(
arr2.filter(function (item) {
return arr1.indexOf(item) === -1;
@@ -746,7 +769,7 @@ Module.register("calendar", {
* @param {string} url The calendar url
* @returns {string} The class to be used for the symbols of the calendar
*/
symbolClassForUrl: function (url) {
symbolClassForUrl (url) {
return this.getCalendarProperty(url, "symbolClass", "");
},
@@ -755,7 +778,7 @@ Module.register("calendar", {
* @param {string} url The calendar url
* @returns {string} The class to be used for the title of the calendar
*/
titleClassForUrl: function (url) {
titleClassForUrl (url) {
return this.getCalendarProperty(url, "titleClass", "");
},
@@ -764,7 +787,7 @@ Module.register("calendar", {
* @param {string} url The calendar url
* @returns {string} The class to be used for the time of the calendar
*/
timeClassForUrl: function (url) {
timeClassForUrl (url) {
return this.getCalendarProperty(url, "timeClass", "");
},
@@ -773,7 +796,7 @@ Module.register("calendar", {
* @param {string} url The calendar url
* @returns {string} The name of the calendar
*/
calendarNameForUrl: function (url) {
calendarNameForUrl (url) {
return this.getCalendarProperty(url, "name", "");
},
@@ -783,7 +806,7 @@ Module.register("calendar", {
* @param {boolean} isBg Determines if we fetch the bgColor or not
* @returns {string} The color
*/
colorForUrl: function (url, isBg) {
colorForUrl (url, isBg) {
return this.getCalendarProperty(url, isBg ? "bgColor" : "color", "#fff");
},
@@ -792,7 +815,7 @@ Module.register("calendar", {
* @param {string} url The calendar url
* @returns {string} The title
*/
countTitleForUrl: function (url) {
countTitleForUrl (url) {
return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
},
@@ -801,7 +824,7 @@ Module.register("calendar", {
* @param {string} url The calendar url
* @returns {number} The maximum entry count
*/
maximumEntriesForUrl: function (url) {
maximumEntriesForUrl (url) {
return this.getCalendarProperty(url, "maximumEntries", this.config.maximumEntries);
},
@@ -810,7 +833,7 @@ Module.register("calendar", {
* @param {string} url The calendar url
* @returns {number} The maximum past days count
*/
maximumPastDaysForUrl: function (url) {
maximumPastDaysForUrl (url) {
return this.getCalendarProperty(url, "pastDaysCount", this.config.pastDaysCount);
},
@@ -821,7 +844,7 @@ Module.register("calendar", {
* @param {string} defaultValue The value if the property is not found
* @returns {*} The property
*/
getCalendarProperty: function (url, property, defaultValue) {
getCalendarProperty (url, property, defaultValue) {
for (const calendar of this.config.calendars) {
if (calendar.url === url && calendar.hasOwnProperty(property)) {
return calendar[property];
@@ -831,7 +854,7 @@ Module.register("calendar", {
return defaultValue;
},
getCalendarPropertyAsArray: function (url, property, defaultValue) {
getCalendarPropertyAsArray (url, property, defaultValue) {
let p = this.getCalendarProperty(url, property, defaultValue);
if (property === "symbol" || property === "recurringSymbol" || property === "fullDaySymbol") {
const className = this.getCalendarProperty(url, "symbolClassName", this.config.defaultSymbolClassName);
@@ -842,7 +865,7 @@ Module.register("calendar", {
return p;
},
hasCalendarProperty: function (url, property) {
hasCalendarProperty (url, property) {
return !!this.getCalendarProperty(url, property, undefined);
},
@@ -850,7 +873,7 @@ Module.register("calendar", {
* Broadcasts the events to all other modules for reuse.
* The all events available in one array, sorted on startdate.
*/
broadcastEvents: function () {
broadcastEvents () {
const eventList = this.createEventList(false);
for (const event of eventList) {
event.symbol = this.symbolsForEvent(event);
@@ -860,5 +883,30 @@ Module.register("calendar", {
}
this.sendNotification("CALENDAR_EVENTS", eventList);
},
/**
* Refresh the DOM every minute if needed: When using relative date format for events that start
* or end in less than an hour, the date shows minute granularity and we want to keep that accurate.
* --
* When updateOnFetch is not set, it will Avoid fade out/in on updateDom when many calendars are used
* and it's allow to refresh The DOM every minute with animation speed too
* (because updateDom is not set in CALENDAR_EVENTS for this case)
*/
selfUpdate () {
const ONE_MINUTE = 60 * 1000;
setTimeout(
() => {
setInterval(() => {
Log.debug("[Calendar] self update");
if (this.config.updateOnFetch) {
this.updateDom(1);
} else {
this.updateDom(this.config.animationSpeed);
}
}, ONE_MINUTE);
},
ONE_MINUTE - (new Date() % ONE_MINUTE)
);
}
});

View File

@@ -10,10 +10,12 @@
*/
const path = require("path");
const moment = require("moment");
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
const Log = require("../../../js/logger");
const CalendarFetcherUtils = {
/**
* Calculate the time correction, either dst/std or full day in cases where
* utc time is day before plus offset
@@ -21,7 +23,7 @@ const CalendarFetcherUtils = {
* @param {Date} date the date on which this event happens
* @returns {number} the necessary adjustment in hours
*/
calculateTimezoneAdjustment: function (event, date) {
calculateTimezoneAdjustment (event, date) {
let adjustHours = 0;
// if a timezone was specified
if (!event.start.tz) {
@@ -122,7 +124,7 @@ const CalendarFetcherUtils = {
* @param {object} config The configuration object
* @returns {string[]} the filtered events
*/
filterEvents: function (data, config) {
filterEvents (data, config) {
const newEvents = [];
// limitFunction doesn't do much limiting, see comment re: the dates
@@ -141,7 +143,12 @@ const CalendarFetcherUtils = {
Log.debug("Processing entry...");
const now = new Date();
const today = moment().startOf("day").toDate();
const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
const future
= moment()
.startOf("day")
.add(config.maximumNumberOfDays, "days")
.subtract(1, "seconds") // Subtract 1 second so that events that start on the middle of the night will not repeat.
.toDate();
let past = today;
if (config.includePastEvents) {
@@ -247,7 +254,6 @@ const CalendarFetcherUtils = {
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
const rule = event.rrule;
let addedEvents = 0;
const pastMoment = moment(past);
const futureMoment = moment(future);
@@ -283,8 +289,12 @@ const CalendarFetcherUtils = {
futureLocal = futureMoment.toDate(); // future
}
Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
let dates = rule.between(pastLocal, futureLocal, true, limitFunction);
Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
dates = dates.filter((d) => {
if (JSON.stringify(d) === "null") return false;
else return true;
});
// The "dates" array contains the set of dates within our desired date range range that are valid
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
// had its date changed from outside the range to inside the range. For the time being,
@@ -305,11 +315,6 @@ const CalendarFetcherUtils = {
// Loop through the set of date entries to see which recurrences should be added to our event list.
for (let d in dates) {
let date = dates[d];
// Remove the time information of each date by using its substring, using the following method:
// .toISOString().substring(0,10).
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
const dateKey = date.toISOString().substring(0, 10);
let curEvent = event;
let showRecurrence = true;
@@ -402,6 +407,13 @@ const CalendarFetcherUtils = {
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date);
// Remove the time information of each date by using its substring, using the following method:
// .toISOString().substring(0,10).
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
// This must be done after `date` is adjusted
const dateKey = date.toISOString().substring(0, 10);
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
@@ -435,7 +447,6 @@ const CalendarFetcherUtils = {
if (showRecurrence === true) {
Log.debug(`saving event: ${description}`);
addedEvents++;
newEvents.push({
title: recurrenceTitle,
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
@@ -516,7 +527,7 @@ const CalendarFetcherUtils = {
* @param {string} msTZName the timezone name to lookup
* @returns {string|null} the iana name or null of none is found
*/
getIanaTZFromMS: function (msTZName) {
getIanaTZFromMS (msTZName) {
// Get hash entry
const he = zoneTable[msTZName];
// If found return iana name, else null
@@ -528,7 +539,7 @@ const CalendarFetcherUtils = {
* @param {object} event The event object to check.
* @returns {string} The title of the event, or "Event" if no title is found.
*/
getTitleFromEvent: function (event) {
getTitleFromEvent (event) {
let title = "Event";
if (event.summary) {
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
@@ -544,7 +555,7 @@ const CalendarFetcherUtils = {
* @param {object} event The event object to check.
* @returns {boolean} True if the event is a fullday event, false otherwise
*/
isFullDayEvent: function (event) {
isFullDayEvent (event) {
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
return true;
}
@@ -567,7 +578,7 @@ const CalendarFetcherUtils = {
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
* @returns {boolean} True if the event should be filtered out, false otherwise
*/
timeFilterApplies: function (now, endDate, filter) {
timeFilterApplies (now, endDate, filter) {
if (filter) {
const until = filter.split(" "),
value = parseInt(until[0]),
@@ -588,7 +599,7 @@ const CalendarFetcherUtils = {
* @param {string} regexFlags flags that should be applied to the regex
* @returns {boolean} True if the title should be filtered out, false otherwise
*/
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
titleFilterApplies (title, filter, useRegex, regexFlags) {
if (useRegex) {
let regexFilter = filter;
// Assume if leading slash, there is also trailing slash

View File

@@ -5,12 +5,13 @@
* MIT Licensed.
*/
const CalendarUtils = {
/**
* Capitalize the first letter of a string
* @param {string} string The string to capitalize
* @returns {string} The capitalized string
*/
capFirst: function (string) {
capFirst (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
@@ -21,7 +22,7 @@ const CalendarUtils = {
* @param {number} timeFormat Specifies either 12 or 24-hour time format
* @returns {moment.LocaleSpecification} formatted time
*/
getLocaleSpecification: function (timeFormat) {
getLocaleSpecification (timeFormat) {
switch (timeFormat) {
case 12: {
return { longDateFormat: { LT: "h:mm A" } };
@@ -43,7 +44,7 @@ const CalendarUtils = {
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
* @returns {string} The shortened string
*/
shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
shorten (string, maxLength, wrapEvents, maxTitleLines) {
if (typeof string !== "string") {
return "";
}
@@ -90,23 +91,39 @@ const CalendarUtils = {
/**
* Transforms the title of an event for usage.
* Replaces parts of the text as defined in config.titleReplace.
* Shortens title based on config.maxTitleLength and config.wrapEvents
* @param {string} title The title to transform.
* @param {object} titleReplace Pairs of strings to be replaced in the title
* @param {object} titleReplace object definition of parts to be replaced in the title
* object definition:
* search: {string,required} RegEx in format //x or simple string to be searched. For (birthday) year calcluation, the element matching the year must be in a RegEx group
* replace: {string,required} Replacement string, may contain match group references (latter is required for year calculation)
* yearmatchgroup: {number,optional} match group for year element
* @returns {string} The transformed title.
*/
titleTransform: function (title, titleReplace) {
titleTransform (title, titleReplace) {
let transformedTitle = title;
for (let needle in titleReplace) {
const replacement = titleReplace[needle];
for (let tr in titleReplace) {
let transform = titleReplace[tr];
if (typeof transform === "object") {
if (typeof transform.search !== "undefined" && transform.search !== "" && typeof transform.replace !== "undefined") {
let regParts = transform.search.match(/^\/(.+)\/([gim]*)$/);
let needle = new RegExp(transform.search, "g");
if (regParts) {
// the parsed pattern is a regexp with flags.
needle = new RegExp(regParts[1], regParts[2]);
}
const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
if (regParts) {
// the parsed pattern is a regexp.
needle = new RegExp(regParts[1], regParts[2]);
let replacement = transform.replace;
if (typeof transform.yearmatchgroup !== "undefined" && transform.yearmatchgroup !== "") {
const yearmatch = [...title.matchAll(needle)];
if (yearmatch[0].length >= transform.yearmatchgroup + 1 && yearmatch[0][transform.yearmatchgroup] * 1 >= 1900) {
let calcage = new Date().getFullYear() - yearmatch[0][transform.yearmatchgroup] * 1;
let searchstr = `$${transform.yearmatchgroup}`;
replacement = replacement.replace(searchstr, calcage);
}
}
transformedTitle = transformedTitle.replace(needle, replacement);
}
}
transformedTitle = transformedTitle.replace(needle, replacement);
}
return transformedTitle;
}

View File

@@ -10,13 +10,13 @@ const CalendarFetcher = require("./calendarfetcher");
module.exports = NodeHelper.create({
// Override start method.
start: function () {
start () {
Log.log(`Starting node helper for: ${this.name}`);
this.fetchers = [];
},
// Override socketNotificationReceived method.
socketNotificationReceived: function (notification, payload) {
socketNotificationReceived (notification, payload) {
if (notification === "ADD_CALENDAR") {
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.selfSignedCert, payload.id);
} else if (notification === "FETCH_CALENDAR") {
@@ -43,7 +43,7 @@ module.exports = NodeHelper.create({
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
* @param {string} identifier ID of the module
*/
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
createFetcher (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
try {
new URL(url);
} catch (error) {
@@ -85,7 +85,7 @@ module.exports = NodeHelper.create({
* @param {object} fetcher the fetcher associated with the calendar
* @param {string} identifier the identifier of the calendar
*/
broadcastEvents: function (fetcher, identifier) {
broadcastEvents (fetcher, identifier) {
this.sendSocketNotification("CALENDAR_EVENTS", {
id: identifier,
url: fetcher.url(),