mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 11:50:00 +00:00
## [2.26.0] - 01-01-2024 Thanks to: @bnitkin, @bugsounet, @dependabot, @jkriegshauser, @kaennchenstruggle, @KristjanESPERANTO and @Ybbet. Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome! This release also marks the latest release by Michael Teeuw. For more info, please read the following post: [A New Chapter for MagicMirror: The Community Takes the Lead](https://forum.magicmirror.builders/topic/18329/a-new-chapter-for-magicmirror-the-community-takes-the-lead). ### Added - Added update notification updater (for 3rd party modules) - Added node 21 to the test matrix - Added transform object to calendar:customEvents - Added ESLint rules for jest (including jest/expect-expect and jest/no-done-callback) ### Removed - Removed Codecov workflow (not working anymore, other workflow required) (#3107) - Removed titleReplace from calendar, replaced + extended by customEvents (backward compatibility included) (#3249) - Removed failing unit test (#3254) - Removed some unused variables ### Updated - Update electron to v27 and update other dependencies as well as github actions - Update newsfeed: Use `html-to-text` instead of regex for transform description - Review ESLint config (#3269) - Updated dependencies - Clock module: optionally display current moon phase in addition to rise/set times - electron is now per default started without gpu, if needed it must be enabled with new env var `ELECTRON_ENABLE_GPU=1` on startup (#3226) - Replace prettier by stylistic in ESLint config to lint JavaScript (and disable some rules for `config/config.js*` files) - Update node-ical to v0.17.1 and fix tests ### Fixed - Avoid fade out/in on updateDom when many calendars are used - Fix the option eventClass on customEvents. - Fix yr API version in locationforecast and sunrise call (#3227) - Fix cloneObject() function to respect RegExp (#3237) - Fix newsfeed module for feeds using "a10:updated" tag (#3238) - Fix issue template (#3167) - Fix #3256 filter out bad results from rrule.between - Fix calendar events sometimes not respecting deleted events (#3250) - Fix electron loadurl locally on Windows when address "0.0.0.0" (#2550) - Fix updatanotification (update_helper.js): catch error if reponse is not an JSON format (check PM2) - Fix missing typeof in calendar module - Fix style issues after prettier update - Fix calendar test (#3291) by moving "Exdate check" from e2e to electron to run on a Thursday - Fix calendar config params `fetchInterval` and `excludedEvents` were never used from single calendar config (#3297) - Fix MM_PORT variable not used in electron and allow full path for MM_CONFIG_FILE variable (#3302) --------- Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Karsten Hassel <hassel@gmx.de> Co-authored-by: Malte Hallström <46646495+SkySails@users.noreply.github.com> Co-authored-by: Veeck <github@veeck.de> Co-authored-by: veeck <michael@veeck.de> Co-authored-by: dWoolridge <dwoolridge@charter.net> Co-authored-by: Johan <jojjepersson@yahoo.se> Co-authored-by: Dario Mratovich <dario_mratovich@hotmail.com> Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com> Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com> Co-authored-by: Naveen <172697+naveensrinivasan@users.noreply.github.com> Co-authored-by: buxxi <buxxi@omfilm.net> Co-authored-by: Thomas Hirschberger <47733292+Tom-Hirschberger@users.noreply.github.com> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com> Co-authored-by: Dave Child <dave@addedbytes.com> Co-authored-by: grenagit <46225780+grenagit@users.noreply.github.com> Co-authored-by: Grena <grena@grenabox.fr> Co-authored-by: Magnus Marthinsen <magmar@online.no> Co-authored-by: Patrick <psieg@users.noreply.github.com> Co-authored-by: Piotr Rajnisz <56397164+rajniszp@users.noreply.github.com> Co-authored-by: Suthep Yonphimai <tomzt@users.noreply.github.com> Co-authored-by: CarJem Generations (Carter Wallace) <cwallacecs@gmail.com> Co-authored-by: Nicholas Fogal <nfogal.misc@gmail.com> Co-authored-by: JakeBinney <126349119+JakeBinney@users.noreply.github.com> Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com> Co-authored-by: Oscar Björkman <17575446+oscarb@users.noreply.github.com> Co-authored-by: Ismar Slomic <ismar@slomic.no> Co-authored-by: Jørgen Veum-Wahlberg <jorgen.wahlberg@amedia.no> Co-authored-by: Eddie Hung <6740044+eddiehung@users.noreply.github.com> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: bugsounet <bugsounet@bugsounet.fr> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Knapoc <Knapoc@users.noreply.github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: veeck <michael.veeck@nebenan.de> Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com> Co-authored-by: NolanKingdon <27908974+NolanKingdon@users.noreply.github.com> Co-authored-by: J. Kenzal Hunter <kenzal.hunter@gmail.com> Co-authored-by: Teddy <teddy.payet@gmail.com> Co-authored-by: TeddyStarinvest <teddy.payet@starinvest.com> Co-authored-by: martingron <61826403+martingron@users.noreply.github.com> Co-authored-by: dgoth <132394363+dgoth@users.noreply.github.com> Co-authored-by: kaennchenstruggle <54073894+kaennchenstruggle@users.noreply.github.com> Co-authored-by: jkriegshauser <jkriegshauser@gmail.com> Co-authored-by: Ben Nitkin <ben@nitkin.net>
410 lines
12 KiB
JavaScript
410 lines
12 KiB
JavaScript
/* MagicMirror²
|
|
* Module: NewsFeed
|
|
*
|
|
* By Michael Teeuw https://michaelteeuw.nl
|
|
* MIT Licensed.
|
|
*/
|
|
Module.register("newsfeed", {
|
|
// Default module config.
|
|
defaults: {
|
|
feeds: [
|
|
{
|
|
title: "New York Times",
|
|
url: "https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml",
|
|
encoding: "UTF-8" //ISO-8859-1
|
|
}
|
|
],
|
|
showAsList: false,
|
|
showSourceTitle: true,
|
|
showPublishDate: true,
|
|
broadcastNewsFeeds: true,
|
|
broadcastNewsUpdates: true,
|
|
showDescription: false,
|
|
showTitleAsUrl: false,
|
|
wrapTitle: true,
|
|
wrapDescription: true,
|
|
truncDescription: true,
|
|
lengthDescription: 400,
|
|
hideLoading: false,
|
|
reloadInterval: 5 * 60 * 1000, // every 5 minutes
|
|
updateInterval: 10 * 1000,
|
|
animationSpeed: 2.5 * 1000,
|
|
maxNewsItems: 0, // 0 for unlimited
|
|
ignoreOldItems: false,
|
|
ignoreOlderThan: 24 * 60 * 60 * 1000, // 1 day
|
|
removeStartTags: "",
|
|
removeEndTags: "",
|
|
startTags: [],
|
|
endTags: [],
|
|
prohibitedWords: [],
|
|
scrollLength: 500,
|
|
logFeedWarnings: false,
|
|
dangerouslyDisableAutoEscaping: false
|
|
},
|
|
|
|
getUrlPrefix (item) {
|
|
if (item.useCorsProxy) {
|
|
return `${location.protocol}//${location.host}/cors?url=`;
|
|
} else {
|
|
return "";
|
|
}
|
|
},
|
|
|
|
// Define required scripts.
|
|
getScripts () {
|
|
return ["moment.js"];
|
|
},
|
|
|
|
//Define required styles.
|
|
getStyles () {
|
|
return ["newsfeed.css"];
|
|
},
|
|
|
|
// Define required translations.
|
|
getTranslations () {
|
|
// 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.
|
|
// If you're trying to build your own module including translations, check out the documentation.
|
|
return false;
|
|
},
|
|
|
|
// Define start sequence.
|
|
start () {
|
|
Log.info(`Starting module: ${this.name}`);
|
|
|
|
// Set locale.
|
|
moment.locale(config.language);
|
|
|
|
this.newsItems = [];
|
|
this.loaded = false;
|
|
this.error = null;
|
|
this.activeItem = 0;
|
|
this.scrollPosition = 0;
|
|
|
|
this.registerFeeds();
|
|
|
|
this.isShowingDescription = this.config.showDescription;
|
|
},
|
|
|
|
// Override socket notification handler.
|
|
socketNotificationReceived (notification, payload) {
|
|
if (notification === "NEWS_ITEMS") {
|
|
this.generateFeed(payload);
|
|
|
|
if (!this.loaded) {
|
|
if (this.config.hideLoading) {
|
|
this.show();
|
|
}
|
|
this.scheduleUpdateInterval();
|
|
}
|
|
|
|
this.loaded = true;
|
|
this.error = null;
|
|
} else if (notification === "NEWSFEED_ERROR") {
|
|
this.error = this.translate(payload.error_type);
|
|
this.scheduleUpdateInterval();
|
|
}
|
|
},
|
|
|
|
//Override fetching of template name
|
|
getTemplate () {
|
|
if (this.config.feedUrl) {
|
|
return "oldconfig.njk";
|
|
} else if (this.config.showFullArticle) {
|
|
return "fullarticle.njk";
|
|
}
|
|
return "newsfeed.njk";
|
|
},
|
|
|
|
//Override template data and return whats used for the current template
|
|
getTemplateData () {
|
|
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
|
|
if (this.config.showFullArticle) {
|
|
return {
|
|
url: this.getActiveItemURL()
|
|
};
|
|
}
|
|
if (this.error) {
|
|
return {
|
|
error: this.error
|
|
};
|
|
}
|
|
if (this.newsItems.length === 0) {
|
|
return {
|
|
empty: true
|
|
};
|
|
}
|
|
if (this.activeItem >= this.newsItems.length) {
|
|
this.activeItem = 0;
|
|
}
|
|
|
|
const item = this.newsItems[this.activeItem];
|
|
const items = this.newsItems.map(function (item) {
|
|
item.publishDate = moment(new Date(item.pubdate)).fromNow();
|
|
return item;
|
|
});
|
|
|
|
return {
|
|
loaded: true,
|
|
config: this.config,
|
|
sourceTitle: item.sourceTitle,
|
|
publishDate: moment(new Date(item.pubdate)).fromNow(),
|
|
title: item.title,
|
|
url: this.getUrlPrefix(item) + item.url,
|
|
description: item.description,
|
|
items: items
|
|
};
|
|
},
|
|
|
|
getActiveItemURL () {
|
|
const item = this.newsItems[this.activeItem];
|
|
if (item) {
|
|
return typeof item.url === "string" ? this.getUrlPrefix(item) + item.url : this.getUrlPrefix(item) + item.url.href;
|
|
} else {
|
|
return "";
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Registers the feeds to be used by the backend.
|
|
*/
|
|
registerFeeds () {
|
|
for (let feed of this.config.feeds) {
|
|
this.sendSocketNotification("ADD_FEED", {
|
|
feed: feed,
|
|
config: this.config
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Generate an ordered list of items for this configured module.
|
|
* @param {object} feeds An object with feeds returned by the node helper.
|
|
*/
|
|
generateFeed (feeds) {
|
|
let newsItems = [];
|
|
for (let feed in feeds) {
|
|
const feedItems = feeds[feed];
|
|
if (this.subscribedToFeed(feed)) {
|
|
for (let item of feedItems) {
|
|
item.sourceTitle = this.titleForFeed(feed);
|
|
if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) {
|
|
newsItems.push(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
newsItems.sort(function (a, b) {
|
|
const dateA = new Date(a.pubdate);
|
|
const dateB = new Date(b.pubdate);
|
|
return dateB - dateA;
|
|
});
|
|
|
|
if (this.config.maxNewsItems > 0) {
|
|
newsItems = newsItems.slice(0, this.config.maxNewsItems);
|
|
}
|
|
|
|
if (this.config.prohibitedWords.length > 0) {
|
|
newsItems = newsItems.filter(function (item) {
|
|
for (let word of this.config.prohibitedWords) {
|
|
if (item.title.toLowerCase().indexOf(word.toLowerCase()) > -1) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}, this);
|
|
}
|
|
newsItems.forEach((item) => {
|
|
//Remove selected tags from the beginning of rss feed items (title or description)
|
|
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
|
|
for (let startTag of this.config.startTags) {
|
|
if (item.title.slice(0, startTag.length) === startTag) {
|
|
item.title = item.title.slice(startTag.length, item.title.length);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") {
|
|
if (this.isShowingDescription) {
|
|
for (let startTag of this.config.startTags) {
|
|
if (item.description.slice(0, startTag.length) === startTag) {
|
|
item.description = item.description.slice(startTag.length, item.description.length);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Remove selected tags from the end of rss feed items (title or description)
|
|
if (this.config.removeEndTags) {
|
|
for (let endTag of this.config.endTags) {
|
|
if (item.title.slice(-endTag.length) === endTag) {
|
|
item.title = item.title.slice(0, -endTag.length);
|
|
}
|
|
}
|
|
|
|
if (this.isShowingDescription) {
|
|
for (let endTag of this.config.endTags) {
|
|
if (item.description.slice(-endTag.length) === endTag) {
|
|
item.description = item.description.slice(0, -endTag.length);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// get updated news items and broadcast them
|
|
const updatedItems = [];
|
|
newsItems.forEach((value) => {
|
|
if (this.newsItems.findIndex((value1) => value1 === value) === -1) {
|
|
// Add item to updated items list
|
|
updatedItems.push(value);
|
|
}
|
|
});
|
|
|
|
// check if updated items exist, if so and if we should broadcast these updates, then lets do so
|
|
if (this.config.broadcastNewsUpdates && updatedItems.length > 0) {
|
|
this.sendNotification("NEWS_FEED_UPDATE", { items: updatedItems });
|
|
}
|
|
|
|
this.newsItems = newsItems;
|
|
},
|
|
|
|
/**
|
|
* Check if this module is configured to show this feed.
|
|
* @param {string} feedUrl Url of the feed to check.
|
|
* @returns {boolean} True if it is subscribed, false otherwise
|
|
*/
|
|
subscribedToFeed (feedUrl) {
|
|
for (let feed of this.config.feeds) {
|
|
if (feed.url === feedUrl) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Returns title for the specific feed url.
|
|
* @param {string} feedUrl Url of the feed
|
|
* @returns {string} The title of the feed
|
|
*/
|
|
titleForFeed (feedUrl) {
|
|
for (let feed of this.config.feeds) {
|
|
if (feed.url === feedUrl) {
|
|
return feed.title || "";
|
|
}
|
|
}
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* Schedule visual update.
|
|
*/
|
|
scheduleUpdateInterval () {
|
|
this.updateDom(this.config.animationSpeed);
|
|
|
|
// Broadcast NewsFeed if needed
|
|
if (this.config.broadcastNewsFeeds) {
|
|
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
|
}
|
|
|
|
// #2638 Clear timer if it already exists
|
|
if (this.timer) clearInterval(this.timer);
|
|
|
|
this.timer = setInterval(() => {
|
|
this.activeItem++;
|
|
this.updateDom(this.config.animationSpeed);
|
|
|
|
// Broadcast NewsFeed if needed
|
|
if (this.config.broadcastNewsFeeds) {
|
|
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
|
}
|
|
}, this.config.updateInterval);
|
|
},
|
|
|
|
resetDescrOrFullArticleAndTimer () {
|
|
this.isShowingDescription = this.config.showDescription;
|
|
this.config.showFullArticle = false;
|
|
this.scrollPosition = 0;
|
|
// reset bottom bar alignment
|
|
document.getElementsByClassName("region bottom bar")[0].classList.remove("newsfeed-fullarticle");
|
|
if (!this.timer) {
|
|
this.scheduleUpdateInterval();
|
|
}
|
|
},
|
|
|
|
notificationReceived (notification, payload, sender) {
|
|
const before = this.activeItem;
|
|
if (notification === "MODULE_DOM_CREATED" && this.config.hideLoading) {
|
|
this.hide();
|
|
} else if (notification === "ARTICLE_NEXT") {
|
|
this.activeItem++;
|
|
if (this.activeItem >= this.newsItems.length) {
|
|
this.activeItem = 0;
|
|
}
|
|
this.resetDescrOrFullArticleAndTimer();
|
|
Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
|
this.updateDom(100);
|
|
} else if (notification === "ARTICLE_PREVIOUS") {
|
|
this.activeItem--;
|
|
if (this.activeItem < 0) {
|
|
this.activeItem = this.newsItems.length - 1;
|
|
}
|
|
this.resetDescrOrFullArticleAndTimer();
|
|
Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
|
this.updateDom(100);
|
|
}
|
|
// if "more details" is received the first time: show article summary, on second time show full article
|
|
else if (notification === "ARTICLE_MORE_DETAILS") {
|
|
// full article is already showing, so scrolling down
|
|
if (this.config.showFullArticle === true) {
|
|
this.scrollPosition += this.config.scrollLength;
|
|
window.scrollTo(0, this.scrollPosition);
|
|
Log.debug(`${this.name} - scrolling down`);
|
|
Log.debug(`${this.name} - ARTICLE_MORE_DETAILS, scroll position: ${this.config.scrollLength}`);
|
|
} else {
|
|
this.showFullArticle();
|
|
}
|
|
} else if (notification === "ARTICLE_SCROLL_UP") {
|
|
if (this.config.showFullArticle === true) {
|
|
this.scrollPosition -= this.config.scrollLength;
|
|
window.scrollTo(0, this.scrollPosition);
|
|
Log.debug(`${this.name} - scrolling up`);
|
|
Log.debug(`${this.name} - ARTICLE_SCROLL_UP, scroll position: ${this.config.scrollLength}`);
|
|
}
|
|
} else if (notification === "ARTICLE_LESS_DETAILS") {
|
|
this.resetDescrOrFullArticleAndTimer();
|
|
Log.debug(`${this.name} - showing only article titles again`);
|
|
this.updateDom(100);
|
|
} else if (notification === "ARTICLE_TOGGLE_FULL") {
|
|
if (this.config.showFullArticle) {
|
|
this.activeItem++;
|
|
this.resetDescrOrFullArticleAndTimer();
|
|
} else {
|
|
this.showFullArticle();
|
|
}
|
|
} else if (notification === "ARTICLE_INFO_REQUEST") {
|
|
this.sendNotification("ARTICLE_INFO_RESPONSE", {
|
|
title: this.newsItems[this.activeItem].title,
|
|
source: this.newsItems[this.activeItem].sourceTitle,
|
|
date: this.newsItems[this.activeItem].pubdate,
|
|
desc: this.newsItems[this.activeItem].description,
|
|
url: this.getActiveItemURL()
|
|
});
|
|
}
|
|
},
|
|
|
|
showFullArticle () {
|
|
this.isShowingDescription = !this.isShowingDescription;
|
|
this.config.showFullArticle = !this.isShowingDescription;
|
|
// make bottom bar align to top to allow scrolling
|
|
if (this.config.showFullArticle === true) {
|
|
document.getElementsByClassName("region bottom bar")[0].classList.add("newsfeed-fullarticle");
|
|
}
|
|
clearInterval(this.timer);
|
|
this.timer = null;
|
|
Log.debug(`${this.name} - showing ${this.isShowingDescription ? "article description" : "full article"}`);
|
|
this.updateDom(100);
|
|
}
|
|
});
|