Skip to content

Commit

Permalink
feat: implement very crude and bare-bones RSS feed (louislam#5047)
Browse files Browse the repository at this point in the history
  • Loading branch information
MrYakobo authored Sep 3, 2024
1 parent 11f108b commit 935194b
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 0 deletions.
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
157 changes: 157 additions & 0 deletions server/model/status_page.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
const jsesc = require("jsesc");
const googleAnalytics = require("../google-analytics");
const { marked } = require("marked");
const { Feed } = require("feed");
const config = require("../config");

const { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE, DOWN } = require("../../src/util");

class StatusPage extends BeanModel {

Expand All @@ -14,6 +18,24 @@ class StatusPage extends BeanModel {
*/
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 = ? ", [
slug
]);

if (statusPage) {
response.send(await StatusPage.renderRSS(statusPage, slug));
} else {
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
}
}

/**
* Handle responses to status page
* @param {Response} response Response object
Expand All @@ -39,6 +61,38 @@ class StatusPage extends BeanModel {
}
}

/**
* 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, statusDescription } = 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: `current status: ${statusDescription}`,
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}`,
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 +152,109 @@ class StatusPage extends BeanModel {
return $.root().html();
}

/**
* @param {heartbeats} heartbeats from getRSSPageData
* @returns {number} status_page constant from util.ts
*/
static overallStatus(heartbeats) {
if (heartbeats.length === 0) {
return -1;
}

let status = STATUS_PAGE_ALL_UP;
let hasUp = false;

for (let beat of heartbeats) {
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;
}

/**
* @param {number} status from overallStatus
* @returns {string} description
*/
static getStatusDescription(status) {
if (status === -1) {
return "No Services";
}

if (status === STATUS_PAGE_ALL_UP) {
return "All Systems Operational";
}

if (status === STATUS_PAGE_PARTIAL_DOWN) {
return "Partially Degraded Service";
}

if (status === STATUS_PAGE_ALL_DOWN) {
return "Degraded Service";
}

// TODO: show the real maintenance information: title, description, time
if (status === MAINTENANCE) {
return "Under maintenance";
}

return "?";
}

/**
* 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 monitor of monitorGroup.monitorList) {
const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]);
if (heartbeat) {
heartbeats.push({
...monitor,
status: heartbeat.status,
time: heartbeat.time
});
}
}
}

// calculate RSS feed description
let status = StatusPage.overallStatus(heartbeats);
let statusDescription = StatusPage.getStatusDescription(status);

// keep only DOWN heartbeats in the RSS feed
heartbeats = heartbeats.filter(heartbeat => heartbeat.status === DOWN);

return {
heartbeats,
statusDescription
};
}

/**
* 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

0 comments on commit 935194b

Please sign in to comment.