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

Open
deefdragon wants to merge 6 commits from deefdragon/Template-Engine into master
  1. 12910
      package-lock.json
  2. 3
      package.json
  3. 165
      server/notification.js
  4. 3
      server/server.js
  5. 33
      src/components/NotificationDialog.vue
  6. 15
      src/components/notifications/index.js
  7. 12
      src/languages/en.js

12910
package-lock.json

File diff suppressed because it is too large

3
package.json

@ -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",
@ -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",

165
server/notification.js

@ -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");
@ -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 },
tags: [{ "id": 21,
"monitor_id": 16,
"tag_id": 2,
"value": "",
"name": "Internal",
"color": "#059669" }],
};
}
providerList = {};
Saibamen commented 4 years ago (Migrated from github.com)
Review
            notificationIDList: {
                "1": true,
                "5": true,
            },
```suggestion notificationIDList: { "1": true, "5": true, }, ```
static init() {
@ -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) {
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) {
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);
@ -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() {
@ -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
server/server.js

@ -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,

33
src/components/NotificationDialog.vue

@ -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" />
@ -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";
export default {
components: {
@ -92,7 +110,9 @@ export default {
type: null,
isDefault: false,
// Do not set default value here, please scroll to show()
}
},
detailLevels: NotificationDetailList,
};
},
Saibamen commented 4 years ago (Migrated from github.com)
Review
```suggestion ```
@ -102,6 +122,9 @@ export default {
return null;
}
return NotificationFormList[this.notification.type];
},
enableTemplateOptions() {
return (TemplateEnabledList.includes(this.notification.type));
}
},
@ -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);
@ -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}}";
}
this.modal.show();

15
src/components/notifications/index.js

@ -26,7 +26,7 @@ import DingDing from "./DingDing.vue";
*
* @type { Record<string, any> }
*/
const NotificationFormList = {
export const NotificationFormList = {
"telegram": Telegram,
"webhook": Webhook,
"smtp": STMP,
@ -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
src/languages/en.js

@ -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",
Saibamen commented 4 years ago (Migrated from github.com)
Review
    "Message Template": "Message Template",
    "Default Template": "[{{monitor.name}}] [{{monitor.health}}] {{monitor.msg}}",
    HealthyStatus: "✅ Healthy",
    UnhealthyStatus: "❌ Unhealthy",
```suggestion "Message Template": "Message Template", "Default Template": "[{{monitor.name}}] [{{monitor.health}}] {{monitor.msg}}", HealthyStatus: "✅ Healthy", UnhealthyStatus: "❌ Unhealthy", ```
//template levels
Saibamen commented 4 years ago (Migrated from github.com)
Review
    // template levels
```suggestion // 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",
Saibamen commented 4 years ago (Migrated from github.com)
Review
    "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",
```suggestion "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", ```
};

Loading…
Cancel
Save