add telegram notification

pull/6/head
LouisLam 2021-07-09 14:14:03 +08:00
parent 04ec91d7a9
commit 3bdf174e90
11 changed files with 418 additions and 85 deletions

39
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "0.0.0",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -14,6 +14,12 @@
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz",
"integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA=="
},
"@babel/standalone": {
"version": "7.14.7",
"resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.14.7.tgz",
"integrity": "sha512-7RlfMPR4604SbYpj5zvs2ZK587hVhixgU9Pd9Vs8lB8KYtT3U0apXSf0vZXhy8XRh549eUmJOHXhWKTO3ObzOQ==",
"dev": true
},
"@babel/types": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz",
@ -48,6 +54,19 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz",
"integrity": "sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA=="
},
"@vitejs/plugin-legacy": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.4.3.tgz",
"integrity": "sha512-lxZUJaMWYMQuqvZM1wPzDP6KABQgA/drVL5fnaygEPcz9adc2OHhfFNN/SvvHQ1V0rP8gybIc7uA+iI1gAdkVQ==",
"dev": true,
"requires": {
"@babel/standalone": "^7.14.7",
"core-js": "^3.15.1",
"magic-string": "^0.25.7",
"regenerator-runtime": "^0.13.7",
"systemjs": "^6.10.1"
}
},
"@vitejs/plugin-vue": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.2.3.tgz",
@ -618,6 +637,12 @@
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
},
"core-js": {
"version": "3.15.2",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.15.2.tgz",
"integrity": "sha512-tKs41J7NJVuaya8DxIOCnl8QuPHx5/ZVbFo1oKgVl1qHFBBrDctzQGtuLjPpRdNTWmKPH6oEvgN/MUID+l485Q==",
"dev": true
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@ -2682,6 +2707,12 @@
}
}
},
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
},
"regex-not": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
@ -3203,6 +3234,12 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
},
"systemjs": {
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/systemjs/-/systemjs-6.10.2.tgz",
"integrity": "sha512-PwaC0Z6Y1E6gFekY2u38EC5+5w2M65jYVrD1aAcOptpHVhCwPIwPFJvYJyryQKUyeuQ5bKKI3PBHWNjdE9aizg==",
"dev": true
},
"tarn": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz",

View File

@ -3,9 +3,10 @@
"version": "1.0.0",
"scripts": {
"dev": "vite --host",
"dev-server": "node server/server.js",
"build": "vite build",
"serve": "vite preview --host"
"start-server": "node server/server.js",
"update": "",
"build": "npm install && vite build",
"vite-preview-dist": "vite preview --host"
},
"dependencies": {
"@popperjs/core": "^2.9.2",
@ -25,8 +26,10 @@
"vue-toastification": "^2.0.0-rc.1"
},
"devDependencies": {
"@vitejs/plugin-legacy": "^1.4.3",
"@vitejs/plugin-vue": "^1.2.3",
"@vue/compiler-sfc": "^3.0.5",
"core-js": "^3.15.2",
"sass": "^1.35.1",
"vite": "^2.3.7"
}

View File

@ -141,71 +141,52 @@ class Monitor extends BeanModel {
}
/**
*
* Uptime with calculation
* Calculation based on:
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
* @param duration : int Hours
*/
static async sendUptime(duration, io, monitorID, userID) {
let sec = duration * 3600;
let downtimeList = await R.getAll(`
SELECT duration, time
SELECT duration, time, status
FROM heartbeat
WHERE time > DATE('now', ? || ' hours')
AND status = 0
AND monitor_id = ? `, [
-duration,
monitorID
]);
let downtime = 0;
let uptime = 0;
let total = 0;
let uptime;
if (downtimeList.length === 0) {
for (let row of downtimeList) {
let value = parseInt(row.duration)
let time = row.time
for (let row of downtimeList) {
let value = parseInt(row.duration)
let time = row.time
// Handle if heartbeat duration longer than the target duration
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
if (value <= sec) {
downtime += value;
} else {
let trim = dayjs.utc().diff(dayjs(time), 'second');
// Handle if heartbeat duration longer than the target duration
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
if (value > sec) {
let trim = dayjs.utc().diff(dayjs(time), 'second');
value = sec - trim;
value = sec - trim;
if (value < 0) {
value = 0;
}
downtime += value;
if (value < 0) {
value = 0;
}
}
uptime = (sec - downtime) / sec;
if (uptime < 0) {
uptime = 0;
total += value;
if (row.status === 0) {
downtime += value;
}
} else {
// This case for someone who are not running UptimeKuma 24x7.
// If there is no heartbeat in this time range, use last heartbeat as reference
// If is down, uptime = 0
// If is up, uptime = 1
}
let lastHeartbeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
monitorID
]);
uptime = (total - downtime) / total;
if (lastHeartbeat) {
if (lastHeartbeat.status === 1) {
uptime = 1;
} else {
uptime = 0;
}
} else {
// No heartbeat is found, assume 100%
uptime = 1;
}
if (uptime < 0) {
uptime = 0;
}
io.to(userID).emit("uptime", monitorID, duration, uptime);

58
server/notification.js Normal file
View File

@ -0,0 +1,58 @@
const axios = require("axios");
const {R} = require("redbean-node");
class Notification {
static async send(notification, msg) {
if (notification.type === "telegram") {
let res = await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
params: {
chat_id: notification.telegramChatID,
text: msg,
}
})
return true;
} else {
throw new Error("Notification type is not supported")
}
}
static async save(notification, notificationID, userID) {
let bean
if (notificationID) {
bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
notificationID,
userID,
])
if (! bean) {
throw new Error("notification not found")
}
} else {
bean = R.dispense("notification")
}
bean.name = notification.name;
bean.user_id = userID;
bean.config = JSON.stringify(notification)
await R.store(bean)
}
static async delete(notificationID, userID) {
let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
notificationID,
userID,
])
if (! bean) {
throw new Error("notification not found")
}
await R.trash(bean)
}
}
module.exports = {
Notification,
}

View File

@ -10,6 +10,7 @@ const passwordHash = require('password-hash');
const jwt = require('jsonwebtoken');
const Monitor = require("./model/monitor");
const {getSettings} = require("./util-server");
const {Notification} = require("./notification")
let totalClient = 0;
let jwtSecret = null;
@ -26,6 +27,10 @@ let monitorList = {};
app.use('/', express.static("dist"));
app.get('*', function(request, response, next) {
response.sendFile(process.cwd() + '/dist/index.html');
});
io.on('connection', async (socket) => {
console.log('a user connected');
totalClient++;
@ -318,6 +323,65 @@ let monitorList = {};
}
});
// Add or Edit
socket.on("addNotification", async (notification, notificationID, callback) => {
try {
checkLogin(socket)
await Notification.save(notification, notificationID, socket.userID)
await sendNotificationList(socket)
callback({
ok: true,
msg: "Saved",
});
} catch (e) {
callback({
ok: false,
msg: e.message
});
}
});
socket.on("deleteNotification", async (notificationID, callback) => {
try {
checkLogin(socket)
await Notification.delete(notificationID, socket.userID)
await sendNotificationList(socket)
callback({
ok: true,
msg: "Deleted",
});
} catch (e) {
callback({
ok: false,
msg: e.message
});
}
});
socket.on("testNotification", async (notification, callback) => {
try {
checkLogin(socket)
await Notification.send(notification, notification.name + " Testing")
callback({
ok: true,
msg: "Sent Successfully"
});
} catch (e) {
callback({
ok: false,
msg: e.message
});
}
});
});
server.listen(3001, () => {
@ -344,6 +408,20 @@ async function sendMonitorList(socket) {
return list;
}
async function sendNotificationList(socket) {
let result = [];
let list = await R.find("notification", " user_id = ? ", [
socket.userID
]);
for (let bean of list) {
result.push(bean.export())
}
io.to(socket.userID).emit("notificationList", result)
return list;
}
async function afterLogin(socket, user) {
socket.userID = user.id;
socket.join(user.id)
@ -355,6 +433,8 @@ async function afterLogin(socket, user) {
await sendImportantHeartbeatList(socket, monitorID);
await Monitor.sendStats(io, monitorID, user.id)
}
await sendNotificationList(socket)
}
async function getMonitorJSONList(userID) {

View File

@ -1,5 +1,6 @@
const tcpp = require('tcp-ping');
const Ping = require("./ping-lite");
const {R} = require("redbean-node");
exports.tcping = function (hostname, port) {
return new Promise((resolve, reject) => {
@ -37,3 +38,25 @@ exports.ping = function (hostname) {
});
});
}
exports.setting = async function (key) {
return await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
key
])
}
exports.getSettings = async function (type) {
let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [
type
])
let result = {};
for (let row of list) {
result[row.key] = row.value;
}
console.log(result)
return result;
}

View File

@ -10,6 +10,10 @@ export function sleep(ms) {
}
export function ucfirst(str) {
if (! str) {
return str;
}
const firstLetter = str.substr(0, 1);
return firstLetter.toUpperCase() + str.substr(1);
}

View File

@ -1,42 +1,81 @@
<template>
<div class="modal fade" tabindex="-1" ref="modal" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Setup Notification</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form @submit.prevent="submit">
<form @submit.prevent="submit">
<div class="mb-3">
<label for="type" class="form-label">Notification Type</label>
<select class="form-select" id="type" v-model="type">
<option value="email">Email</option>
<option value="webhook">Webhook</option>
<option value="telegram">Telegram</option>
<option value="discord">Discord</option>
</select>
</div>
<div class="modal fade" tabindex="-1" ref="modal" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Setup Notification</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="name" class="form-label">Friendly Name</label>
<input type="text" class="form-control" id="name" required v-model="name">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" @click="yes" data-bs-dismiss="modal">Save</button>
<div class="mb-3">
<label for="type" class="form-label">Notification Type</label>
<select class="form-select" id="type" v-model="notification.type">
<option value="telegram">Telegram</option>
<option value="webhook">Webhook</option>
<option value="email">Email</option>
<option value="discord">Discord</option>
</select>
</div>
<div class="mb-3">
<label for="name" class="form-label">Friendly Name</label>
<input type="text" class="form-control" id="name" required v-model="notification.name">
</div>
<div class="mb-3" v-if="notification.type === 'telegram'">
<label for="telegram-bot-token" class="form-label">Bot Token</label>
<input type="text" class="form-control" id="telegram-bot-token" required v-model="notification.telegramBotToken">
<div class="form-text">You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.</div>
</div>
<div class="mb-3" v-if="notification.type === 'telegram'">
<label for="telegram-chat-id" class="form-label">Chat ID</label>
<div class="input-group mb-3">
<input type="text" class="form-control" id="telegram-chat-id" required v-model="notification.telegramChatID">
<button class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID" v-if="notification.telegramBotToken">Auto Get</button>
</div>
<div class="form-text">
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
<p style="margin-top: 8px;">
<template v-if="notification.telegramBotToken">
<a :href="telegramGetUpdatesURL" target="_blank">{{ telegramGetUpdatesURL }}</a>
</template>
<template v-else>
{{ telegramGetUpdatesURL }}
</template>
</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" @click="deleteNotification" :disabled="processing" v-if="id">Delete</button>
<button type="button" class="btn btn-warning" @click="test" :disabled="processing">Test</button>
<button type="submit" class="btn btn-primary" :disabled="processing">Save</button>
</div>
</div>
</div>
</div>
</div>
</form>
</template>
<script>
import { Modal } from 'bootstrap'
import { ucfirst } from "../../server/util";
import axios from "axios";
import { useToast } from 'vue-toastification'
const toast = useToast()
export default {
props: {
@ -45,33 +84,123 @@ export default {
data() {
return {
model: null,
type: null,
name: "",
processing: false,
id: null,
notification: {
name: "",
type: null,
},
}
},
mounted() {
this.modal = new Modal(this.$refs.modal)
// TODO: for edit
this.$root.getSocket().emit("getSettings", "notification", (data) => {
// this.notification = data
})
},
methods: {
show() {
show(notificationID) {
if (notificationID) {
this.id = notificationID;
for (let n of this.$root.notificationList) {
if (n.id === notificationID) {
this.notification = JSON.parse(n.config);
break;
}
}
} else {
this.id = null;
this.notification = {
name: "",
type: null,
}
// Default set to Telegram
this.notification.type = "telegram"
}
this.modal.show()
},
submit() {
}
submit() {
this.processing = true;
this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
this.$root.toastRes(res)
this.processing = false;
if (res.ok) {
this.modal.hide()
}
})
},
test() {
this.processing = true;
this.$root.getSocket().emit("testNotification", this.notification, (res) => {
this.$root.toastRes(res)
this.processing = false;
})
},
deleteNotification() {
this.processing = true;
this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
this.$root.toastRes(res)
this.processing = false;
if (res.ok) {
this.modal.hide()
}
})
},
async autoGetTelegramChatID() {
try {
let res = await axios.get(this.telegramGetUpdatesURL)
if (res.data.result.length >= 1) {
let update = res.data.result[res.data.result.length - 1]
this.notification.telegramChatID = update.message.chat.id;
} else {
throw new Error("Chat ID is not found, please send a message to this bot first")
}
} catch (error) {
toast.error(error.message)
}
},
},
computed: {
telegramGetUpdatesURL() {
let token = "<YOUR BOT TOKEN HERE>"
if (this.notification.telegramBotToken) {
token = this.notification.telegramBotToken;
}
return `https://api.telegram.org/bot${token}/getUpdates`;
},
},
watch: {
type(to, from) {
"notification.type"(to, from) {
let oldName;
if (from) {
oldName = `My ${ucfirst(from)} Notification`;
oldName = `My ${ucfirst(from)} Alert (1)`;
} else {
oldName = "";
}
if (! this.name || this.name === oldName) {
this.name = `My ${ucfirst(to)} Alert (1)`
if (! this.notification.name || this.notification.name === oldName) {
this.notification.name = `My ${ucfirst(to)} Alert (1)`
}
}
}

View File

@ -23,7 +23,8 @@ export default {
heartbeatList: { },
importantHeartbeatList: { },
avgPingList: { },
uptimeList: { }
uptimeList: { },
notificationList: [],
}
},
@ -36,6 +37,10 @@ export default {
this.monitorList = data;
});
socket.on('notificationList', (data) => {
this.notificationList = data;
});
socket.on('heartbeat', (data) => {
if (! (data.monitorID in this.heartbeatList)) {
this.heartbeatList[data.monitorID] = [];

View File

@ -55,7 +55,13 @@
<div class="col-md-6">
<h2>Notifications</h2>
<p>Not available, please setup.</p>
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
<div class="form-check form-switch mb-3" v-for="notification in $root.notificationList">
<input class="form-check-input" type="checkbox" :id=" 'notification' + notification.id">
<label class="form-check-label" :for=" 'notification' + notification.id">{{ notification.name }} <a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a></label>
</div>
<button class="btn btn-primary me-2" @click="$refs.notificationDialog.show()" type="button">Setup Notification</button>
</div>
</div>

View File

@ -1,7 +1,14 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import legacy from '@vitejs/plugin-legacy'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
plugins: [
vue(),
legacy({
targets: ['ie >= 11'],
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
})
]
})