MagicMirror/modules/default/calendar/calendarfetcher.js

472 lines
14 KiB
JavaScript
Raw Normal View History

2016-04-15 12:18:59 +02:00
/* Magic Mirror
* Node Helper: Calendar - CalendarFetcher
*
2020-04-28 23:05:28 +02:00
* By Michael Teeuw https://michaelteeuw.nl
2016-04-15 12:18:59 +02:00
* MIT Licensed.
*/
2020-05-31 22:12:26 +02:00
const Log = require("../../../js/logger.js");
2020-06-17 21:17:26 +02:00
const ical = require("ical");
2020-05-02 10:39:09 +02:00
const moment = require("moment");
const request = require("request");
2016-04-15 12:18:59 +02:00
2020-06-20 20:43:09 +02:00
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNumberOfDays, auth, includePastEvents) {
2020-06-17 21:37:49 +02:00
const self = this;
2016-04-15 12:18:59 +02:00
2020-06-18 21:54:51 +02:00
let reloadTimer = null;
let events = [];
2016-04-15 12:18:59 +02:00
2020-06-17 21:37:49 +02:00
let fetchFailedCallback = function () {};
let eventsReceivedCallback = function () {};
2016-04-15 12:18:59 +02:00
/* fetchCalendar()
* Initiates calendar fetch.
*/
2020-06-17 21:17:26 +02:00
const fetchCalendar = function () {
2016-04-15 12:18:59 +02:00
clearTimeout(reloadTimer);
reloadTimer = null;
2020-06-17 21:17:26 +02:00
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
const opts = {
headers: {
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
2018-09-08 23:05:19 +08:00
},
gzip: true
};
if (auth) {
if (auth.method === "bearer") {
opts.auth = {
bearer: auth.pass
};
2019-06-05 09:32:10 +02:00
} else {
opts.auth = {
user: auth.user,
pass: auth.pass,
sendImmediately: auth.method !== "digest"
};
}
}
request(url, opts, function (err, r, requestData) {
if (err) {
fetchFailedCallback(self, err);
scheduleTimer();
return;
} else if (r.statusCode !== 200) {
fetchFailedCallback(self, r.statusCode + ": " + r.statusMessage);
scheduleTimer();
return;
}
2017-02-07 23:51:13 +01:00
const data = ical.parseICS(requestData);
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;
};
2016-04-15 12:18:59 +02:00
const eventDate = function (event, time) {
return event[time].length === 8 ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
};
2016-04-15 12:18:59 +02:00
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;
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;
}
}
2020-06-18 21:54:51 +02:00
if (event.type === "VEVENT") {
let startDate = eventDate(event, "start");
let endDate;
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) {
endDate = startDate;
} else {
endDate = moment(startDate).add(1, "days");
}
}
// 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);
2020-06-17 21:17:26 +02:00
let excluded = false,
dateFilter = null;
2020-06-17 21:17:26 +02:00
for (let f in excludedEvents) {
let filter = excludedEvents[f],
testTitle = title.toLowerCase(),
until = null,
useRegex = false,
regexFlags = "g";
2020-06-17 21:17:26 +02:00
if (filter instanceof Object) {
if (typeof filter.until !== "undefined") {
until = filter.until;
}
2020-06-17 21:17:26 +02:00
if (typeof filter.regex !== "undefined") {
useRegex = filter.regex;
}
2020-06-17 21:17:26 +02:00
// 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;
}
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;
2020-06-18 21:54:51 +02:00
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
const pastLocal = pastMoment.subtract(past.getTimezoneOffset(), "minutes").toDate();
const futureLocal = futureMoment.subtract(future.getTimezoneOffset(), "minutes").toDate();
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
// 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));
2020-06-17 21:17:26 +02:00
}
}
}
// Loop through the set of date entries to see which recurrences should be added to our event list.
for (let d in dates) {
const 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;
startDate = moment(date);
2020-06-17 21:17:26 +02:00
// 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;
}
2020-06-17 21:17:26 +02:00
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (startDate.format("x") === endDate.format("x")) {
endDate = endDate.endOf("day");
}
2020-06-17 21:17:26 +02:00
const recurrenceTitle = getTitleFromEvent(curEvent);
2020-06-17 21:17:26 +02:00
// 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;
}
2020-06-17 21:17:26 +02:00
if (timeFilterApplies(now, endDate, dateFilter)) {
showRecurrence = false;
}
if (showRecurrence === true) {
addedEvents++;
newEvents.push({
title: recurrenceTitle,
startDate: startDate.format("x"),
endDate: 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);
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;
}
2020-06-17 21:17:26 +02:00
// It's a fullday event, and it is before today, So skip.
if (fullDayEvent && endDate <= today) {
return;
2016-04-20 15:19:36 +02:00
}
}
// 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);
2016-04-15 12:18:59 +02:00
}
// Every thing is good. Add it to the list.
newEvents.push({
title: title,
startDate: startDate.format("x"),
endDate: endDate.format("x"),
fullDayEvent: fullDayEvent,
class: event.class,
location: location,
geo: geo,
description: description
});
2016-04-15 12:18:59 +02:00
}
}
});
2016-04-15 12:18:59 +02:00
newEvents.sort(function (a, b) {
return a.startDate - b.startDate;
});
2016-04-15 12:18:59 +02:00
events = newEvents;
2016-04-15 12:18:59 +02:00
self.broadcastEvents();
scheduleTimer();
});
2016-04-15 12:18:59 +02:00
};
/* scheduleTimer()
* Schedule the timer for the next update.
*/
2020-06-17 21:37:49 +02:00
const scheduleTimer = function () {
2016-04-15 12:18:59 +02:00
clearTimeout(reloadTimer);
reloadTimer = setTimeout(function () {
2016-04-15 12:18:59 +02:00
fetchCalendar();
}, reloadInterval);
};
2016-04-19 10:34:14 +02:00
/* isFullDayEvent(event)
* Checks if an event is a fullday event.
*
2019-03-08 11:19:15 +01:00
* argument event object - The event object to check.
2016-04-19 10:34:14 +02:00
*
* return bool - The event is a fullday event.
*/
2020-06-17 21:37:49 +02:00
const isFullDayEvent = function (event) {
if (event.start.length === 8 || event.start.dateOnly) {
2016-04-19 10:34:14 +02:00
return true;
}
2020-06-17 21:37:49 +02:00
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) {
2016-04-19 10:34:14 +02:00
// Is 24 hours, and starts on the middle of the night.
return true;
2016-04-19 10:34:14 +02:00
}
return false;
};
/* timeFilterApplies()
* Determines if the user defined time filter should apply
*
* argument now Date - Date object using previously created object for consistency
* argument endDate Moment - Moment object representing the event end date
* argument filter string - The time to subtract from the end date to determine if an event should be shown
*
* return bool - The event should be filtered out
*/
2020-06-17 21:37:49 +02:00
const timeFilterApplies = function (now, endDate, filter) {
if (filter) {
2020-06-17 21:37:49 +02:00
const until = filter.split(" "),
value = parseInt(until[0]),
2020-06-20 08:45:22 +02:00
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;
};
/* getTitleFromEvent(event)
* Gets the title from the event.
*
* argument event object - The event object to check.
*
* return string - The title of the event, or "Event" if no title is found.
*/
2020-06-17 21:37:49 +02:00
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;
};
2020-06-17 21:37:49 +02:00
const testTitleByFilter = function (title, filter, useRegex, regexFlags) {
2018-06-03 17:34:16 -04:00
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);
}
2019-06-05 09:32:10 +02:00
};
2018-06-03 17:34:16 -04:00
2016-04-15 12:18:59 +02:00
/* public methods */
/* startFetch()
* Initiate fetchCalendar();
*/
this.startFetch = function () {
2016-04-15 12:18:59 +02:00
fetchCalendar();
};
/* broadcastItems()
* Broadcast the existing events.
2016-04-15 12:18:59 +02:00
*/
2020-05-25 18:57:15 +02:00
this.broadcastEvents = function () {
2020-06-01 16:40:20 +02:00
Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events.");
2016-04-15 12:18:59 +02:00
eventsReceivedCallback(self);
};
/* onReceive(callback)
* Sets the on success callback
*
* argument callback function - The on success callback.
*/
this.onReceive = function (callback) {
2016-04-15 12:18:59 +02:00
eventsReceivedCallback = callback;
};
/* onError(callback)
* Sets the on error callback
*
* argument callback function - The on error callback.
*/
this.onError = function (callback) {
2016-04-15 12:18:59 +02:00
fetchFailedCallback = callback;
};
/* url()
* Returns the url of this fetcher.
*
* return string - The url of this fetcher.
*/
this.url = function () {
2016-04-15 12:18:59 +02:00
return url;
};
/* events()
* Returns current available events for this fetcher.
*
* return array - The current available events for this fetcher.
*/
this.events = function () {
2016-04-15 12:18:59 +02:00
return events;
};
};
module.exports = CalendarFetcher;