Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement very crude and bare-bones RSS feed #5047

Merged
merged 5 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"express": "~4.19.2",
"express-basic-auth": "~1.2.1",
"express-static-gzip": "~2.1.7",
"feed": "^4.2.2",
"form-data": "~4.0.0",
"gamedig": "^4.2.0",
"html-escaper": "^3.0.3",
Expand Down
81 changes: 81 additions & 0 deletions server/model/status_page.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
const jsesc = require("jsesc");
const googleAnalytics = require("../google-analytics");
const { marked } = require("marked");
const { Feed } = require("feed");
const config = require("../config");

class StatusPage extends BeanModel {

Expand All @@ -14,6 +16,19 @@
*/
static domainMappingList = { };

/**
* Handle responses to RSS pages
* @param {Response} response Response object
* @param {string} slug Status page slug
* @returns {Promise<void>}
*/
static async handleStatusPageRSSResponse(response, slug) {
let statusPage = await R.findOne("status_page", " slug = ? ", [
MrYakobo marked this conversation as resolved.
Show resolved Hide resolved
slug
]);
response.send(await StatusPage.renderRSS(statusPage, slug));
CommanderStorm marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Handle responses to status page
* @param {Response} response Response object
Expand All @@ -39,6 +54,38 @@
}
}

/**
* SSR for RSS feed
* @param {statusPage} statusPage object
* @param {slug} slug from router
* @returns {Promise<string>} the rendered html
*/
static async renderRSS(statusPage, slug) {
const { heartbeats } = await StatusPage.getRSSPageData(statusPage);

let proto = config.isSSL ? "https" : "http";
let host = `${proto}://${config.hostname || "localhost"}:${config.port}/status/${slug}`;

const feed = new Feed({
title: "uptime kuma rss feed",
description: "feed for monitors that are down",
link: host,
language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
updated: new Date(), // optional, default = today
});

heartbeats.forEach(heartbeat => {
feed.addItem({
title: `${heartbeat.name} is down`,
description: `${heartbeat.name} has been down since ${heartbeat.time}`,
Copy link
Collaborator

@CommanderStorm CommanderStorm Aug 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite true.
That is just the last heartbeat that failed.
That description however can be misinterpreted as the first DOWN heartbeat (=> the total downtime for said heatbeat).

Also: I don't think that it has to be down, right? (we also have MAINTENANCE, UP and Pending as statuses, but I would need to look up if they can be heartbeats)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this code in StatusPage.vue that interprets the status field from the database:

            let status = STATUS_PAGE_ALL_UP;
            let hasUp = false;

            for (let id in this.$root.publicLastHeartbeatList) {
                let beat = this.$root.publicLastHeartbeatList[id];

                if (beat.status === MAINTENANCE) {
                    return STATUS_PAGE_MAINTENANCE;
                } else if (beat.status === UP) {
                    hasUp = true;
                } else {
                    status = STATUS_PAGE_PARTIAL_DOWN;
                }
            }

            if (! hasUp) {
                status = STATUS_PAGE_ALL_DOWN;
            }

            return status;

I'll do something similar for the RSS feed 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To your questions,

That is just the last heartbeat that failed.
That description however can be misinterpreted as the first DOWN heartbeat (=> the total downtime for said heatbeat).

in getRSSPageData, I'm filtering for the last heartbeat of monitor_id=?. If that heartbeat is falsy, its added to the RSS feed. When the monitor is UP again, the RSS feed won't publish it anymore. I think this is the desired behaviour.

Copy link
Collaborator

@CommanderStorm CommanderStorm Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filtering for DOWN sounds fine (I don't use RSS-Feeds, your call).

Have you tested that a falsy heartbeat means what you think it does?
Only the following values are falsy: https://developer.mozilla.org/en-US/docs/Glossary/Falsy

=> you are not filtering for the things you think you are..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great call mate. I've not worked with js for many years, so I'm a little bit rusty on these quirks 👍

id: heartbeat.monitorID,
date: new Date(heartbeat.time),
});
});

return feed.rss2();
}

/**
* SSR for status pages
* @param {string} indexHTML HTML page to render
Expand Down Expand Up @@ -98,6 +145,40 @@
return $.root().html();
}

/**
* Get all data required for RSS
* @param {StatusPage} statusPage Status page to get data for
* @returns {object} Status page data
*/
static async getRSSPageData(statusPage) {
// get all heartbeats that correspond to this statusPage
const config = await statusPage.toPublicJSON();

// Public Group List
const showTags = !!statusPage.show_tags;

const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);

let heartbeats = [];

for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
for (const a of monitorGroup.monitorList) {
const id = a.id;
const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ id ]);
CommanderStorm marked this conversation as resolved.
Show resolved Hide resolved
if (heartbeat && !heartbeat.status) {
heartbeats.push({ ...a, status: heartbeat.status, time: heartbeat.time });

Check failure on line 172 in server/model/status_page.js

View workflow job for this annotation

GitHub Actions / check-linters

Object properties must go on a new line

Check failure on line 172 in server/model/status_page.js

View workflow job for this annotation

GitHub Actions / check-linters

Object properties must go on a new line
MrYakobo marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

return {
heartbeats
};
}

/**
* Get all status page data in one call
* @param {StatusPage} statusPage Status page to get data for
Expand Down
5 changes: 5 additions & 0 deletions server/routers/status-page-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});

router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => {
let slug = request.params.slug;
await StatusPage.handleStatusPageRSSResponse(response, slug);
});

router.get("/status", cache("5 minutes"), async (request, response) => {
let slug = "default";
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
Expand Down
Loading