Merge pull request #2376 from rejas/calendar_refactor

This commit is contained in:
Michael Teeuw 2021-03-14 12:55:24 +01:00 committed by GitHub
commit 30c7a24fc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 829 additions and 849 deletions

View File

@ -38,6 +38,7 @@ _This release is scheduled to be released on 2021-04-01._
- Cleaned up jsdoc and tests. - Cleaned up jsdoc and tests.
- Exposed logger as node module for easier access for 3rd party modules - Exposed logger as node module for easier access for 3rd party modules
- Replaced deprecated `request` package with `node-fetch` and `digest-fetch` - Replaced deprecated `request` package with `node-fetch` and `digest-fetch`
- Refactored calendar fetcher
### Removed ### Removed
@ -96,7 +97,7 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
### Deleted ### Deleted
- Removed Travis CI intergration. - Removed Travis CI integration.
### Fixed ### Fixed
@ -113,8 +114,8 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
- Fix non-fullday recurring rule processing. (#2216) - Fix non-fullday recurring rule processing. (#2216)
- Catch errors when parsing calendar data with ical. (#2022) - Catch errors when parsing calendar data with ical. (#2022)
- Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228) - Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228)
- Weather module - Always displays night icons when local is other then English. (#2221) - Weather module - Always displays night icons when local is other than English. (#2221)
- Update Node-ical 0.12.4 , fix invalid RRULE format in cal entries - Update node-ical 0.12.4, fix invalid RRULE format in cal entries
- Fix package.json for optional electron dependency (2378) - Fix package.json for optional electron dependency (2378)
- Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379) - Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379)
- Remove undefined objects from modules array (#2382) - Remove undefined objects from modules array (#2382)
@ -129,11 +130,11 @@ Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura
### Added ### Added
- `--dry-run` option adde in fetch call within updatenotification node_helper. This is to prevent - `--dry-run` Added option in fetch call within updatenotification node_helper. This is to prevent
MagicMirror from consuming any fetch result. Causes conflict with MMPM when attempting to check MagicMirror from consuming any fetch result. Causes conflict with MMPM when attempting to check
for updates to MagicMirror and/or MagicMirror modules. for updates to MagicMirror and/or MagicMirror modules.
- Test coverage with Istanbul, run it with `npm run test:coverage`. - Test coverage with Istanbul, run it with `npm run test:coverage`.
- Add lithuanian language. - Added lithuanian language.
- Added support in weatherforecast for OpenWeather onecall API. - Added support in weatherforecast for OpenWeather onecall API.
- Added config option to calendar-icons for recurring- and fullday-events. - Added config option to calendar-icons for recurring- and fullday-events.
- Added current, hourly (max 48), and daily (max 7) weather forecasts to weather module via OpenWeatherMap One Call API. - Added current, hourly (max 48), and daily (max 7) weather forecasts to weather module via OpenWeatherMap One Call API.
@ -197,7 +198,7 @@ Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryan
- Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MichMich/MagicMirror/issues/2018) - Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
- Throw error when check_config fails. [#1928](https://github.com/MichMich/MagicMirror/issues/1928) - Throw error when check_config fails. [#1928](https://github.com/MichMich/MagicMirror/issues/1928)
- Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MichMich/MagicMirror/issues/2050) - Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MichMich/MagicMirror/issues/2050)
- Updated ical library to latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926) - Updated ical library to the latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926)
- Fix config check after merge of prettier [#2109](https://github.com/MichMich/MagicMirror/issues/2109) - Fix config check after merge of prettier [#2109](https://github.com/MichMich/MagicMirror/issues/2109)
## [2.11.0] - 2020-04-01 ## [2.11.0] - 2020-04-01

View File

@ -77,7 +77,7 @@ Module.register("calendar", {
// Define required translations. // Define required translations.
getTranslations: function () { getTranslations: function () {
// The translations for the default modules are defined in the core translation files. // The translations for the default modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionary. // 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. // If you're trying to build your own module including translations, check out the documentation.
return false; return false;
}, },
@ -95,16 +95,16 @@ Module.register("calendar", {
// indicate no data available yet // indicate no data available yet
this.loaded = false; this.loaded = false;
for (var c in this.config.calendars) { this.config.calendars.forEach((calendar) => {
var calendar = this.config.calendars[c];
calendar.url = calendar.url.replace("webcal://", "http://"); calendar.url = calendar.url.replace("webcal://", "http://");
var calendarConfig = { const calendarConfig = {
maximumEntries: calendar.maximumEntries, maximumEntries: calendar.maximumEntries,
maximumNumberOfDays: calendar.maximumNumberOfDays, maximumNumberOfDays: calendar.maximumNumberOfDays,
broadcastPastEvents: calendar.broadcastPastEvents, broadcastPastEvents: calendar.broadcastPastEvents,
selfSignedCert: calendar.selfSignedCert selfSignedCert: calendar.selfSignedCert
}; };
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) { if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
calendarConfig.symbolClass = ""; calendarConfig.symbolClass = "";
} }
@ -128,7 +128,7 @@ Module.register("calendar", {
// tell helper to start a fetcher for this calendar // tell helper to start a fetcher for this calendar
// fetcher till cycle // fetcher till cycle
this.addCalendar(calendar.url, calendar.auth, calendarConfig); this.addCalendar(calendar.url, calendar.auth, calendarConfig);
} });
}, },
// Override socket notification handler. // Override socket notification handler.
@ -164,8 +164,8 @@ Module.register("calendar", {
const oneHour = oneMinute * 60; const oneHour = oneMinute * 60;
const oneDay = oneHour * 24; const oneDay = oneHour * 24;
var events = this.createEventList(); const events = this.createEventList();
var wrapper = document.createElement("table"); const wrapper = document.createElement("table");
wrapper.className = this.config.tableClass; wrapper.className = this.config.tableClass;
if (events.length === 0) { if (events.length === 0) {
@ -174,37 +174,37 @@ Module.register("calendar", {
return wrapper; return wrapper;
} }
let currentFadeStep = 0;
let startFade;
let fadeSteps;
if (this.config.fade && this.config.fadePoint < 1) { if (this.config.fade && this.config.fadePoint < 1) {
if (this.config.fadePoint < 0) { if (this.config.fadePoint < 0) {
this.config.fadePoint = 0; this.config.fadePoint = 0;
} }
var startFade = events.length * this.config.fadePoint; startFade = events.length * this.config.fadePoint;
var fadeSteps = events.length - startFade; fadeSteps = events.length - startFade;
} }
var currentFadeStep = 0; let lastSeenDate = "";
var lastSeenDate = "";
var ev;
var needle;
for (var e in events) { events.forEach((event, index) => {
var event = events[e]; const dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
var dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
if (this.config.timeFormat === "dateheaders") { if (this.config.timeFormat === "dateheaders") {
if (lastSeenDate !== dateAsString) { if (lastSeenDate !== dateAsString) {
var dateRow = document.createElement("tr"); const dateRow = document.createElement("tr");
dateRow.className = "normal"; dateRow.className = "normal";
var dateCell = document.createElement("td");
const dateCell = document.createElement("td");
dateCell.colSpan = "3"; dateCell.colSpan = "3";
dateCell.innerHTML = dateAsString; dateCell.innerHTML = dateAsString;
dateCell.style.paddingTop = "10px"; dateCell.style.paddingTop = "10px";
dateRow.appendChild(dateCell); dateRow.appendChild(dateCell);
wrapper.appendChild(dateRow); wrapper.appendChild(dateRow);
if (e >= startFade) { if (this.config.fade && index >= startFade) {
//fading //fading
currentFadeStep = e - startFade; currentFadeStep = index - startFade;
dateRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep; dateRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
} }
@ -212,7 +212,7 @@ Module.register("calendar", {
} }
} }
var eventWrapper = document.createElement("tr"); const eventWrapper = document.createElement("tr");
if (this.config.colored && !this.config.coloredSymbolOnly) { if (this.config.colored && !this.config.coloredSymbolOnly) {
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url); eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
@ -220,22 +220,22 @@ Module.register("calendar", {
eventWrapper.className = "normal event"; eventWrapper.className = "normal event";
if (this.config.displaySymbol) { const symbolWrapper = document.createElement("td");
var symbolWrapper = document.createElement("td");
if (this.config.displaySymbol) {
if (this.config.colored && this.config.coloredSymbolOnly) { if (this.config.colored && this.config.coloredSymbolOnly) {
symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url); symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
} }
var symbolClass = this.symbolClassForUrl(event.url); const symbolClass = this.symbolClassForUrl(event.url);
symbolWrapper.className = "symbol align-right " + symbolClass; symbolWrapper.className = "symbol align-right " + symbolClass;
var symbols = this.symbolsForEvent(event); const symbols = this.symbolsForEvent(event);
// If symbols are displayed and custom symbol is set, replace event symbol // If symbols are displayed and custom symbol is set, replace event symbol
if (this.config.displaySymbol && this.config.customEvents.length > 0) { if (this.config.displaySymbol && this.config.customEvents.length > 0) {
for (ev in this.config.customEvents) { for (let ev in this.config.customEvents) {
if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") { if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") {
needle = new RegExp(this.config.customEvents[ev].keyword, "gi"); let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
if (needle.test(event.title)) { if (needle.test(event.title)) {
symbols[0] = this.config.customEvents[ev].symbol; symbols[0] = this.config.customEvents[ev].symbol;
break; break;
@ -243,31 +243,29 @@ Module.register("calendar", {
} }
} }
} }
symbols.forEach((s, index) => {
for (var i = 0; i < symbols.length; i++) { const symbol = document.createElement("span");
var symbol = document.createElement("span"); symbol.className = "fa fa-fw fa-" + s;
symbol.className = "fa fa-fw fa-" + symbols[i]; if (index > 0) {
if (i > 0) {
symbol.style.paddingLeft = "5px"; symbol.style.paddingLeft = "5px";
} }
symbolWrapper.appendChild(symbol); symbolWrapper.appendChild(symbol);
} });
eventWrapper.appendChild(symbolWrapper); eventWrapper.appendChild(symbolWrapper);
} else if (this.config.timeFormat === "dateheaders") { } else if (this.config.timeFormat === "dateheaders") {
var blankCell = document.createElement("td"); const blankCell = document.createElement("td");
blankCell.innerHTML = "&nbsp;&nbsp;&nbsp;"; blankCell.innerHTML = "&nbsp;&nbsp;&nbsp;";
eventWrapper.appendChild(blankCell); eventWrapper.appendChild(blankCell);
} }
var titleWrapper = document.createElement("td"), const titleWrapper = document.createElement("td");
repeatingCountTitle = ""; let repeatingCountTitle = "";
if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) { if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) {
repeatingCountTitle = this.countTitleForUrl(event.url); repeatingCountTitle = this.countTitleForUrl(event.url);
if (repeatingCountTitle !== "") { if (repeatingCountTitle !== "") {
var thisYear = new Date(parseInt(event.startDate)).getFullYear(), const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
yearDiff = thisYear - event.firstYear; yearDiff = thisYear - event.firstYear;
repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle; repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
@ -276,9 +274,9 @@ Module.register("calendar", {
// Color events if custom color is specified // Color events if custom color is specified
if (this.config.customEvents.length > 0) { if (this.config.customEvents.length > 0) {
for (ev in this.config.customEvents) { for (let ev in this.config.customEvents) {
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") { if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
needle = new RegExp(this.config.customEvents[ev].keyword, "gi"); let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
if (needle.test(event.title)) { if (needle.test(event.title)) {
// Respect parameter ColoredSymbolOnly also for custom events // Respect parameter ColoredSymbolOnly also for custom events
if (!this.config.coloredSymbolOnly) { if (!this.config.coloredSymbolOnly) {
@ -296,7 +294,7 @@ Module.register("calendar", {
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle; titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
var titleClass = this.titleClassForUrl(event.url); const titleClass = this.titleClassForUrl(event.url);
if (!this.config.colored) { if (!this.config.colored) {
titleWrapper.className = "title bright " + titleClass; titleWrapper.className = "title bright " + titleClass;
@ -304,14 +302,12 @@ Module.register("calendar", {
titleWrapper.className = "title " + titleClass; titleWrapper.className = "title " + titleClass;
} }
var timeWrapper;
if (this.config.timeFormat === "dateheaders") { if (this.config.timeFormat === "dateheaders") {
if (event.fullDayEvent) { if (event.fullDayEvent) {
titleWrapper.colSpan = "2"; titleWrapper.colSpan = "2";
titleWrapper.align = "left"; titleWrapper.align = "left";
} else { } else {
timeWrapper = document.createElement("td"); const timeWrapper = document.createElement("td");
timeWrapper.className = "time light " + this.timeClassForUrl(event.url); timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
timeWrapper.align = "left"; timeWrapper.align = "left";
timeWrapper.style.paddingLeft = "2px"; timeWrapper.style.paddingLeft = "2px";
@ -322,10 +318,10 @@ Module.register("calendar", {
eventWrapper.appendChild(titleWrapper); eventWrapper.appendChild(titleWrapper);
} else { } else {
timeWrapper = document.createElement("td"); const timeWrapper = document.createElement("td");
eventWrapper.appendChild(titleWrapper); eventWrapper.appendChild(titleWrapper);
var now = new Date(); const now = new Date();
if (this.config.timeFormat === "absolute") { if (this.config.timeFormat === "absolute") {
// Use dateFormat // Use dateFormat
@ -401,22 +397,22 @@ Module.register("calendar", {
wrapper.appendChild(eventWrapper); wrapper.appendChild(eventWrapper);
// Create fade effect. // Create fade effect.
if (e >= startFade) { if (index >= startFade) {
currentFadeStep = e - startFade; currentFadeStep = index - startFade;
eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep; eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
} }
if (this.config.showLocation) { if (this.config.showLocation) {
if (event.location !== false) { if (event.location !== false) {
var locationRow = document.createElement("tr"); const locationRow = document.createElement("tr");
locationRow.className = "normal xsmall light"; locationRow.className = "normal xsmall light";
if (this.config.displaySymbol) { if (this.config.displaySymbol) {
var symbolCell = document.createElement("td"); const symbolCell = document.createElement("td");
locationRow.appendChild(symbolCell); locationRow.appendChild(symbolCell);
} }
var descCell = document.createElement("td"); const descCell = document.createElement("td");
descCell.className = "location"; descCell.className = "location";
descCell.colSpan = "2"; descCell.colSpan = "2";
descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines); descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines);
@ -424,13 +420,13 @@ Module.register("calendar", {
wrapper.appendChild(locationRow); wrapper.appendChild(locationRow);
if (e >= startFade) { if (index >= startFade) {
currentFadeStep = e - startFade; currentFadeStep = index - startFade;
locationRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep; locationRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
} }
} }
} }
} });
return wrapper; return wrapper;
}, },
@ -464,8 +460,7 @@ Module.register("calendar", {
* @returns {boolean} True if the calendar config contains the url, False otherwise * @returns {boolean} True if the calendar config contains the url, False otherwise
*/ */
hasCalendarURL: function (url) { hasCalendarURL: function (url) {
for (var c in this.config.calendars) { for (const calendar of this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url) { if (calendar.url === url) {
return true; return true;
} }
@ -480,14 +475,16 @@ Module.register("calendar", {
* @returns {object[]} Array with events. * @returns {object[]} Array with events.
*/ */
createEventList: function () { createEventList: function () {
var events = []; const now = new Date();
var today = moment().startOf("day"); const today = moment().startOf("day");
var now = new Date(); const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
var future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate(); let events = [];
for (var c in this.calendarData) {
var calendar = this.calendarData[c]; for (const calendarUrl in this.calendarData) {
for (var e in calendar) { const calendar = this.calendarData[calendarUrl];
var event = JSON.parse(JSON.stringify(calendar[e])); // clone object console.log(calendar);
for (const e in calendar) {
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
if (event.endDate < now) { if (event.endDate < now) {
continue; continue;
@ -506,19 +503,19 @@ Module.register("calendar", {
if (this.listContainsEvent(events, event)) { if (this.listContainsEvent(events, event)) {
continue; continue;
} }
event.url = c; event.url = calendarUrl;
event.today = event.startDate >= today && event.startDate < today + 24 * 60 * 60 * 1000; event.today = event.startDate >= today && event.startDate < today + 24 * 60 * 60 * 1000;
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days, /* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
* otherwise, esp. in dateheaders mode it is not clear how long these events are. * otherwise, esp. in dateheaders mode it is not clear how long these events are.
*/ */
var maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / (1000 * 60 * 60 * 24)) + 1; const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / (1000 * 60 * 60 * 24)) + 1;
if (this.config.sliceMultiDayEvents && maxCount > 1) { if (this.config.sliceMultiDayEvents && maxCount > 1) {
var splitEvents = []; const splitEvents = [];
var 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");
var count = 1; let count = 1;
while (event.endDate > midnight) { while (event.endDate > midnight) {
var thisEvent = JSON.parse(JSON.stringify(event)); // clone object const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + 24 * 60 * 60 * 1000; thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + 24 * 60 * 60 * 1000;
thisEvent.endDate = midnight; thisEvent.endDate = midnight;
thisEvent.title += " (" + count + "/" + maxCount + ")"; thisEvent.title += " (" + count + "/" + maxCount + ")";
@ -532,9 +529,9 @@ Module.register("calendar", {
event.title += " (" + count + "/" + maxCount + ")"; event.title += " (" + count + "/" + maxCount + ")";
splitEvents.push(event); splitEvents.push(event);
for (event of splitEvents) { for (let splitEvent of splitEvents) {
if (event.endDate > now && event.endDate <= future) { if (splitEvent.endDate > now && splitEvent.endDate <= future) {
events.push(event); events.push(splitEvent);
} }
} }
} else { } else {
@ -550,12 +547,11 @@ Module.register("calendar", {
// Limit the number of days displayed // Limit the number of days displayed
// If limitDays is set > 0, limit display to that number of days // If limitDays is set > 0, limit display to that number of days
if (this.config.limitDays > 0) { if (this.config.limitDays > 0) {
var newEvents = []; let newEvents = [];
var lastDate = today.clone().subtract(1, "days").format("YYYYMMDD"); let lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
var days = 0; let days = 0;
var eventDate; for (const ev of events) {
for (var ev of events) { let eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
// if date of event is later than lastdate // if date of event is later than lastdate
// check if we already are showing max unique days // check if we already are showing max unique days
if (eventDate > lastDate) { if (eventDate > lastDate) {
@ -579,7 +575,7 @@ Module.register("calendar", {
}, },
listContainsEvent: function (eventList, event) { listContainsEvent: function (eventList, event) {
for (var evt of eventList) { for (const evt of eventList) {
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) { if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) {
return true; return true;
} }
@ -595,8 +591,6 @@ Module.register("calendar", {
* @param {object} calendarConfig The config of the specific calendar * @param {object} calendarConfig The config of the specific calendar
*/ */
addCalendar: function (url, auth, calendarConfig) { addCalendar: function (url, auth, calendarConfig) {
var self = this;
this.sendSocketNotification("ADD_CALENDAR", { this.sendSocketNotification("ADD_CALENDAR", {
id: this.identifier, id: this.identifier,
url: url, url: url,
@ -710,8 +704,7 @@ Module.register("calendar", {
* @returns {*} The property * @returns {*} The property
*/ */
getCalendarProperty: function (url, property, defaultValue) { getCalendarProperty: function (url, property, defaultValue) {
for (var c in this.config.calendars) { for (const calendar of this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url && calendar.hasOwnProperty(property)) { if (calendar.url === url && calendar.hasOwnProperty(property)) {
return calendar[property]; return calendar[property];
} }
@ -745,13 +738,13 @@ Module.register("calendar", {
} }
if (wrapEvents === true) { if (wrapEvents === true) {
var temp = ""; const words = string.split(" ");
var currentLine = ""; let temp = "";
var words = string.split(" "); let currentLine = "";
var line = 0; let line = 0;
for (var i = 0; i < words.length; i++) { for (let i = 0; i < words.length; i++) {
var word = words[i]; const word = words[i];
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
// max - 1 to account for a space // max - 1 to account for a space
currentLine += word + " "; currentLine += word + " ";
@ -806,10 +799,10 @@ Module.register("calendar", {
* @returns {string} The transformed title. * @returns {string} The transformed title.
*/ */
titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) { titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
for (var needle in titleReplace) { for (let needle in titleReplace) {
var replacement = titleReplace[needle]; const replacement = titleReplace[needle];
var regParts = needle.match(/^\/(.+)\/([gim]*)$/); const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
if (regParts) { if (regParts) {
// the parsed pattern is a regexp. // the parsed pattern is a regexp.
needle = new RegExp(regParts[1], regParts[2]); needle = new RegExp(regParts[1], regParts[2]);
@ -827,11 +820,10 @@ Module.register("calendar", {
* The all events available in one array, sorted on startdate. * The all events available in one array, sorted on startdate.
*/ */
broadcastEvents: function () { broadcastEvents: function () {
var eventList = []; const eventList = [];
for (var url in this.calendarData) { for (const url in this.calendarData) {
var calendar = this.calendarData[url]; for (const ev of this.calendarData[url]) {
for (var e in calendar) { const event = cloneObject(ev);
var event = cloneObject(calendar[e]);
event.symbol = this.symbolsForEvent(event); event.symbol = this.symbolsForEvent(event);
event.calendarName = this.calendarNameForUrl(url); event.calendarName = this.calendarNameForUrl(url);
event.color = this.colorForUrl(url); event.color = this.colorForUrl(url);

View File

@ -4,6 +4,7 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
const CalendarUtils = require("./calendarutils");
const Log = require("logger"); const Log = require("logger");
const ical = require("node-ical"); const ical = require("node-ical");
const fetch = require("node-fetch"); const fetch = require("node-fetch");
@ -11,14 +12,6 @@ const digest = require("digest-fetch");
const https = require("https"); const https = require("https");
const base64 = require("base-64"); const base64 = require("base-64");
/**
* Moment date
*
* @external Moment
* @see {@link http://momentjs.com}
*/
const moment = require("moment");
/** /**
* *
* @param {string} url The url of the calendar to fetch * @param {string} url The url of the calendar to fetch
@ -32,8 +25,6 @@ const moment = require("moment");
* @class * @class
*/ */
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents, selfSignedCert) { const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents, selfSignedCert) {
const self = this;
let reloadTimer = null; let reloadTimer = null;
let events = []; let events = [];
@ -43,7 +34,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/** /**
* Initiates calendar fetch. * Initiates calendar fetch.
*/ */
const fetchCalendar = function () { const fetchCalendar = () => {
clearTimeout(reloadTimer); clearTimeout(reloadTimer);
reloadTimer = null; reloadTimer = null;
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
@ -73,12 +64,12 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
fetcher fetcher
.catch((error) => { .catch((error) => {
fetchFailedCallback(self, error); fetchFailedCallback(this, error);
scheduleTimer(); scheduleTimer();
}) })
.then((response) => { .then((response) => {
if (response.status !== 200) { if (response.status !== 200) {
fetchFailedCallback(self, response.statusText); fetchFailedCallback(this, response.statusText);
scheduleTimer(); scheduleTimer();
} }
return response; return response;
@ -89,452 +80,23 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
try { try {
data = ical.parseICS(responseData); data = ical.parseICS(responseData);
Log.debug("parsed data=" + JSON.stringify(data));
events = CalendarUtils.filterEvents(data, {
excludedEvents,
includePastEvents,
maximumEntries,
maximumNumberOfDays
});
} catch (error) { } catch (error) {
fetchFailedCallback(self, error.message); fetchFailedCallback(self, error.message);
scheduleTimer(); scheduleTimer();
}
Log.debug(" parsed data=" + JSON.stringify(data));
const newEvents = [];
// limitFunction doesn't do much limiting, see comment re: the dates array in rrule section below as to why we need to do the filtering ourselves
const limitFunction = function (date, i) {
return true;
};
const eventDate = function (event, time) {
return isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
};
Log.debug("there are " + Object.entries(data).length + " calendar entries");
Object.entries(data).forEach(([key, event]) => {
const now = new Date();
const today = moment().startOf("day").toDate();
const future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
let past = today;
Log.debug("have entries ");
if (includePastEvents) {
past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate();
}
// FIXME: Ugly fix to solve the facebook birthday issue.
// Otherwise, the recurring events only show the birthday for next year.
let isFacebookBirthday = false;
if (typeof event.uid !== "undefined") {
if (event.uid.indexOf("@facebook.com") !== -1) {
isFacebookBirthday = true;
}
}
if (event.type === "VEVENT") {
let startDate = eventDate(event, "start");
let endDate;
Log.debug("\nevent=" + JSON.stringify(event));
if (typeof event.end !== "undefined") {
endDate = eventDate(event, "end");
} else if (typeof event.duration !== "undefined") {
endDate = startDate.clone().add(moment.duration(event.duration));
} else {
if (!isFacebookBirthday) {
// make copy of start date, separate storage area
endDate = moment(startDate.format("x"), "x");
} else {
endDate = moment(startDate).add(1, "days");
}
}
Log.debug(" start=" + startDate.toDate() + " end=" + endDate.toDate());
// calculate the duration of the event for use with recurring events.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
if (event.start.length === 8) {
startDate = startDate.startOf("day");
}
const title = getTitleFromEvent(event);
let excluded = false,
dateFilter = null;
for (let f in excludedEvents) {
let filter = excludedEvents[f],
testTitle = title.toLowerCase(),
until = null,
useRegex = false,
regexFlags = "g";
if (filter instanceof Object) {
if (typeof filter.until !== "undefined") {
until = filter.until;
}
if (typeof filter.regex !== "undefined") {
useRegex = filter.regex;
}
// If additional advanced filtering is added in, this section
// must remain last as we overwrite the filter object with the
// filterBy string
if (filter.caseSensitive) {
filter = filter.filterBy;
testTitle = title;
} else if (useRegex) {
filter = filter.filterBy;
testTitle = title;
regexFlags += "i";
} else {
filter = filter.filterBy.toLowerCase();
}
} else {
filter = filter.toLowerCase();
}
if (testTitleByFilter(testTitle, filter, useRegex, regexFlags)) {
if (until) {
dateFilter = until;
} else {
excluded = true;
}
break;
}
}
if (excluded) {
return; return;
} }
this.broadcastEvents();
const location = event.location || false;
const geo = event.geo || false;
const description = event.description || false;
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
const rule = event.rrule;
let addedEvents = 0;
const pastMoment = moment(past);
const futureMoment = moment(future);
// can cause problems with e.g. birthdays before 1900
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
rule.origOptions.dtstart.setYear(1900);
rule.options.dtstart.setYear(1900);
}
// For recurring events, get the set of start dates that fall within the range
// of dates we're looking for.
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
let pastLocal = 0;
let futureLocal = 0;
if (isFullDayEvent(event)) {
// if full day event, only use the date part of the ranges
pastLocal = pastMoment.toDate();
futureLocal = futureMoment.toDate();
} else {
// if we want past events
if (includePastEvents) {
// use the calculated past time for the between from
pastLocal = pastMoment.toDate();
} else {
// otherwise use NOW.. cause we shouldnt use any before now
pastLocal = moment().toDate(); //now
}
futureLocal = futureMoment.toDate(); // future
}
Log.debug(" between=" + pastLocal + " to " + futureLocal);
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
Log.debug("title=" + event.summary + " dates=" + JSON.stringify(dates));
// 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,
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
// because the logic below will filter out any recurrences that don't actually belong within
// our display range.
// Would be great if there was a better way to handle this.
if (event.recurrences !== undefined) {
for (let r in event.recurrences) {
// Only add dates that weren't already in the range we added from the rrule so that
// we don"t double-add those events.
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
dates.push(new Date(r));
}
}
}
// 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];
// ical.js started returning recurrences and exdates as ISOStrings without time information.
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
// (see https://github.com/peterbraden/ical.js/pull/84 )
const dateKey = date.toISOString().substring(0, 10);
let curEvent = event;
let showRecurrence = true;
// for full day events, the time might be off from RRULE/Luxon problem
if (isFullDayEvent(event)) {
Log.debug("fullday");
// if the offset is negative, east of GMT where the problem is
if (date.getTimezoneOffset() < 0) {
// get the offset of today where we are processing
// this will be the correction we need to apply
let nowOffset = new Date().getTimezoneOffset();
Log.debug("now offset is " + nowOffset);
// reduce the time by the offset
Log.debug(" recurring date is " + date + " offset is " + date.getTimezoneOffset());
// apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
duration = 24 * 60 * 60 * 1000;
Log.debug("new recurring date is " + date);
}
}
startDate = moment(date);
let adjustDays = getCorrection(event, date);
// 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.
curEvent = curEvent.recurrences[dateKey];
startDate = moment(curEvent.start);
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
}
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
}
Log.debug("duration=" + duration);
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (startDate.format("x") === endDate.format("x")) {
endDate = endDate.endOf("day");
}
const recurrenceTitle = getTitleFromEvent(curEvent);
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
// it to the event list.
if (endDate.isBefore(past) || startDate.isAfter(future)) {
showRecurrence = false;
}
if (timeFilterApplies(now, endDate, dateFilter)) {
showRecurrence = false;
}
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"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: isFullDayEvent(event),
recurringEvent: true,
class: event.class,
firstYear: event.start.getFullYear(),
location: location,
geo: geo,
description: description
});
}
}
// end recurring event parsing
} else {
// Single event.
const fullDayEvent = isFacebookBirthday ? true : isFullDayEvent(event);
// Log.debug("full day event")
if (includePastEvents) {
// Past event is too far in the past, so skip.
if (endDate < past) {
return;
}
} else {
// It's not a fullday event, and it is in the past, so skip.
if (!fullDayEvent && endDate < new Date()) {
return;
}
// It's a fullday event, and it is before today, So skip.
if (fullDayEvent && endDate <= today) {
return;
}
}
// It exceeds the maximumNumberOfDays limit, so skip.
if (startDate > future) {
return;
}
if (timeFilterApplies(now, endDate, dateFilter)) {
return;
}
// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
if (fullDayEvent && startDate <= today) {
startDate = moment(today);
}
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
endDate = endDate.endOf("day");
}
// get correction for date saving and dst change between now and then
let adjustDays = getCorrection(event, startDate.toDate());
// Every thing is good. Add it to the list.
newEvents.push({
title: title,
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: fullDayEvent,
class: event.class,
location: location,
geo: geo,
description: description
});
}
}
});
newEvents.sort(function (a, b) {
return a.startDate - b.startDate;
});
// include up to maximumEntries current or upcoming events
// If past events should be included, include all past events
const now = moment();
var entries = 0;
events = [];
for (let ne of newEvents) {
if (moment(ne.endDate, "x").isBefore(now)) {
if (includePastEvents) events.push(ne);
continue;
}
entries++;
// If max events has been saved, skip the rest
if (entries > maximumEntries) break;
events.push(ne);
}
self.broadcastEvents();
scheduleTimer(); scheduleTimer();
}); });
}; };
/*
*
* get the time correction, either dst/std or full day in cases where utc time is day before plus offset
*
*/
const getCorrection = function (event, date) {
let adjustHours = 0;
// if a timezone was specified
if (!event.start.tz) {
Log.debug(" if no tz, guess based on now");
event.start.tz = moment.tz.guess();
}
Log.debug("initial tz=" + event.start.tz);
// if there is a start date specified
if (event.start.tz) {
// if this is a windows timezone
if (event.start.tz.includes(" ")) {
// use the lookup table to get theIANA name as moment and date don't know MS timezones
let tz = getIanaTZFromMS(event.start.tz);
Log.debug("corrected TZ=" + tz);
// watch out for unregistered windows timezone names
// if we had a successfule lookup
if (tz) {
// change the timezone to the IANA name
event.start.tz = tz;
// Log.debug("corrected timezone="+event.start.tz)
}
}
Log.debug("corrected tz=" + event.start.tz);
let current_offset = 0; // offset from TZ string or calculated
let mm = 0; // date with tz or offset
let start_offset = 0; // utc offset of created with tz
// if there is still an offset, lookup failed, use it
if (event.start.tz.startsWith("(")) {
const regex = /[+|-]\d*:\d*/;
const start_offsetString = event.start.tz.match(regex).toString().split(":");
let start_offset = parseInt(start_offsetString[0]);
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
adjustHours = start_offset;
Log.debug("defined offset=" + start_offset + " hours");
current_offset = start_offset;
event.start.tz = "";
Log.debug("ical offset=" + current_offset + " date=" + date);
mm = moment(date);
let x = parseInt(moment(new Date()).utcOffset());
Log.debug("net mins=" + (current_offset * 60 - x));
mm = mm.add(x - current_offset * 60, "minutes");
adjustHours = (current_offset * 60 - x) / 60;
event.start = mm.toDate();
Log.debug("adjusted date=" + event.start);
} else {
// get the start time in that timezone
Log.debug("start date/time=" + moment(event.start).toDate());
start_offset = moment.tz(moment(event.start), event.start.tz).utcOffset();
Log.debug("start offset=" + start_offset);
Log.debug("start date/time w tz =" + moment.tz(moment(event.start), event.start.tz).toDate());
// get the specified date in that timezone
mm = moment.tz(moment(date), event.start.tz);
Log.debug("event date=" + mm.toDate());
current_offset = mm.utcOffset();
}
Log.debug("event offset=" + current_offset + " hour=" + mm.format("H") + " event date=" + mm.toDate());
// if the offset is greater than 0, east of london
if (current_offset !== start_offset) {
// big offset
Log.debug("offset");
let h = parseInt(mm.format("H"));
// check if the event time is less than the offset
if (h > 0 && h < Math.abs(current_offset) / 60) {
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
// we need to fix that
adjustHours = 24;
// Log.debug("adjusting date")
}
//-300 > -240
//if (Math.abs(current_offset) > Math.abs(start_offset)){
if (current_offset > start_offset) {
adjustHours -= 1;
Log.debug("adjust down 1 hour dst change");
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
} else if (current_offset < start_offset) {
adjustHours += 1;
Log.debug("adjust up 1 hour dst change");
}
}
}
Log.debug("adjustHours=" + adjustHours);
return adjustHours;
};
/**
*
* lookup iana tz from windows
*/
let zoneTable = null;
const getIanaTZFromMS = function (msTZName) {
if (!zoneTable) {
const p = require("path");
zoneTable = require(p.join(__dirname, "windowsZones.json"));
}
// Get hash entry
const he = zoneTable[msTZName];
// If found return iana name, else null
return he ? he.iana[0] : null;
};
/** /**
* Schedule the timer for the next update. * Schedule the timer for the next update.
*/ */
@ -545,82 +107,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
}, reloadInterval); }, reloadInterval);
}; };
/**
* Checks if an event is a fullday event.
*
* @param {object} event The event object to check.
* @returns {boolean} True if the event is a fullday event, false otherwise
*/
const isFullDayEvent = function (event) {
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
return true;
}
const start = event.start || 0;
const startDate = new Date(start);
const end = event.end || 0;
if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
// Is 24 hours, and starts on the middle of the night.
return true;
}
return false;
};
/**
* Determines if the user defined time filter should apply
*
* @param {Date} now Date object using previously created object for consistency
* @param {Moment} endDate Moment object representing the event end date
* @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
*/
const timeFilterApplies = function (now, endDate, filter) {
if (filter) {
const until = filter.split(" "),
value = parseInt(until[0]),
increment = until[1].slice(-1) === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
filterUntil = moment(endDate.format()).subtract(value, increment);
return now < filterUntil.format("x");
}
return false;
};
/**
* Gets the title from the event.
*
* @param {object} event The event object to check.
* @returns {string} The title of the event, or "Event" if no title is found.
*/
const getTitleFromEvent = function (event) {
let title = "Event";
if (event.summary) {
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
} else if (event.description) {
title = event.description;
}
return title;
};
const testTitleByFilter = function (title, filter, useRegex, regexFlags) {
if (useRegex) {
// Assume if leading slash, there is also trailing slash
if (filter[0] === "/") {
// Strip leading and trailing slashes
filter = filter.substr(1).slice(0, -1);
}
filter = new RegExp(filter, regexFlags);
return filter.test(title);
} else {
return title.includes(filter);
}
};
/* public methods */ /* public methods */
/** /**
@ -635,7 +121,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
*/ */
this.broadcastEvents = function () { this.broadcastEvents = function () {
Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events."); Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events.");
eventsReceivedCallback(self); eventsReceivedCallback(this);
}; };
/** /**

View File

@ -0,0 +1,545 @@
/* Magic Mirror
* Calendar Util Methods
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
/**
* @external Moment
*/
const moment = require("moment");
const path = require("path");
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
const Log = require("../../../js/logger.js");
const CalendarUtils = {
/**
* Calculate the time correction, either dst/std or full day in cases where
* utc time is day before plus offset
*
* @param {object} event
* @param {Date} date
* @returns {number} the necessary adjustment in hours
*/
calculateTimezoneAdjustment: function (event, date) {
let adjustHours = 0;
// if a timezone was specified
if (!event.start.tz) {
Log.debug(" if no tz, guess based on now");
event.start.tz = moment.tz.guess();
}
Log.debug("initial tz=" + event.start.tz);
// if there is a start date specified
if (event.start.tz) {
// if this is a windows timezone
if (event.start.tz.includes(" ")) {
// use the lookup table to get theIANA name as moment and date don't know MS timezones
let tz = CalendarUtils.getIanaTZFromMS(event.start.tz);
Log.debug("corrected TZ=" + tz);
// watch out for unregistered windows timezone names
// if we had a successful lookup
if (tz) {
// change the timezone to the IANA name
event.start.tz = tz;
// Log.debug("corrected timezone="+event.start.tz)
}
}
Log.debug("corrected tz=" + event.start.tz);
let current_offset = 0; // offset from TZ string or calculated
let mm = 0; // date with tz or offset
let start_offset = 0; // utc offset of created with tz
// if there is still an offset, lookup failed, use it
if (event.start.tz.startsWith("(")) {
const regex = /[+|-]\d*:\d*/;
const start_offsetString = event.start.tz.match(regex).toString().split(":");
let start_offset = parseInt(start_offsetString[0]);
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
adjustHours = start_offset;
Log.debug("defined offset=" + start_offset + " hours");
current_offset = start_offset;
event.start.tz = "";
Log.debug("ical offset=" + current_offset + " date=" + date);
mm = moment(date);
let x = parseInt(moment(new Date()).utcOffset());
Log.debug("net mins=" + (current_offset * 60 - x));
mm = mm.add(x - current_offset * 60, "minutes");
adjustHours = (current_offset * 60 - x) / 60;
event.start = mm.toDate();
Log.debug("adjusted date=" + event.start);
} else {
// get the start time in that timezone
Log.debug("start date/time=" + moment(event.start).toDate());
start_offset = moment.tz(moment(event.start), event.start.tz).utcOffset();
Log.debug("start offset=" + start_offset);
Log.debug("start date/time w tz =" + moment.tz(moment(event.start), event.start.tz).toDate());
// get the specified date in that timezone
mm = moment.tz(moment(date), event.start.tz);
Log.debug("event date=" + mm.toDate());
current_offset = mm.utcOffset();
}
Log.debug("event offset=" + current_offset + " hour=" + mm.format("H") + " event date=" + mm.toDate());
// if the offset is greater than 0, east of london
if (current_offset !== start_offset) {
// big offset
Log.debug("offset");
let h = parseInt(mm.format("H"));
// check if the event time is less than the offset
if (h > 0 && h < Math.abs(current_offset) / 60) {
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
// we need to fix that
adjustHours = 24;
// Log.debug("adjusting date")
}
//-300 > -240
//if (Math.abs(current_offset) > Math.abs(start_offset)){
if (current_offset > start_offset) {
adjustHours -= 1;
Log.debug("adjust down 1 hour dst change");
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
} else if (current_offset < start_offset) {
adjustHours += 1;
Log.debug("adjust up 1 hour dst change");
}
}
}
Log.debug("adjustHours=" + adjustHours);
return adjustHours;
},
filterEvents: function (data, config) {
const newEvents = [];
// limitFunction doesn't do much limiting, see comment re: the dates
// array in rrule section below as to why we need to do the filtering
// ourselves
const limitFunction = function (date, i) {
return true;
};
const eventDate = function (event, time) {
return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
};
Log.debug("there are " + Object.entries(data).length + " calendar entries");
Object.entries(data).forEach(([key, event]) => {
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.
let past = today;
Log.debug("have entries ");
if (config.includePastEvents) {
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
}
// FIXME: Ugly fix to solve the facebook birthday issue.
// Otherwise, the recurring events only show the birthday for next year.
let isFacebookBirthday = false;
if (typeof event.uid !== "undefined") {
if (event.uid.indexOf("@facebook.com") !== -1) {
isFacebookBirthday = true;
}
}
if (event.type === "VEVENT") {
let startDate = eventDate(event, "start");
let endDate;
Log.debug("\nevent=" + JSON.stringify(event));
if (typeof event.end !== "undefined") {
endDate = eventDate(event, "end");
} else if (typeof event.duration !== "undefined") {
endDate = startDate.clone().add(moment.duration(event.duration));
} else {
if (!isFacebookBirthday) {
// make copy of start date, separate storage area
endDate = moment(startDate.format("x"), "x");
} else {
endDate = moment(startDate).add(1, "days");
}
}
Log.debug(" start=" + startDate.toDate() + " end=" + endDate.toDate());
// calculate the duration of the event for use with recurring events.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
if (event.start.length === 8) {
startDate = startDate.startOf("day");
}
const title = CalendarUtils.getTitleFromEvent(event);
let excluded = false,
dateFilter = null;
for (let f in config.excludedEvents) {
let filter = config.excludedEvents[f],
testTitle = title.toLowerCase(),
until = null,
useRegex = false,
regexFlags = "g";
if (filter instanceof Object) {
if (typeof filter.until !== "undefined") {
until = filter.until;
}
if (typeof filter.regex !== "undefined") {
useRegex = filter.regex;
}
// If additional advanced filtering is added in, this section
// must remain last as we overwrite the filter object with the
// filterBy string
if (filter.caseSensitive) {
filter = filter.filterBy;
testTitle = title;
} else if (useRegex) {
filter = filter.filterBy;
testTitle = title;
regexFlags += "i";
} else {
filter = filter.filterBy.toLowerCase();
}
} else {
filter = filter.toLowerCase();
}
if (CalendarUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
if (until) {
dateFilter = until;
} else {
excluded = true;
}
break;
}
}
if (excluded) {
return;
}
const location = event.location || false;
const geo = event.geo || false;
const description = event.description || false;
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
const rule = event.rrule;
let addedEvents = 0;
const pastMoment = moment(past);
const futureMoment = moment(future);
// can cause problems with e.g. birthdays before 1900
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
rule.origOptions.dtstart.setYear(1900);
rule.options.dtstart.setYear(1900);
}
// For recurring events, get the set of start dates that fall within the range
// of dates we're looking for.
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
let pastLocal = 0;
let futureLocal = 0;
if (CalendarUtils.isFullDayEvent(event)) {
// if full day event, only use the date part of the ranges
pastLocal = pastMoment.toDate();
futureLocal = futureMoment.toDate();
} else {
// if we want past events
if (config.includePastEvents) {
// use the calculated past time for the between from
pastLocal = pastMoment.toDate();
} else {
// otherwise use NOW.. cause we shouldn't use any before now
pastLocal = moment().toDate(); //now
}
futureLocal = futureMoment.toDate(); // future
}
Log.debug(" between=" + pastLocal + " to " + futureLocal);
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
Log.debug("title=" + event.summary + " dates=" + JSON.stringify(dates));
// 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,
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
// because the logic below will filter out any recurrences that don't actually belong within
// our display range.
// Would be great if there was a better way to handle this.
if (event.recurrences !== undefined) {
for (let r in event.recurrences) {
// Only add dates that weren't already in the range we added from the rrule so that
// we don"t double-add those events.
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
dates.push(new Date(r));
}
}
}
// 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];
// ical.js started returning recurrences and exdates as ISOStrings without time information.
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
// (see https://github.com/peterbraden/ical.js/pull/84 )
const dateKey = date.toISOString().substring(0, 10);
let curEvent = event;
let showRecurrence = true;
// for full day events, the time might be off from RRULE/Luxon problem
if (CalendarUtils.isFullDayEvent(event)) {
Log.debug("fullday");
// if the offset is negative, east of GMT where the problem is
if (date.getTimezoneOffset() < 0) {
// get the offset of today where we are processing
// this will be the correction we need to apply
let nowOffset = new Date().getTimezoneOffset();
Log.debug("now offset is " + nowOffset);
// reduce the time by the offset
Log.debug(" recurring date is " + date + " offset is " + date.getTimezoneOffset());
// apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
duration = 24 * 60 * 60 * 1000;
Log.debug("new recurring date is " + date);
}
}
startDate = moment(date);
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
// 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.
curEvent = curEvent.recurrences[dateKey];
startDate = moment(curEvent.start);
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
}
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
}
Log.debug("duration=" + duration);
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (startDate.format("x") === endDate.format("x")) {
endDate = endDate.endOf("day");
}
const recurrenceTitle = CalendarUtils.getTitleFromEvent(curEvent);
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
// it to the event list.
if (endDate.isBefore(past) || startDate.isAfter(future)) {
showRecurrence = false;
}
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
showRecurrence = false;
}
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"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: CalendarUtils.isFullDayEvent(event),
recurringEvent: true,
class: event.class,
firstYear: event.start.getFullYear(),
location: location,
geo: geo,
description: description
});
}
}
// end recurring event parsing
} else {
// Single event.
const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event);
// Log.debug("full day event")
if (config.includePastEvents) {
// Past event is too far in the past, so skip.
if (endDate < past) {
return;
}
} else {
// It's not a fullday event, and it is in the past, so skip.
if (!fullDayEvent && endDate < new Date()) {
return;
}
// It's a fullday event, and it is before today, So skip.
if (fullDayEvent && endDate <= today) {
return;
}
}
// It exceeds the maximumNumberOfDays limit, so skip.
if (startDate > future) {
return;
}
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
return;
}
// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
if (fullDayEvent && startDate <= today) {
startDate = moment(today);
}
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
endDate = endDate.endOf("day");
}
// get correction for date saving and dst change between now and then
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, startDate.toDate());
// Every thing is good. Add it to the list.
newEvents.push({
title: title,
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: fullDayEvent,
class: event.class,
location: location,
geo: geo,
description: description
});
}
}
});
newEvents.sort(function (a, b) {
return a.startDate - b.startDate;
});
// include up to maximumEntries current or upcoming events
// If past events should be included, include all past events
const now = moment();
let entries = 0;
let events = [];
for (let ne of newEvents) {
if (moment(ne.endDate, "x").isBefore(now)) {
if (config.includePastEvents) events.push(ne);
continue;
}
entries++;
// If max events has been saved, skip the rest
if (entries > config.maximumEntries) break;
events.push(ne);
}
return events;
},
/**
* Lookup iana tz from windows
*
* @param msTZName
* @returns {*|null}
*/
getIanaTZFromMS: function (msTZName) {
// Get hash entry
const he = zoneTable[msTZName];
// If found return iana name, else null
return he ? he.iana[0] : null;
},
/**
* Gets the title from the event.
*
* @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) {
let title = "Event";
if (event.summary) {
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
} else if (event.description) {
title = event.description;
}
return title;
},
/**
* Checks if an event is a fullday event.
*
* @param {object} event The event object to check.
* @returns {boolean} True if the event is a fullday event, false otherwise
*/
isFullDayEvent: function (event) {
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
return true;
}
const start = event.start || 0;
const startDate = new Date(start);
const end = event.end || 0;
if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
// Is 24 hours, and starts on the middle of the night.
return true;
}
return false;
},
/**
* Determines if the user defined time filter should apply
*
* @param {Date} now Date object using previously created object for consistency
* @param {Moment} endDate Moment object representing the event end date
* @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) {
if (filter) {
const until = filter.split(" "),
value = parseInt(until[0]),
increment = until[1].slice(-1) === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
filterUntil = moment(endDate.format()).subtract(value, increment);
return now < filterUntil.format("x");
}
return false;
},
/**
*
* @param title
* @param filter
* @param useRegex
* @param regexFlags
* @returns {boolean|*}
*/
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
if (useRegex) {
// Assume if leading slash, there is also trailing slash
if (filter[0] === "/") {
// Strip leading and trailing slashes
filter = filter.substr(1).slice(0, -1);
}
filter = new RegExp(filter, regexFlags);
return filter.test(title);
} else {
return title.includes(filter);
}
}
};
if (typeof module !== "undefined") {
module.exports = CalendarUtils;
}

View File

@ -38,41 +38,49 @@ module.exports = NodeHelper.create({
* @param {string} identifier ID of the module * @param {string} identifier ID of the module
*/ */
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) { createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
var self = this;
if (!validUrl.isUri(url)) { if (!validUrl.isUri(url)) {
self.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url }); this.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url });
return; return;
} }
var fetcher; let fetcher;
if (typeof self.fetchers[identifier + url] === "undefined") { if (typeof this.fetchers[identifier + url] === "undefined") {
Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval); Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert); fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
fetcher.onReceive(function (fetcher) { fetcher.onReceive((fetcher) => {
self.sendSocketNotification("CALENDAR_EVENTS", { this.broadcastEvents(fetcher, identifier);
id: identifier,
url: fetcher.url(),
events: fetcher.events()
});
}); });
fetcher.onError(function (fetcher, error) { fetcher.onError((fetcher, error) => {
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error); Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
self.sendSocketNotification("FETCH_ERROR", { this.sendSocketNotification("FETCH_ERROR", {
id: identifier, id: identifier,
url: fetcher.url(), url: fetcher.url(),
error: error error: error
}); });
}); });
self.fetchers[identifier + url] = fetcher; this.fetchers[identifier + url] = fetcher;
} else { } else {
Log.log("Use existing calendar fetcher for url: " + url); Log.log("Use existing calendar fetcher for url: " + url);
fetcher = self.fetchers[identifier + url]; fetcher = this.fetchers[identifier + url];
fetcher.broadcastEvents();
} }
fetcher.startFetch(); fetcher.startFetch();
},
/**
*
* @param {object} fetcher the fetcher associated with the calendar
* @param {string} identifier the identifier of the calendar
*/
broadcastEvents: function (fetcher, identifier) {
this.sendSocketNotification("CALENDAR_EVENTS", {
id: identifier,
url: fetcher.url(),
events: fetcher.events()
});
} }
}); });

318
package-lock.json generated
View File

@ -370,6 +370,21 @@
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"strip-json-comments": "^3.1.1" "strip-json-comments": "^3.1.1"
},
"dependencies": {
"globals": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
"integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
"requires": {
"type-fest": "^0.8.1"
}
},
"type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="
}
} }
}, },
"@istanbuljs/load-nyc-config": { "@istanbuljs/load-nyc-config": {
@ -1286,9 +1301,9 @@
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
}, },
"chai": { "chai": {
"version": "4.3.3", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.3.tgz", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz",
"integrity": "sha512-MPSLOZwxxnA0DhLE84klnGPojWFK5KuhP7/j5dTsxpr2S3XlkqJP5WbyYl1gCTWvG2Z5N+HD4F472WsbEZL6Pw==", "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==",
"dev": true, "dev": true,
"requires": { "requires": {
"assertion-error": "^1.1.0", "assertion-error": "^1.1.0",
@ -1385,12 +1400,6 @@
} }
} }
}, },
"ci-info": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
"dev": true
},
"clarinet": { "clarinet": {
"version": "0.12.4", "version": "0.12.4",
"resolved": "https://registry.npmjs.org/clarinet/-/clarinet-0.12.4.tgz", "resolved": "https://registry.npmjs.org/clarinet/-/clarinet-0.12.4.tgz",
@ -1474,12 +1483,6 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true "dev": true
}, },
"compare-versions": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
"integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==",
"dev": true
},
"component-emitter": { "component-emitter": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@ -2065,9 +2068,9 @@
} }
}, },
"engine.io": { "engine.io": {
"version": "4.1.1", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.1.1.tgz", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.0.0.tgz",
"integrity": "sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==", "integrity": "sha512-BATIdDV3H1SrE9/u2BAotvsmjJg0t1P4+vGedImSs1lkFAtQdvk4Ev1y4LDiPF7BPWgXWEG+NDY+nLvW3UrMWw==",
"requires": { "requires": {
"accepts": "~1.3.4", "accepts": "~1.3.4",
"base64id": "2.0.0", "base64id": "2.0.0",
@ -2143,18 +2146,24 @@
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
}, },
"escodegen": { "escodegen": {
"version": "1.14.3", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
"integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==",
"dev": true, "dev": true,
"requires": { "requires": {
"esprima": "^4.0.1", "esprima": "^4.0.1",
"estraverse": "^4.2.0", "estraverse": "^5.2.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"optionator": "^0.8.1", "optionator": "^0.8.1",
"source-map": "~0.6.1" "source-map": "~0.6.1"
}, },
"dependencies": { "dependencies": {
"estraverse": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"dev": true
},
"levn": { "levn": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@ -2197,9 +2206,9 @@
} }
}, },
"eslint": { "eslint": {
"version": "7.21.0", "version": "7.22.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.21.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.22.0.tgz",
"integrity": "sha512-W2aJbXpMNofUp0ztQaF40fveSsJBjlSCSWpy//gzfTvwC+USs/nceBrKmlJOiM8r1bLwP2EuYkCqArn/6QTIgg==", "integrity": "sha512-3VawOtjSJUQiiqac8MQc+w457iGLfuNGLFn8JmF051tTKbh5/x/0vlcEj8OgDCaw7Ysa2Jn8paGshV7x2abKXg==",
"requires": { "requires": {
"@babel/code-frame": "7.12.11", "@babel/code-frame": "7.12.11",
"@eslint/eslintrc": "^0.4.0", "@eslint/eslintrc": "^0.4.0",
@ -2218,7 +2227,7 @@
"file-entry-cache": "^6.0.1", "file-entry-cache": "^6.0.1",
"functional-red-black-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1",
"glob-parent": "^5.0.0", "glob-parent": "^5.0.0",
"globals": "^12.1.0", "globals": "^13.6.0",
"ignore": "^4.0.6", "ignore": "^4.0.6",
"import-fresh": "^3.0.0", "import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
@ -2226,7 +2235,7 @@
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"json-stable-stringify-without-jsonify": "^1.0.1", "json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1", "levn": "^0.4.1",
"lodash": "^4.17.20", "lodash": "^4.17.21",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"optionator": "^0.9.1", "optionator": "^0.9.1",
@ -2723,15 +2732,6 @@
"path-exists": "^4.0.0" "path-exists": "^4.0.0"
} }
}, },
"find-versions": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz",
"integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==",
"dev": true,
"requires": {
"semver-regex": "^3.1.2"
}
},
"flat": { "flat": {
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
@ -2975,17 +2975,17 @@
} }
}, },
"globals": { "globals": {
"version": "12.4.0", "version": "13.6.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-13.6.0.tgz",
"integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", "integrity": "sha512-YFKCX0SiPg7l5oKYCJ2zZGxcXprVXHcSnVuvzrT3oSENQonVLqM5pf9fN5dLGZGyCjhw8TN8Btwe/jKnZ0pjvQ==",
"requires": { "requires": {
"type-fest": "^0.8.1" "type-fest": "^0.20.2"
}, },
"dependencies": { "dependencies": {
"type-fest": { "type-fest": {
"version": "0.8.1", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="
} }
} }
}, },
@ -3134,9 +3134,9 @@
"integrity": "sha512-G8tp0wUMI7i8wkMk2xLcEvESg5PiCitFMYgGRc/PwULB0RVhTP5GFdxOwvJwp9XVha8CuS8mnhmE8I/8dx/pbw==" "integrity": "sha512-G8tp0wUMI7i8wkMk2xLcEvESg5PiCitFMYgGRc/PwULB0RVhTP5GFdxOwvJwp9XVha8CuS8mnhmE8I/8dx/pbw=="
}, },
"hosted-git-info": { "hosted-git-info": {
"version": "3.0.8", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.0.tgz",
"integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", "integrity": "sha512-fqhGdjk4av7mT9fU/B01dUtZ+WZSc/XEXMoLXDVZukiQRXxeHSSz3AqbeWRJHtF8EQYHlAgB1NSAHU0Cm7aqZA==",
"dev": true, "dev": true,
"requires": { "requires": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@ -3251,22 +3251,10 @@
"dev": true "dev": true
}, },
"husky": { "husky": {
"version": "4.3.8", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz", "resolved": "https://registry.npmjs.org/husky/-/husky-5.1.3.tgz",
"integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==", "integrity": "sha512-fbNJ+Gz5wx2LIBtMweJNY1D7Uc8p1XERi5KNRMccwfQA+rXlxWNSdUxswo0gT8XqxywTIw7Ywm/F4v/O35RdMg==",
"dev": true, "dev": true
"requires": {
"chalk": "^4.0.0",
"ci-info": "^2.0.0",
"compare-versions": "^3.6.0",
"cosmiconfig": "^7.0.0",
"find-versions": "^4.0.0",
"opencollective-postinstall": "^2.0.2",
"pkg-dir": "^5.0.0",
"please-upgrade-node": "^3.2.0",
"slash": "^3.0.0",
"which-pm-runs": "^1.0.0"
}
}, },
"iconv-lite": { "iconv-lite": {
"version": "0.6.2", "version": "0.6.2",
@ -3343,12 +3331,6 @@
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
}, },
"ip-regex": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
"integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
"dev": true
},
"ip6": { "ip6": {
"version": "0.0.4", "version": "0.0.4",
"resolved": "https://registry.npmjs.org/ip6/-/ip6-0.0.4.tgz", "resolved": "https://registry.npmjs.org/ip6/-/ip6-0.0.4.tgz",
@ -3614,49 +3596,61 @@
"dev": true "dev": true
}, },
"jsdom": { "jsdom": {
"version": "16.4.0", "version": "16.5.1",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.5.1.tgz",
"integrity": "sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==", "integrity": "sha512-pF73EOsJgwZekbDHEY5VO/yKXUkab/DuvrQB/ANVizbr6UAHJsDdHXuotZYwkJSGQl1JM+ivXaqY+XBDDL4TiA==",
"dev": true, "dev": true,
"requires": { "requires": {
"abab": "^2.0.3", "abab": "^2.0.5",
"acorn": "^7.1.1", "acorn": "^8.0.5",
"acorn-globals": "^6.0.0", "acorn-globals": "^6.0.0",
"cssom": "^0.4.4", "cssom": "^0.4.4",
"cssstyle": "^2.2.0", "cssstyle": "^2.3.0",
"data-urls": "^2.0.0", "data-urls": "^2.0.0",
"decimal.js": "^10.2.0", "decimal.js": "^10.2.1",
"domexception": "^2.0.1", "domexception": "^2.0.1",
"escodegen": "^1.14.1", "escodegen": "^2.0.0",
"html-encoding-sniffer": "^2.0.1", "html-encoding-sniffer": "^2.0.1",
"is-potential-custom-element-name": "^1.0.0", "is-potential-custom-element-name": "^1.0.0",
"nwsapi": "^2.2.0", "nwsapi": "^2.2.0",
"parse5": "5.1.1", "parse5": "6.0.1",
"request": "^2.88.2", "request": "^2.88.2",
"request-promise-native": "^1.0.8", "request-promise-native": "^1.0.9",
"saxes": "^5.0.0", "saxes": "^5.0.1",
"symbol-tree": "^3.2.4", "symbol-tree": "^3.2.4",
"tough-cookie": "^3.0.1", "tough-cookie": "^4.0.0",
"w3c-hr-time": "^1.0.2", "w3c-hr-time": "^1.0.2",
"w3c-xmlserializer": "^2.0.0", "w3c-xmlserializer": "^2.0.0",
"webidl-conversions": "^6.1.0", "webidl-conversions": "^6.1.0",
"whatwg-encoding": "^1.0.5", "whatwg-encoding": "^1.0.5",
"whatwg-mimetype": "^2.3.0", "whatwg-mimetype": "^2.3.0",
"whatwg-url": "^8.0.0", "whatwg-url": "^8.0.0",
"ws": "^7.2.3", "ws": "^7.4.4",
"xml-name-validator": "^3.0.0" "xml-name-validator": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"acorn": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.1.0.tgz",
"integrity": "sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==",
"dev": true
},
"tough-cookie": { "tough-cookie": {
"version": "3.0.1", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
"integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
"dev": true, "dev": true,
"requires": { "requires": {
"ip-regex": "^2.1.0", "psl": "^1.1.33",
"psl": "^1.1.28", "punycode": "^2.1.1",
"punycode": "^2.1.1" "universalify": "^0.1.2"
} }
},
"ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"dev": true
} }
} }
}, },
@ -3944,9 +3938,9 @@
} }
}, },
"map-obj": { "map-obj": {
"version": "4.1.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.0.tgz",
"integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", "integrity": "sha512-NAq0fCmZYGz9UFEQyndp7sisrow4GroyGeKluyKC/chuITZsPyOyC1UJZPJlVFImhXdROIP5xqouRLThT3BbpQ==",
"dev": true "dev": true
}, },
"marky": { "marky": {
@ -4172,9 +4166,9 @@
"dev": true "dev": true
}, },
"mocha": { "mocha": {
"version": "8.3.0", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-8.3.0.tgz", "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.3.2.tgz",
"integrity": "sha512-TQqyC89V1J/Vxx0DhJIXlq9gbbL9XFNdeLQ1+JsnZsVaSOV1z3tWfw0qZmQJGQRIfkvZcs7snQnZnOCKoldq1Q==", "integrity": "sha512-UdmISwr/5w+uXLPKspgoV7/RXZwKRTiTjJ2/AC5ZiEztIoOYdfKb19+9jNmEInzx5pBsCyJQzarAxqIGBNYJhg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@ungap/promise-all-settled": "1.1.2", "@ungap/promise-all-settled": "1.1.2",
@ -4348,25 +4342,14 @@
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
}, },
"node-ical": { "node-ical": {
"version": "0.12.8", "version": "0.12.9",
"resolved": "https://registry.npmjs.org/node-ical/-/node-ical-0.12.8.tgz", "resolved": "https://registry.npmjs.org/node-ical/-/node-ical-0.12.9.tgz",
"integrity": "sha512-SocIwrKSSej/Ufv7a3E7AzffuMnXeydu8Dh6d1arlCMqG5+2qD1vlKohX97/TiTqCAuOXF6CxUHunT4nFEW9Zw==", "integrity": "sha512-5nUEZfZPpBpeZbmYCCmNRLsoP08+SGZy/fKxNBX9k67JMUTMFPLEyZ0CXApPDIExX0izMRndG1PsymhEkkSL2Q==",
"requires": { "requires": {
"moment-timezone": "^0.5.31", "moment-timezone": "^0.5.31",
"request": "^2.88.2", "request": "^2.88.2",
"rrule": "2.6.6", "rrule": "2.6.8",
"uuid": "^8.3.1" "uuid": "^8.3.1"
},
"dependencies": {
"rrule": {
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.6.tgz",
"integrity": "sha512-h6tb/hRo9SNv8xKjcvsEfdmhXvElMXsU3Yz0KmqMehUqxP6a4Qjmth2EuL1FsjdawADjajLS0eBbWfsZzn3SIw==",
"requires": {
"luxon": "^1.21.3",
"tslib": "^1.10.0"
}
}
} }
}, },
"node-preload": { "node-preload": {
@ -4385,12 +4368,12 @@
"dev": true "dev": true
}, },
"normalize-package-data": { "normalize-package-data": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.1.tgz",
"integrity": "sha512-6lUjEI0d3v6kFrtgA/lOx4zHCWULXsFNIjHolnZCKCTLA6m/G625cdn3O7eNmT0iD3jfo6HZ9cdImGZwf21prw==", "integrity": "sha512-D/ttLdxo71msR4FF3VgSwK4blHfE3/vGByz1NCeE7/Dh8reQOKNJJjk5L10mLq9jxa+ZHzT1/HLgxljzbXE7Fw==",
"dev": true, "dev": true,
"requires": { "requires": {
"hosted-git-info": "^3.0.6", "hosted-git-info": "^4.0.0",
"resolve": "^1.17.0", "resolve": "^1.17.0",
"semver": "^7.3.2", "semver": "^7.3.2",
"validate-npm-package-license": "^3.0.1" "validate-npm-package-license": "^3.0.1"
@ -4651,12 +4634,6 @@
"mimic-fn": "^2.1.0" "mimic-fn": "^2.1.0"
} }
}, },
"opencollective-postinstall": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
"dev": true
},
"optionator": { "optionator": {
"version": "0.9.1", "version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@ -4755,9 +4732,9 @@
} }
}, },
"parse5": { "parse5": {
"version": "5.1.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
"dev": true "dev": true
}, },
"parseurl": { "parseurl": {
@ -4826,24 +4803,6 @@
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"optional": true "optional": true
}, },
"pkg-dir": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
"integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==",
"dev": true,
"requires": {
"find-up": "^5.0.0"
}
},
"please-upgrade-node": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
"integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
"dev": true,
"requires": {
"semver-compare": "^1.0.0"
}
},
"postcss": { "postcss": {
"version": "7.0.35", "version": "7.0.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz",
@ -5730,13 +5689,8 @@
"semver-compare": { "semver-compare": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=" "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
}, "optional": true
"semver-regex": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.2.tgz",
"integrity": "sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA==",
"dev": true
}, },
"send": { "send": {
"version": "0.17.1", "version": "0.17.1",
@ -5898,9 +5852,9 @@
} }
}, },
"socket.io": { "socket.io": {
"version": "3.1.2", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.1.2.tgz", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.0.0.tgz",
"integrity": "sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==", "integrity": "sha512-/c1riZMV/4yz7KEpaMhDQbwhJDIoO55whXaRKgyEBQrLU9zUHXo9rzeTMvTOqwL9mbKfHKdrXcMoCeQ/1YtMsg==",
"requires": { "requires": {
"@types/cookie": "^0.4.0", "@types/cookie": "^0.4.0",
"@types/cors": "^2.8.8", "@types/cors": "^2.8.8",
@ -5908,15 +5862,15 @@
"accepts": "~1.3.4", "accepts": "~1.3.4",
"base64id": "~2.0.0", "base64id": "~2.0.0",
"debug": "~4.3.1", "debug": "~4.3.1",
"engine.io": "~4.1.0", "engine.io": "~5.0.0",
"socket.io-adapter": "~2.1.0", "socket.io-adapter": "~2.2.0",
"socket.io-parser": "~4.0.3" "socket.io-parser": "~4.0.3"
} }
}, },
"socket.io-adapter": { "socket.io-adapter": {
"version": "2.1.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz",
"integrity": "sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==" "integrity": "sha512-rG49L+FwaVEwuAdeBRq49M97YI3ElVabJPzvHT9S6a2CWhDKnjSFasvwAwSYPRhQzfn4NtDIbCaGYgOCOU/rlg=="
}, },
"socket.io-parser": { "socket.io-parser": {
"version": "4.0.4", "version": "4.0.4",
@ -6099,9 +6053,9 @@
"dev": true "dev": true
}, },
"stylelint": { "stylelint": {
"version": "13.11.0", "version": "13.12.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.11.0.tgz", "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.12.0.tgz",
"integrity": "sha512-DhrKSWDWGZkCiQMtU+VroXM6LWJVC8hSK24nrUngTSQvXGK75yZUq4yNpynqrxD3a/fzKMED09V+XxO4z4lTbw==", "integrity": "sha512-P8O1xDy41B7O7iXaSlW+UuFbE5+ZWQDb61ndGDxKIt36fMH50DtlQTbwLpFLf8DikceTAb3r6nPrRv30wBlzXw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@stylelint/postcss-css-in-js": "^0.37.2", "@stylelint/postcss-css-in-js": "^0.37.2",
@ -6114,7 +6068,7 @@
"execall": "^2.0.0", "execall": "^2.0.0",
"fast-glob": "^3.2.5", "fast-glob": "^3.2.5",
"fastest-levenshtein": "^1.0.12", "fastest-levenshtein": "^1.0.12",
"file-entry-cache": "^6.0.0", "file-entry-cache": "^6.0.1",
"get-stdin": "^8.0.0", "get-stdin": "^8.0.0",
"global-modules": "^2.0.0", "global-modules": "^2.0.0",
"globby": "^11.0.2", "globby": "^11.0.2",
@ -6124,7 +6078,7 @@
"import-lazy": "^4.0.0", "import-lazy": "^4.0.0",
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
"known-css-properties": "^0.21.0", "known-css-properties": "^0.21.0",
"lodash": "^4.17.20", "lodash": "^4.17.21",
"log-symbols": "^4.0.0", "log-symbols": "^4.0.0",
"mathml-tag-names": "^2.1.3", "mathml-tag-names": "^2.1.3",
"meow": "^9.0.0", "meow": "^9.0.0",
@ -6144,7 +6098,7 @@
"resolve-from": "^5.0.0", "resolve-from": "^5.0.0",
"slash": "^3.0.0", "slash": "^3.0.0",
"specificity": "^0.4.1", "specificity": "^0.4.1",
"string-width": "^4.2.0", "string-width": "^4.2.2",
"strip-ansi": "^6.0.0", "strip-ansi": "^6.0.0",
"style-search": "^0.1.0", "style-search": "^0.1.0",
"sugarss": "^2.0.0", "sugarss": "^2.0.0",
@ -6175,18 +6129,18 @@
"dev": true "dev": true
}, },
"stylelint-config-recommended": { "stylelint-config-recommended": {
"version": "3.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-4.0.0.tgz",
"integrity": "sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ==", "integrity": "sha512-sgna89Ng+25Hr9kmmaIxpGWt2LStVm1xf1807PdcWasiPDaOTkOHRL61sINw0twky7QMzafCGToGDnHT/kTHtQ==",
"dev": true "dev": true
}, },
"stylelint-config-standard": { "stylelint-config-standard": {
"version": "20.0.0", "version": "21.0.0",
"resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-20.0.0.tgz", "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-21.0.0.tgz",
"integrity": "sha512-IB2iFdzOTA/zS4jSVav6z+wGtin08qfj+YyExHB3LF9lnouQht//YyB0KZq9gGz5HNPkddHOzcY8HsUey6ZUlA==", "integrity": "sha512-Yf6mx5oYEbQQJxWuW7X3t1gcxqbUx52qC9SMS3saC2ruOVYEyqmr5zSW6k3wXflDjjFrPhar3kp68ugRopmlzg==",
"dev": true, "dev": true,
"requires": { "requires": {
"stylelint-config-recommended": "^3.0.0" "stylelint-config-recommended": "^4.0.0"
} }
}, },
"stylelint-prettier": { "stylelint-prettier": {
@ -6247,9 +6201,9 @@
}, },
"dependencies": { "dependencies": {
"ajv": { "ajv": {
"version": "7.1.1", "version": "7.2.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-7.1.1.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.2.1.tgz",
"integrity": "sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ==", "integrity": "sha512-+nu0HDv7kNSOua9apAVc979qd932rrZeb3WOvoiD31A/p1mIE5/9bN2027pE2rOPYEdS3UHzsvof4hY+lM9/WQ==",
"requires": { "requires": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0", "json-schema-traverse": "^1.0.0",
@ -6493,9 +6447,9 @@
} }
}, },
"unist-util-is": { "unist-util-is": {
"version": "4.0.4", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.4.tgz", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz",
"integrity": "sha512-3dF39j/u423v4BBQrk1AQ2Ve1FxY5W3JKwXxVFzBODQ6WEvccguhgp802qQLKSnxPODE6WuRZtV+ohlUg4meBA==", "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==",
"dev": true "dev": true
}, },
"unist-util-stringify-position": { "unist-util-stringify-position": {
@ -6549,9 +6503,9 @@
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
}, },
"v8-compile-cache": { "v8-compile-cache": {
"version": "2.2.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
"integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==" "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA=="
}, },
"valid-url": { "valid-url": {
"version": "1.0.9", "version": "1.0.9",
@ -6887,12 +6841,6 @@
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true "dev": true
}, },
"which-pm-runs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz",
"integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=",
"dev": true
},
"wide-align": { "wide-align": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
@ -7003,9 +6951,9 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}, },
"yaml": { "yaml": {
"version": "1.10.0", "version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true "dev": true
}, },
"yargs": { "yargs": {

View File

@ -43,16 +43,16 @@
}, },
"homepage": "https://magicmirror.builders", "homepage": "https://magicmirror.builders",
"devDependencies": { "devDependencies": {
"chai": "^4.3.0", "chai": "^4.3.4",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.1.0",
"eslint-plugin-jsdoc": "^32.2.0", "eslint-plugin-jsdoc": "^32.2.0",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.3.1",
"express-basic-auth": "^1.2.0", "express-basic-auth": "^1.2.0",
"husky": "^4.3.8", "husky": "^5.1.3",
"jsdom": "^16.4.0", "jsdom": "^16.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mocha": "^8.3.0", "mocha": "^8.3.2",
"mocha-each": "^2.0.1", "mocha-each": "^2.0.1",
"mocha-logger": "^1.0.7", "mocha-logger": "^1.0.7",
"nyc": "^15.1.0", "nyc": "^15.1.0",
@ -60,10 +60,10 @@
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.0",
"sinon": "^9.2.4", "sinon": "^9.2.4",
"spectron": "^13.0.0", "spectron": "^13.0.0",
"stylelint": "^13.11.0", "stylelint": "^13.12.0",
"stylelint-config-prettier": "^8.0.2", "stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^20.0.0", "stylelint-config-standard": "^21.0.0",
"stylelint-prettier": "^1.1.2" "stylelint-prettier": "^1.2.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"electron": "^11.3.0" "electron": "^11.3.0"
@ -72,7 +72,7 @@
"colors": "^1.4.0", "colors": "^1.4.0",
"console-stamp": "^3.0.0-rc4.2", "console-stamp": "^3.0.0-rc4.2",
"digest-fetch": "^1.1.6", "digest-fetch": "^1.1.6",
"eslint": "^7.20.0", "eslint": "^7.22.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-ipfilter": "^1.1.2", "express-ipfilter": "^1.1.2",
"feedme": "^2.0.2", "feedme": "^2.0.2",
@ -81,11 +81,11 @@
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"moment": "^2.29.1", "moment": "^2.29.1",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"node-ical": "^0.12.8", "node-ical": "^0.12.9",
"rrule": "^2.6.8", "rrule": "^2.6.8",
"rrule-alt": "^2.2.8", "rrule-alt": "^2.2.8",
"simple-git": "^2.36.2", "simple-git": "^2.36.2",
"socket.io": "^3.1.2", "socket.io": "^4.0.0",
"valid-url": "^1.0.9" "valid-url": "^1.0.9"
}, },
"_moduleAliases": { "_moduleAliases": {

View File

@ -10,7 +10,7 @@ const afterEach = global.afterEach;
describe("Calendar module", function () { describe("Calendar module", function () {
helpers.setupTimeout(this); helpers.setupTimeout(this);
var app = null; let app = null;
beforeEach(function () { beforeEach(function () {
return helpers return helpers

View File

@ -45,42 +45,42 @@ describe("Functions into modules/default/calendar/calendar.js", function () {
}); });
it("should return a 12-hour longDateFormat when using the 'en' locale", function () { it("should return a 12-hour longDateFormat when using the 'en' locale", function () {
var localeBackup = moment.locale(); const localeBackup = moment.locale();
moment.locale("en"); moment.locale("en");
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
it("should return a 12-hour longDateFormat when using the 'au' locale", function () { it("should return a 12-hour longDateFormat when using the 'au' locale", function () {
var localeBackup = moment.locale(); const localeBackup = moment.locale();
moment.locale("au"); moment.locale("au");
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
it("should return a 12-hour longDateFormat when using the 'eg' locale", function () { it("should return a 12-hour longDateFormat when using the 'eg' locale", function () {
var localeBackup = moment.locale(); const localeBackup = moment.locale();
moment.locale("eg"); moment.locale("eg");
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
it("should return a 24-hour longDateFormat when using the 'nl' locale", function () { it("should return a 24-hour longDateFormat when using the 'nl' locale", function () {
var localeBackup = moment.locale(); const localeBackup = moment.locale();
moment.locale("nl"); moment.locale("nl");
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
it("should return a 24-hour longDateFormat when using the 'fr' locale", function () { it("should return a 24-hour longDateFormat when using the 'fr' locale", function () {
var localeBackup = moment.locale(); const localeBackup = moment.locale();
moment.locale("fr"); moment.locale("fr");
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
it("should return a 24-hour longDateFormat when using the 'uk' locale", function () { it("should return a 24-hour longDateFormat when using the 'uk' locale", function () {
var localeBackup = moment.locale(); const localeBackup = moment.locale();
moment.locale("uk"); moment.locale("uk");
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup); moment.locale(localeBackup);