mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-28 20:22:53 +00:00
fixes #3613 wanted to write a test for `basePath` but have no idea at the moment to simulate this without a reverse proxy. Here my test setup for documentation: ```yaml networks: proxy: driver: bridge services: socket-proxy: privileged: true image: tecnativa/docker-socket-proxy:edge container_name: socket-proxy restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock:ro environment: CONTAINERS: 1 ports: - "127.0.0.1:2375:2375" networks: - proxy traefik: image: traefik:latest container_name: traefik restart: unless-stopped user: 1000:1000 command: - "--providers.docker=true" - "--providers.docker.network=traefik_proxy" - "--providers.docker.endpoint=tcp://socket-proxy:2375" - "--entryPoints.http.address=:80" - "--global.sendAnonymousUsage=false" - "--log.level=INFO" - "--api=true" - "--api.dashboard=true" # - "--accessLog=true" # - "--accesslog.fields.defaultmode=keep" # - "--accesslog.fields.headers.defaultmode=keep" networks: - proxy ports: - "80:80" magicmirror: image: karsten13/magicmirror:develop container_name: mm restart: unless-stopped entrypoint: - sleep - infinity networks: - proxy labels: - "traefik.http.services.karsten13.loadbalancer.server.port=8080" - "traefik.http.routers.k13-http.service=karsten13" - "traefik.http.routers.k13-http.entrypoints=http" - "traefik.http.routers.k13-http.rule=Host(`localhost`) && PathPrefix(`/testbasepath`)" - "traefik.http.middlewares.k13-stripprefix.stripprefix.prefixes=/testbasepath" - "traefik.http.routers.k13-http.middlewares=k13-stripprefix" ```
429 lines
13 KiB
JavaScript
429 lines
13 KiB
JavaScript
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}${config.basePath}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 () {
|
|
if (this.activeItem >= this.newsItems.length) {
|
|
this.activeItem = 0;
|
|
}
|
|
this.activeItemCount = this.newsItems.length;
|
|
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
|
|
if (this.config.showFullArticle) {
|
|
this.activeItemHash = this.newsItems[this.activeItem]?.hash;
|
|
return {
|
|
url: this.getActiveItemURL()
|
|
};
|
|
}
|
|
if (this.error) {
|
|
this.activeItemHash = undefined;
|
|
return {
|
|
error: this.error
|
|
};
|
|
}
|
|
if (this.newsItems.length === 0) {
|
|
this.activeItemHash = undefined;
|
|
return {
|
|
empty: true
|
|
};
|
|
}
|
|
|
|
const item = this.newsItems[this.activeItem];
|
|
this.activeItemHash = item.hash;
|
|
|
|
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.getActiveItemURL(),
|
|
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(() => {
|
|
|
|
/*
|
|
* When animations are enabled, don't update the DOM unless we are actually changing what we are displaying.
|
|
* (Animating from a headline to itself is unsightly.)
|
|
* Cases:
|
|
*
|
|
* Number of items | Number of items | Display
|
|
* at last update | right now | Behaviour
|
|
* ----------------------------------------------------
|
|
* 0 | 0 | do not update
|
|
* 0 | >0 | update
|
|
* 1 | 0 or >1 | update
|
|
* 1 | 1 | update only if item details (hash value) changed
|
|
* >1 | any | update
|
|
*
|
|
* (N.B. We set activeItemCount and activeItemHash in getTemplateData().)
|
|
*/
|
|
if (this.newsItems.length > 1 || this.newsItems.length !== this.activeItemCount || this.activeItemHash !== this.newsItems[0]?.hash) {
|
|
this.activeItem++; // this is OK if newsItems.Length==1; getTemplateData will wrap it around
|
|
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);
|
|
}
|
|
});
|