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

Implement template engine for message [On Hold for #760] #751

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
12,910 changes: 258 additions & 12,652 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@
"axios": "~0.21.4",
"bcryptjs": "~2.4.3",
"bootstrap": "~5.1.1",
"chardet": "^1.3.0",
"bree": "~6.3.1",
"chardet": "^1.3.0",
"chart.js": "~3.5.1",
"chartjs-adapter-dayjs": "~1.0.0",
"command-exists": "~1.2.9",
Expand All @@ -75,6 +75,7 @@
"http-graceful-shutdown": "~3.1.4",
"iconv-lite": "^0.6.3",
"jsonwebtoken": "~8.5.1",
"liquidjs": "^9.28.1",
"nodemailer": "~6.6.5",
"notp": "~2.0.3",
"password-hash": "~1.2.2",
Expand Down
165 changes: 152 additions & 13 deletions server/notification.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
const { R } = require("redbean-node");
const { Liquid } = require( "liquidjs");
const { UP } = require("../src/util");
const dayjs = require("dayjs");

const engine = new Liquid();

const Apprise = require("./notification-providers/apprise");
const Discord = require("./notification-providers/discord");
const Gotify = require("./notification-providers/gotify");
Expand All @@ -22,8 +28,99 @@ const Feishu = require("./notification-providers/feishu");
const AliyunSms = require("./notification-providers/aliyun-sms");
const DingDing = require("./notification-providers/dingding");

const MinimalDetailTemplate = "{{monitor.name}}: {{monitor.health}}";
const LowDetailTemplate = "[{{monitor.name}}] [{{monitor.health}}] {{heartbeat.msg}}";
const MediumDetailTemplate = `Monitor: {{monitor.name}}
Health: {{monitor.health}}
Address: {{monitor.url}}

{% if heartbeat.status == 1 and monitor.upsideDown -%}
Your Upside down {{monitor.type}} monitor is unexpectedly connected
{%- elsif heartbeat.status == 1 and monitor.upsideDown==false -%}
Your {{monitor.type}} monitor is up
{%- elsif heartbeat.status == 0 and monitor.upsideDown -%}
Your Upside down {{monitor.type}} monitor is no longer connected
{%- elsif heartbeat.status == 0 and monitor.upsideDown == false -%}
Your {{monitor.type}} monitor is unexpectedly down.
{%- endif %}
Time: {{heartbeat.time}}
Uptime Message: {{heartbeat.msg}}`;

const FullDetailTemplate = `Monitor: {{monitor.name}}
Health: {{monitor.health}}
Address: {{monitor.url}}

{% if heartbeat.status == 1 and monitor.upsideDown -%}
Your Upside down {{monitor.type}} monitor is unexpectedly connected
{%- elsif heartbeat.status == 1 and monitor.upsideDown==false -%}
Your {{monitor.type}} monitor is up
{%- elsif heartbeat.status == 0 and monitor.upsideDown -%}
Your Upside down {{monitor.type}} monitor is no longer connected
{%- elsif heartbeat.status == 0 and monitor.upsideDown == false -%}
Your {{monitor.type}} monitor is unexpectedly down.
{%- endif %}
Time: {{heartbeat.time}}
Uptime Message: {{heartbeat.msg}}

Tags
----------------------------------------
{% for tag in monitor.tags -%}
{{ tag.name }}
{%- if tag.value and tag.value != "" -%}
: {{tag.value}}
{%- endif %}
{% endfor -%}`;

class Notification {

static generateTestHeartbeat() {
return {
monitorID: 5,
status: 1,
time: R.isoDateTime(dayjs.utc()),
msg: "TEST NOTIFICATION MESSAGE",
ping: 278,
important: true,
duration: 8,
};
}

static generateTestMonitor() {
return {
id: 5,
name: "Test Notification Monitor",
url: "https://www.example.com",
method: "Get",
body: "OK",
headers: null,
hostname: "www.example.com",
port: 443,
maxretries: 2,
weight: 2000,
active: 1,
type: "HTTP",
interval: 60,
retryInterval: this.retryInterval,
keyword: null,
ignoreTls: false,
upsideDown: false,
maxredirects: 10,
accepted_statuscodes: ["200-299"],
dns_resolve_type: "A",
dns_resolve_server: "1.1.1.1",
dns_last_result: null,
pushToken: null,
notificationIDList: { "1": true,
"5": true },
Comment on lines +113 to +114
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
notificationIDList: { "1": true,
"5": true },
notificationIDList: {
"1": true,
"5": true,
},

tags: [{ "id": 21,
"monitor_id": 16,
"tag_id": 2,
"value": "",
"name": "Internal",
"color": "#059669" }],
Comment on lines +115 to +120
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
tags: [{ "id": 21,
"monitor_id": 16,
"tag_id": 2,
"value": "",
"name": "Internal",
"color": "#059669" }],
tags: [{
"id": 21,
"monitor_id": 16,
"tag_id": 2,
"value": "",
"name": "Internal",
"color": "#059669",
}],

};
}

providerList = {};

static init() {
Expand Down Expand Up @@ -77,36 +174,78 @@ class Notification {
* @returns {Promise<string>} Successful msg
* Throw Error with fail msg
*/
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
static async send(notification, msg, monitorJSON, heartbeatJSON) {
Copy link
Owner

Choose a reason for hiding this comment

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

The method signature should not be changed.

As I said in: #646 (comment)

This is used for general message.

Notification.send(notification, "Hello👋");

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 have been wondering about that. There were only two calls to send, and now the test call has example monitors and heartbeats. As such, there are no calls for a general message. Can you expand on what you plan on using that for beyond the test message?

I wonder especially given the number of special cases that pop up around the more complicated notifiers, if it would not be better to split out those calls explicitly instead of having one function with logic for both cases.

Notification.notify(notification,monitor,heartbeat) for notification data
Notification.message(notification,message) for simple messages

It would make it a larger change, but given I am already going to have to go through basically every notification type to verify/test the templates and notifications anyway, I don't think that it would be all that much more.

Copy link
Owner

Choose a reason for hiding this comment

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

Planning for more notification such as domain renewal, cert renewal remind.

Copy link
Collaborator

Choose a reason for hiding this comment

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

IMO if the function name is send, then it should really be as accommodating as possible with respect to what can be sent, so yeah it would be a problem if in the future there are other things to send. Keep extending send() with more parameters doesn't seem like a good practice anyway. I haven't really thought about this, but is it possible that send is just send(notification, httpBody), then we can have a method for each type of notification which wraps the send() call, maybe something like sendDown() sendUp() sendCertExp() etc?

if (this.providerList[notification.type]) {
return this.providerList[notification.type].send(notification, msg, monitorJSON, heartbeatJSON);

monitorJSON.health = ((heartbeatJSON.status == 1) !== monitorJSON.upsideDown) ? "✅ Healthy" : "❌ Unhealthy";

let parseData = {
// I actually dont think that it is necessary to put the notification in the data sent to the template.
// notification: notification,
monitor: monitorJSON,
heartbeat: heartbeatJSON,
};
let template = this.getTemplateFromNotification(notification);
console.log(`Template: (${template})`);
let message = await engine.parseAndRender(template, parseData);

return this.providerList[notification.type].send(notification, message, monitorJSON, heartbeatJSON);

//Removed try-catch here. I am not sure what the default should be in the case of a broken template.
//switch to manually building the message?
//the problem is that it would still need to send a message after the template fails of if it failed to send completely..
//im not sure if that is the desired result on a template fail.

} else {
throw new Error("Notification type is not supported");
}
}

static getTemplateFromNotification(notification) {

Comment on lines +204 to +205
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
static getTemplateFromNotification(notification) {
static getTemplateFromNotification(notification) {

let template = notification.template;
let detail = notification.detail;
console.log(`Detail: (${detail}) Template: (${template})`);
switch (detail) {
case "Minimal Detail":
return MinimalDetailTemplate;
case "Low Detail":
return LowDetailTemplate;
case "Medium Detail":
return MediumDetailTemplate;
case "Full Detail":
return FullDetailTemplate;
case "Custom Template":
if (template) {
return template;
}
//returns low in the case of a template being empty string or undefined.
}
return LowDetailTemplate;
}

static async save(notification, notificationID, userID) {
let bean
let bean;

if (notificationID) {
bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
notificationID,
userID,
])
]);

if (! bean) {
throw new Error("notification not found")
throw new Error("notification not found");
}

} else {
bean = R.dispense("notification")
bean = R.dispense("notification");
}

bean.name = notification.name;
bean.user_id = userID;
bean.config = JSON.stringify(notification);
bean.is_default = notification.isDefault || false;
await R.store(bean)
await R.store(bean);

if (notification.applyExisting) {
await applyNotificationEveryMonitor(bean.id, userID);
Expand All @@ -119,13 +258,13 @@ class Notification {
let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
notificationID,
userID,
])
]);

if (! bean) {
throw new Error("notification not found")
throw new Error("notification not found");
}

await R.trash(bean)
await R.trash(bean);
}

static checkApprise() {
Expand All @@ -145,17 +284,17 @@ async function applyNotificationEveryMonitor(notificationID, userID) {
let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [
monitors[i].id,
notificationID,
])
]);

if (! checkNotification) {
let relation = R.dispense("monitor_notification");
relation.monitor_id = monitors[i].id;
relation.notification_id = notificationID;
await R.store(relation)
await R.store(relation);
}
}
}

module.exports = {
Notification,
}
};
3 changes: 2 additions & 1 deletion server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,8 @@ exports.entryPage = "dashboard";
try {
checkLogin(socket);

let msg = await Notification.send(notification, notification.name + " Testing");
let notificationText = `[Monitor Name] [Up/Down] Status Message (${notification.name} Testing)`;
let msg = await Notification.send(notification, notificationText, Notification.generateTestMonitor(), Notification.generateTestHeartbeat());

callback({
ok: true,
Expand Down
33 changes: 31 additions & 2 deletions src/components/NotificationDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@
<input id="notification-name" v-model="notification.name" type="text" class="form-control" required>
</div>

<div v-show="enableTemplateOptions" class="mb-3">
<label for="notification-detail" class="form-label">{{ $t("Notification Message Detail") }}</label>
<select id="notification-detail" v-model="notification.detail" class="form-select">
<option v-for="detail in detailLevels" :key="detail" :value="detail">{{ $t(detail) }}</option>
</select>
</div>

<!-- using show so that if the user toggels to a different template level, they dont loose what is in the field. -->
<div v-show="notification.detail === 'Custom Template' && enableTemplateOptions" class="mb-3">
<label for="notification-text" class="form-label">{{ $t("Custom Message Template") }}</label>
<textarea id="notification-text" v-model="notification.template" type="text" class="form-control"></textarea>
<div v-pre class="form-text">
Uses Liquid templates Via LiquidJS.<br />
See <a href="https://github.com/louislam/uptime-kuma/wiki">the Uptime Kuma Wiki</a> for full detauls.<br />
</div>
</div>

<!-- form body -->
<component :is="currentForm" />

Expand Down Expand Up @@ -72,7 +89,8 @@ import { Modal } from "bootstrap";
import { ucfirst } from "../util.ts";

import Confirm from "./Confirm.vue";
import NotificationFormList from "./notifications";
import { NotificationFormList, NotificationDetailList, TemplateEnabledList } from "./notifications";
// import from "./notifications";
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// import from "./notifications";


export default {
components: {
Expand All @@ -92,7 +110,9 @@ export default {
type: null,
isDefault: false,
// Do not set default value here, please scroll to show()
}
},
detailLevels: NotificationDetailList,

Comment on lines +114 to +115
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
detailLevels: NotificationDetailList,
detailLevels: NotificationDetailList,

};
},

Expand All @@ -102,6 +122,9 @@ export default {
return null;
}
return NotificationFormList[this.notification.type];
},
enableTemplateOptions() {
return (TemplateEnabledList.includes(this.notification.type));
}
},

Expand All @@ -118,6 +141,9 @@ export default {
this.notification.name = this.getUniqueDefaultName(to);
}
},
"notification.detail"(to, from) {
this.notification.detail = to;
}
},
mounted() {
this.modal = new Modal(this.$refs.modal);
Expand Down Expand Up @@ -149,6 +175,9 @@ export default {

// Set Default value here
this.notification.type = this.notificationTypes[0];
this.notification.detail = this.detailLevels[1];
this.notification.template = "[{{monitor.name}}] [{{monitor.health}}] {{monitor.msg}}";

Comment on lines +179 to +180
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
this.notification.template = "[{{monitor.name}}] [{{monitor.health}}] {{monitor.msg}}";
this.notification.template = "[{{monitor.name}}] [{{monitor.health}}] {{monitor.msg}}";

}

this.modal.show();
Expand Down
15 changes: 13 additions & 2 deletions src/components/notifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import DingDing from "./DingDing.vue";
*
* @type { Record<string, any> }
*/
const NotificationFormList = {
export const NotificationFormList = {
"telegram": Telegram,
"webhook": Webhook,
"smtp": STMP,
Expand All @@ -51,4 +51,15 @@ const NotificationFormList = {
"DingDing": DingDing
}

export default NotificationFormList
export const TemplateEnabledList = [
"smtp"
]

export const NotificationDetailList = [
"Minimal Detail",
"Low Detail",
"Medium Detail",
"Full Detail",
"Custom Template",
]

12 changes: 12 additions & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,16 @@ export default {
"One record": "One record",
"Showing {from} to {to} of {count} records": "Showing {from} to {to} of {count} records",
steamApiKeyDescription: "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ",
"Message Template":"Message Template",
"Default Template":"[{{monitor.name}}] [{{monitor.health}}] {{monitor.msg}}",
HealthyStatus:"✅ Healthy",
UnhealthyStatus:"❌ Unhealthy",
Comment on lines +309 to +312
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"Message Template":"Message Template",
"Default Template":"[{{monitor.name}}] [{{monitor.health}}] {{monitor.msg}}",
HealthyStatus:"✅ Healthy",
UnhealthyStatus:"❌ Unhealthy",
"Message Template": "Message Template",
"Default Template": "[{{monitor.name}}] [{{monitor.health}}] {{monitor.msg}}",
HealthyStatus: "✅ Healthy",
UnhealthyStatus: "❌ Unhealthy",

//template levels
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
//template levels
// template levels

"Notification Message Detail":"Notification Message Detail",

"Minimal Detail":"Minimal Detail",
"Low Detail":"Low Detail",
"Medium Detail":"Medium Detail",
"Full Detail":"Full Detail",
"Custom Template":"Custom Template",
Comment on lines +314 to +320
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"Notification Message Detail":"Notification Message Detail",
"Minimal Detail":"Minimal Detail",
"Low Detail":"Low Detail",
"Medium Detail":"Medium Detail",
"Full Detail":"Full Detail",
"Custom Template":"Custom Template",
"Notification Message Detail": "Notification Message Detail",
"Minimal Detail": "Minimal Detail",
"Low Detail": "Low Detail",
"Medium Detail": "Medium Detail",
"Full Detail": "Full Detail",
"Custom Template": "Custom Template",

};