mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 19:53:36 +00:00
replace request with node-fetch
This commit is contained in:
commit
fa0f997928
@ -36,6 +36,7 @@ _This release is scheduled to be released on 2021-04-01._
|
|||||||
- Dont update the DOM when a module is not displayed.
|
- Dont update the DOM when a module is not displayed.
|
||||||
- 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`
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
@ -6,7 +6,10 @@
|
|||||||
*/
|
*/
|
||||||
const Log = require("logger");
|
const Log = require("logger");
|
||||||
const ical = require("node-ical");
|
const ical = require("node-ical");
|
||||||
const request = require("request");
|
const fetch = require("node-fetch");
|
||||||
|
const digest = require("digest-fetch");
|
||||||
|
const https = require("https");
|
||||||
|
const base64 = require("base-64");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moment date
|
* Moment date
|
||||||
@ -41,387 +44,383 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
* Initiates calendar fetch.
|
* Initiates calendar fetch.
|
||||||
*/
|
*/
|
||||||
const fetchCalendar = function () {
|
const fetchCalendar = function () {
|
||||||
|
function getFetcher(url, auth) {
|
||||||
|
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||||
|
let headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
|
||||||
|
};
|
||||||
|
let httpsAgent = null;
|
||||||
|
|
||||||
|
if (selfSignedCert) {
|
||||||
|
httpsAgent = new https.Agent({
|
||||||
|
rejectUnauthorized: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (auth) {
|
||||||
|
if (auth.method === "bearer") {
|
||||||
|
headers.Authorization = "Bearer " + auth.pass;
|
||||||
|
} else if (auth.method === "digest") {
|
||||||
|
return new digest(auth.user, auth.pass).fetch(url, { headers: headers, httpsAgent: httpsAgent });
|
||||||
|
} else {
|
||||||
|
headers.Authorization = "Basic " + base64.encode(auth.user + ":" + auth.pass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fetch(url, { headers: headers, httpsAgent: httpsAgent });
|
||||||
|
}
|
||||||
clearTimeout(reloadTimer);
|
clearTimeout(reloadTimer);
|
||||||
reloadTimer = null;
|
reloadTimer = null;
|
||||||
|
|
||||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
getFetcher(url, auth)
|
||||||
const opts = {
|
.catch((error) => {
|
||||||
headers: {
|
fetchFailedCallback(self, error);
|
||||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
|
|
||||||
},
|
|
||||||
gzip: true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (selfSignedCert) {
|
|
||||||
var agentOptions = {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
};
|
|
||||||
opts.agentOptions = agentOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth) {
|
|
||||||
if (auth.method === "bearer") {
|
|
||||||
opts.auth = {
|
|
||||||
bearer: auth.pass
|
|
||||||
};
|
|
||||||
} 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();
|
scheduleTimer();
|
||||||
return;
|
})
|
||||||
} else if (r.statusCode !== 200) {
|
.then((response) => {
|
||||||
fetchFailedCallback(self, r.statusCode + ": " + r.statusMessage);
|
if (response.status !== 200) {
|
||||||
scheduleTimer();
|
fetchFailedCallback(self, response.statusText);
|
||||||
return;
|
scheduleTimer();
|
||||||
}
|
}
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((responseData) => {
|
||||||
|
let data = [];
|
||||||
|
|
||||||
let data = [];
|
try {
|
||||||
|
data = ical.parseICS(responseData);
|
||||||
try {
|
} catch (error) {
|
||||||
data = ical.parseICS(requestData);
|
fetchFailedCallback(self, error.message);
|
||||||
} catch (error) {
|
scheduleTimer();
|
||||||
fetchFailedCallback(self, error.message);
|
|
||||||
scheduleTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
Log.debug(" parsed data=" + JSON.stringify(data));
|
||||||
// Otherwise, the recurring events only show the birthday for next year.
|
|
||||||
let isFacebookBirthday = false;
|
const newEvents = [];
|
||||||
if (typeof event.uid !== "undefined") {
|
|
||||||
if (event.uid.indexOf("@facebook.com") !== -1) {
|
// 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
|
||||||
isFacebookBirthday = true;
|
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();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "VEVENT") {
|
// FIXME: Ugly fix to solve the facebook birthday issue.
|
||||||
let startDate = eventDate(event, "start");
|
// Otherwise, the recurring events only show the birthday for next year.
|
||||||
let endDate;
|
let isFacebookBirthday = false;
|
||||||
|
if (typeof event.uid !== "undefined") {
|
||||||
|
if (event.uid.indexOf("@facebook.com") !== -1) {
|
||||||
|
isFacebookBirthday = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Log.debug("\nevent=" + JSON.stringify(event));
|
if (event.type === "VEVENT") {
|
||||||
if (typeof event.end !== "undefined") {
|
let startDate = eventDate(event, "start");
|
||||||
endDate = eventDate(event, "end");
|
let endDate;
|
||||||
} else if (typeof event.duration !== "undefined") {
|
|
||||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
Log.debug("\nevent=" + JSON.stringify(event));
|
||||||
} else {
|
if (typeof event.end !== "undefined") {
|
||||||
if (!isFacebookBirthday) {
|
endDate = eventDate(event, "end");
|
||||||
// make copy of start date, separate storage area
|
} else if (typeof event.duration !== "undefined") {
|
||||||
endDate = moment(startDate.format("x"), "x");
|
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||||
} else {
|
} else {
|
||||||
endDate = moment(startDate).add(1, "days");
|
if (!isFacebookBirthday) {
|
||||||
}
|
// make copy of start date, separate storage area
|
||||||
}
|
endDate = moment(startDate.format("x"), "x");
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
filter = filter.filterBy.toLowerCase();
|
endDate = moment(startDate).add(1, "days");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
filter = filter.toLowerCase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (testTitleByFilter(testTitle, filter, useRegex, regexFlags)) {
|
Log.debug(" start=" + startDate.toDate() + " end=" + endDate.toDate());
|
||||||
if (until) {
|
|
||||||
dateFilter = until;
|
|
||||||
} else {
|
|
||||||
excluded = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excluded) {
|
// calculate the duration of the event for use with recurring events.
|
||||||
return;
|
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||||
}
|
|
||||||
|
|
||||||
const location = event.location || false;
|
if (event.start.length === 8) {
|
||||||
const geo = event.geo || false;
|
startDate = startDate.startOf("day");
|
||||||
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
|
const title = getTitleFromEvent(event);
|
||||||
// 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 excluded = false,
|
||||||
let pastLocal = 0;
|
dateFilter = null;
|
||||||
let futureLocal = 0;
|
|
||||||
if (isFullDayEvent(event)) {
|
for (let f in excludedEvents) {
|
||||||
// if full day event, only use the date part of the ranges
|
let filter = excludedEvents[f],
|
||||||
pastLocal = pastMoment.toDate();
|
testTitle = title.toLowerCase(),
|
||||||
futureLocal = futureMoment.toDate();
|
until = null,
|
||||||
} else {
|
useRegex = false,
|
||||||
// if we want past events
|
regexFlags = "g";
|
||||||
if (includePastEvents) {
|
|
||||||
// use the calculated past time for the between from
|
if (filter instanceof Object) {
|
||||||
pastLocal = pastMoment.toDate();
|
if (typeof filter.until !== "undefined") {
|
||||||
} else {
|
until = filter.until;
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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 (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 (isFullDayEvent(event)) {
|
if (isFullDayEvent(event)) {
|
||||||
Log.debug("fullday");
|
// if full day event, only use the date part of the ranges
|
||||||
// if the offset is negative, east of GMT where the problem is
|
pastLocal = pastMoment.toDate();
|
||||||
if (date.getTimezoneOffset() < 0) {
|
futureLocal = futureMoment.toDate();
|
||||||
// get the offset of today where we are processing
|
} else {
|
||||||
// this will be the correction we need to apply
|
// if we want past events
|
||||||
let nowOffset = new Date().getTimezoneOffset();
|
if (includePastEvents) {
|
||||||
Log.debug("now offset is " + nowOffset);
|
// use the calculated past time for the between from
|
||||||
// reduce the time by the offset
|
pastLocal = pastMoment.toDate();
|
||||||
Log.debug(" recurring date is " + date + " offset is " + date.getTimezoneOffset());
|
} else {
|
||||||
// apply the correction to the date/time to get it UTC relative
|
// otherwise use NOW.. cause we shouldnt use any before now
|
||||||
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
|
pastLocal = moment().toDate(); //now
|
||||||
// the duration was calculated way back at the top before we could correct the start time..
|
}
|
||||||
// fix it for this event entry
|
futureLocal = futureMoment.toDate(); // future
|
||||||
duration = 24 * 60 * 60 * 1000;
|
}
|
||||||
Log.debug("new recurring date is " + date);
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
startDate = moment(date);
|
// 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;
|
||||||
|
|
||||||
let adjustDays = getCorrection(event, date);
|
// 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);
|
||||||
|
|
||||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
let adjustDays = getCorrection(event, date);
|
||||||
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.
|
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||||
curEvent = curEvent.recurrences[dateKey];
|
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
||||||
startDate = moment(curEvent.start);
|
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||||
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
// end recurring event parsing
|
||||||
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
|
} else {
|
||||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
// Single event.
|
||||||
showRecurrence = false;
|
const fullDayEvent = isFacebookBirthday ? true : isFullDayEvent(event);
|
||||||
}
|
// Log.debug("full day event")
|
||||||
Log.debug("duration=" + duration);
|
|
||||||
|
|
||||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
if (includePastEvents) {
|
||||||
if (startDate.format("x") === endDate.format("x")) {
|
// Past event is too far in the past, so skip.
|
||||||
endDate = endDate.endOf("day");
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const recurrenceTitle = getTitleFromEvent(curEvent);
|
// It exceeds the maximumNumberOfDays limit, so skip.
|
||||||
|
if (startDate > future) {
|
||||||
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
|
return;
|
||||||
// it to the event list.
|
|
||||||
if (endDate.isBefore(past) || startDate.isAfter(future)) {
|
|
||||||
showRecurrence = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeFilterApplies(now, endDate, dateFilter)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's a fullday event, and it is before today, So skip.
|
// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
|
||||||
if (fullDayEvent && endDate <= today) {
|
if (fullDayEvent && startDate <= today) {
|
||||||
return;
|
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
const Log = require("logger");
|
const Log = require("logger");
|
||||||
const FeedMe = require("feedme");
|
const FeedMe = require("feedme");
|
||||||
const request = require("request");
|
const fetch = require("node-fetch");
|
||||||
const iconv = require("iconv-lite");
|
const iconv = require("iconv-lite");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,22 +79,20 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
|||||||
});
|
});
|
||||||
|
|
||||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||||
const opts = {
|
const headers = {
|
||||||
headers: {
|
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)",
|
||||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)",
|
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
Pragma: "no-cache"
|
||||||
Pragma: "no-cache"
|
|
||||||
},
|
|
||||||
encoding: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
request(url, opts)
|
fetch(url, { headers: headers })
|
||||||
.on("error", function (error) {
|
.catch((error) => {
|
||||||
fetchFailedCallback(self, error);
|
fetchFailedCallback(self, error);
|
||||||
scheduleTimer();
|
scheduleTimer();
|
||||||
})
|
})
|
||||||
.pipe(iconv.decodeStream(encoding))
|
.then((res) => {
|
||||||
.pipe(parser);
|
res.body.pipe(iconv.decodeStream(encoding)).pipe(parser);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
2129
package-lock.json
generated
2129
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -71,6 +71,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"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",
|
||||||
"eslint": "^7.20.0",
|
"eslint": "^7.20.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-ipfilter": "^1.1.2",
|
"express-ipfilter": "^1.1.2",
|
||||||
@ -80,8 +81,8 @@
|
|||||||
"iconv-lite": "^0.6.2",
|
"iconv-lite": "^0.6.2",
|
||||||
"module-alias": "^2.2.2",
|
"module-alias": "^2.2.2",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
"node-ical": "^0.12.8",
|
"node-ical": "^0.12.8",
|
||||||
"request": "^2.88.2",
|
|
||||||
"rrule": "^2.6.8",
|
"rrule": "^2.6.8",
|
||||||
"rrule-alt": "^2.2.8",
|
"rrule-alt": "^2.2.8",
|
||||||
"simple-git": "^2.35.2",
|
"simple-git": "^2.35.2",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const helpers = require("./global-setup");
|
const helpers = require("./global-setup");
|
||||||
const request = require("request");
|
const fetch = require("node-fetch");
|
||||||
const expect = require("chai").expect;
|
const expect = require("chai").expect;
|
||||||
|
|
||||||
const describe = global.describe;
|
const describe = global.describe;
|
||||||
@ -46,15 +46,15 @@ describe("Electron app environment", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("get request from http://localhost:8080 should return 200", function (done) {
|
it("get request from http://localhost:8080 should return 200", function (done) {
|
||||||
request.get("http://localhost:8080", function (err, res, body) {
|
fetch("http://localhost:8080").then((res) => {
|
||||||
expect(res.statusCode).to.equal(200);
|
expect(res.status).to.equal(200);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("get request from http://localhost:8080/nothing should return 404", function (done) {
|
it("get request from http://localhost:8080/nothing should return 404", function (done) {
|
||||||
request.get("http://localhost:8080/nothing", function (err, res, body) {
|
fetch("http://localhost:8080/nothing").then((res) => {
|
||||||
expect(res.statusCode).to.equal(404);
|
expect(res.status).to.equal(404);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const helpers = require("./global-setup");
|
const helpers = require("./global-setup");
|
||||||
const request = require("request");
|
const fetch = require("node-fetch");
|
||||||
const expect = require("chai").expect;
|
const expect = require("chai").expect;
|
||||||
const forEach = require("mocha-each");
|
const forEach = require("mocha-each");
|
||||||
|
|
||||||
@ -40,8 +40,8 @@ describe("All font files from roboto.css should be downloadable", function () {
|
|||||||
|
|
||||||
forEach(fontFiles).it("should return 200 HTTP code for file '%s'", (fontFile, done) => {
|
forEach(fontFiles).it("should return 200 HTTP code for file '%s'", (fontFile, done) => {
|
||||||
var fontUrl = "http://localhost:8080/fonts/" + fontFile;
|
var fontUrl = "http://localhost:8080/fonts/" + fontFile;
|
||||||
request.get(fontUrl, function (err, res, body) {
|
fetch(fontUrl).then((res) => {
|
||||||
expect(res.statusCode).to.equal(200);
|
expect(res.status).to.equal(200);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const helpers = require("./global-setup");
|
const helpers = require("./global-setup");
|
||||||
const request = require("request");
|
const fetch = require("node-fetch");
|
||||||
const expect = require("chai").expect;
|
const expect = require("chai").expect;
|
||||||
|
|
||||||
const describe = global.describe;
|
const describe = global.describe;
|
||||||
@ -32,8 +32,8 @@ describe("ipWhitelist directive configuration", function () {
|
|||||||
process.env.MM_CONFIG_FILE = "tests/configs/noIpWhiteList.js";
|
process.env.MM_CONFIG_FILE = "tests/configs/noIpWhiteList.js";
|
||||||
});
|
});
|
||||||
it("should return 403", function (done) {
|
it("should return 403", function (done) {
|
||||||
request.get("http://localhost:8080", function (err, res, body) {
|
fetch("http://localhost:8080").then((res) => {
|
||||||
expect(res.statusCode).to.equal(403);
|
expect(res.status).to.equal(403);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -45,8 +45,8 @@ describe("ipWhitelist directive configuration", function () {
|
|||||||
process.env.MM_CONFIG_FILE = "tests/configs/empty_ipWhiteList.js";
|
process.env.MM_CONFIG_FILE = "tests/configs/empty_ipWhiteList.js";
|
||||||
});
|
});
|
||||||
it("should return 200", function (done) {
|
it("should return 200", function (done) {
|
||||||
request.get("http://localhost:8080", function (err, res, body) {
|
fetch("http://localhost:8080").then((res) => {
|
||||||
expect(res.statusCode).to.equal(200);
|
expect(res.status).to.equal(200);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const helpers = require("./global-setup");
|
const helpers = require("./global-setup");
|
||||||
const request = require("request");
|
const fetch = require("node-fetch");
|
||||||
const expect = require("chai").expect;
|
const expect = require("chai").expect;
|
||||||
|
|
||||||
const describe = global.describe;
|
const describe = global.describe;
|
||||||
@ -33,8 +33,8 @@ describe("port directive configuration", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return 200", function (done) {
|
it("should return 200", function (done) {
|
||||||
request.get("http://localhost:8090", function (err, res, body) {
|
fetch("http://localhost:8090").then((res) => {
|
||||||
expect(res.statusCode).to.equal(200);
|
expect(res.status).to.equal(200);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -52,8 +52,8 @@ describe("port directive configuration", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return 200", function (done) {
|
it("should return 200", function (done) {
|
||||||
request.get("http://localhost:8100", function (err, res, body) {
|
fetch("http://localhost:8100").then((res) => {
|
||||||
expect(res.statusCode).to.equal(200);
|
expect(res.status).to.equal(200);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const helpers = require("./global-setup");
|
const helpers = require("./global-setup");
|
||||||
const request = require("request");
|
const fetch = require("node-fetch");
|
||||||
const expect = require("chai").expect;
|
const expect = require("chai").expect;
|
||||||
|
|
||||||
const describe = global.describe;
|
const describe = global.describe;
|
||||||
@ -32,8 +32,8 @@ describe("Vendors", function () {
|
|||||||
Object.keys(vendors).forEach((vendor) => {
|
Object.keys(vendors).forEach((vendor) => {
|
||||||
it(`should return 200 HTTP code for vendor "${vendor}"`, function () {
|
it(`should return 200 HTTP code for vendor "${vendor}"`, function () {
|
||||||
var urlVendor = "http://localhost:8080/vendor/" + vendors[vendor];
|
var urlVendor = "http://localhost:8080/vendor/" + vendors[vendor];
|
||||||
request.get(urlVendor, function (err, res, body) {
|
fetch(urlVendor).then((res) => {
|
||||||
expect(res.statusCode).to.equal(200);
|
expect(res.status).to.equal(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -41,8 +41,8 @@ describe("Vendors", function () {
|
|||||||
Object.keys(vendors).forEach((vendor) => {
|
Object.keys(vendors).forEach((vendor) => {
|
||||||
it(`should return 404 HTTP code for vendor https://localhost/"${vendor}"`, function () {
|
it(`should return 404 HTTP code for vendor https://localhost/"${vendor}"`, function () {
|
||||||
var urlVendor = "http://localhost:8080/" + vendors[vendor];
|
var urlVendor = "http://localhost:8080/" + vendors[vendor];
|
||||||
request.get(urlVendor, function (err, res, body) {
|
fetch(urlVendor).then((res) => {
|
||||||
expect(res.statusCode).to.equal(404);
|
expect(res.status).to.equal(404);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user