Compare commits

...

8 Commits

Author SHA1 Message Date
Frank Elsinga 15d5d3a8f0
Merge 48abd79005 into dbbc79a05a 2024-05-02 11:04:32 +02:00
Frank Elsinga dbbc79a05a
Fixed a typo introduced in #3836 (#4729)
Co-authored-by: Nelson Chan <3271800+chakflying@users.noreply.github.com>
2024-05-02 12:11:49 +08:00
Rakovskij Stanislav 9f2cf28a76
Making docker usage with localhost and external ip more clear (#3836)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-04-30 22:53:27 +02:00
Ezhil Shanmugham 988ba79679
feat: keephq notification provider (#4722) 2024-04-30 16:17:34 +02:00
Joschua Becker 19e8c75c3b
SevenIO Notification Provider (#4219)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-04-27 23:40:59 +02:00
凯观生活 126d6cd912
Add the ability to notify `@everyone` in DingTalk notifications (#4718)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-04-27 17:16:39 +02:00
Frank Elsinga 48abd79005 formatting fixes 2024-01-19 00:16:21 +01:00
Frank Elsinga 3a89624d32 extracted the group monitor ot a different monitoring type 2024-01-19 00:01:15 +01:00
14 changed files with 298 additions and 37 deletions

View File

@ -43,11 +43,18 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Sponsore
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
```
Uptime Kuma is now running on http://localhost:3001
Uptime Kuma is now running on <http://0.0.0.0:3001>.
> [!WARNING]
> File Systems like **NFS** (Network File System) are **NOT** supported. Please map to a local directory or volume.
> [!NOTE]
> If you want to limit exposure to localhost (without exposing port for other users or to use a [reverse proxy](https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy)), you can expose the port like this:
>
> ```bash
> docker run -d --restart=always -p 127.0.0.1:3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
> ```
### 💪🏻 Non-Docker
Requirements:

View File

@ -386,39 +386,6 @@ class Monitor extends BeanModel {
if (await Monitor.isUnderMaintenance(this.id)) {
bean.msg = "Monitor under maintenance";
bean.status = MAINTENANCE;
} else if (this.type === "group") {
const children = await Monitor.getChildren(this.id);
if (children.length > 0) {
bean.status = UP;
bean.msg = "All children up and running";
for (const child of children) {
if (!child.active) {
// Ignore inactive childs
continue;
}
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
// Only change state if the monitor is in worse conditions then the ones before
// lastBeat.status could be null
if (!lastBeat) {
bean.status = PENDING;
} else if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
bean.status = lastBeat.status;
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
bean.status = lastBeat.status;
}
}
if (bean.status !== UP) {
bean.msg = "Child inaccessible";
}
} else {
// Set status pending if group is empty
bean.status = PENDING;
bean.msg = "Group empty";
}
} else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") {
// Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf();
@ -1523,7 +1490,7 @@ class Monitor extends BeanModel {
/**
* Gets all Children of the monitor
* @param {number} monitorID ID of monitor to get
* @returns {Promise<LooseObject<any>>} Children
* @returns {Promise<LooseObject<any>[]>} Children
*/
static async getChildren(monitorID) {
return await R.getAll(`

View File

@ -0,0 +1,49 @@
const { UP, PENDING, DOWN } = require("../../src/util");
const { MonitorType } = require("./monitor-type");
const Monitor = require("../model/monitor");
class GroupMonitorType extends MonitorType {
name = "group";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
const children = await Monitor.getChildren(monitor.id);
if (children.length > 0) {
heartbeat.status = UP;
heartbeat.msg = "All children up and running";
for (const child of children) {
if (!child.active) {
// Ignore inactive childs
continue;
}
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
// Only change state if the monitor is in worse conditions then the ones before
// lastBeat.status could be null
if (!lastBeat) {
heartbeat.status = PENDING;
} else if (heartbeat.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
heartbeat.status = lastBeat.status;
} else if (heartbeat.status === PENDING && lastBeat.status === DOWN) {
heartbeat.status = lastBeat.status;
}
}
if (heartbeat.status !== UP) {
heartbeat.msg = "Child inaccessible";
}
} else {
// Set status pending if group is empty
heartbeat.status = PENDING;
heartbeat.msg = "Group empty";
}
}
}
module.exports = {
GroupMonitorType,
};

View File

@ -19,6 +19,9 @@ class DingDing extends NotificationProvider {
markdown: {
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
},
"at": {
"isAtAll": notification.mentioning === "everyone"
}
};
if (await this.sendToDingDing(notification, params)) {

View File

@ -0,0 +1,42 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Keep extends NotificationProvider {
name = "Keep";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let data = {
heartbeat: heartbeatJSON,
monitor: monitorJSON,
msg,
};
let config = {
headers: {
"x-api-key": notification.webhookAPIKey,
"content-type": "application/json",
},
};
let url = notification.webhookURL;
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
let webhookURL = url + "/alerts/event/uptimekuma";
await axios.post(webhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Keep;

View File

@ -0,0 +1,78 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class SevenIO extends NotificationProvider {
name = "SevenIO";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const data = {
to: notification.sevenioTo,
from: notification.sevenioSender || "Uptime Kuma",
text: msg,
};
const config = {
baseURL: "https://gateway.seven.io/api/",
headers: {
"Content-Type": "application/json",
"X-API-Key": notification.sevenioApiKey,
},
};
try {
// testing or certificate expiry notification
if (heartbeatJSON == null) {
await axios.post("sms", data, config);
return okMsg;
}
let address = "";
switch (monitorJSON["type"]) {
case "ping":
address = monitorJSON["hostname"];
break;
case "port":
case "dns":
case "gamedig":
case "steam":
address = monitorJSON["hostname"];
if (monitorJSON["port"]) {
address += ":" + monitorJSON["port"];
}
break;
default:
if (![ "https://", "http://", "" ].includes(monitorJSON["url"])) {
address = monitorJSON["url"];
}
break;
}
if (address !== "") {
address = `(${address}) `;
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] === DOWN) {
data.text = `Your service ${monitorJSON["name"]} ${address}went down at ${heartbeatJSON["localDateTime"]} ` +
`(${heartbeatJSON["timezone"]}). Error: ${heartbeatJSON["msg"]}`;
} else if (heartbeatJSON["status"] === UP) {
data.text = `Your service ${monitorJSON["name"]} ${address}went back up at ${heartbeatJSON["localDateTime"]} ` +
`(${heartbeatJSON["timezone"]}).`;
}
await axios.post("sms", data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SevenIO;

View File

@ -18,6 +18,7 @@ const Gotify = require("./notification-providers/gotify");
const GrafanaOncall = require("./notification-providers/grafana-oncall");
const HomeAssistant = require("./notification-providers/home-assistant");
const HeiiOnCall = require("./notification-providers/heii-oncall");
const Keep = require("./notification-providers/keep");
const Kook = require("./notification-providers/kook");
const Line = require("./notification-providers/line");
const LineNotify = require("./notification-providers/linenotify");
@ -56,6 +57,7 @@ const GoAlert = require("./notification-providers/goalert");
const SMSManager = require("./notification-providers/smsmanager");
const ServerChan = require("./notification-providers/serverchan");
const ZohoCliq = require("./notification-providers/zoho-cliq");
const SevenIO = require("./notification-providers/sevenio");
const Whapi = require("./notification-providers/whapi");
const GtxMessaging = require("./notification-providers/gtx-messaging");
const Cellsynt = require("./notification-providers/cellsynt");
@ -94,6 +96,7 @@ class Notification {
new GrafanaOncall(),
new HomeAssistant(),
new HeiiOnCall(),
new Keep(),
new Kook(),
new Line(),
new LineNotify(),
@ -132,6 +135,7 @@ class Notification {
new WeCom(),
new GoAlert(),
new ZohoCliq(),
new SevenIO(),
new Whapi(),
new GtxMessaging(),
new Cellsynt(),

View File

@ -113,6 +113,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
UptimeKumaServer.monitorTypeList["group"] = new GroupMonitorType();
// Allow all CORS origins (polling) in development
let cors = undefined;
@ -516,3 +517,4 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor
const { TailscalePing } = require("./monitor-types/tailscale-ping");
const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt");
const { GroupMonitorType } = require("./monitor-types/group");

View File

@ -123,6 +123,7 @@ export default {
"GrafanaOncall": "Grafana Oncall",
"HeiiOnCall": "Heii On-Call",
"HomeAssistant": "Home Assistant",
"Keep": "Keep",
"Kook": "Kook",
"line": "LINE Messenger",
"LineNotify": "LINE Notify",
@ -154,6 +155,7 @@ export default {
"webhook": "Webhook",
"GoAlert": "GoAlert",
"ZohoCliq": "ZohoCliq",
"SevenIO": "SevenIO",
"whapi": "WhatsApp (Whapi)",
"gtxmessaging": "GtxMessaging",
"Cellsynt": "Cellsynt",

View File

@ -2,9 +2,10 @@
<div class="mb-3">
<label for="WebHookUrl" class="form-label">{{ $t("WebHookUrl") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="WebHookUrl" v-model="$parent.notification.webHookUrl" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="secretKey" class="form-label">{{ $t("SecretKey") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
<HiddenInput id="secretKey" v-model="$parent.notification.secretKey" :required="true" autocomplete="new-password"></HiddenInput>
<div class="form-text">
<p>{{ $t("For safety, must use secret key") }}</p>
@ -13,4 +14,24 @@
</i18n-t>
</div>
</div>
<div class="mb-3">
<label for="mentioning" class="form-label">{{ $t("Mentioning") }}<span style="color: red;"><sup>*</sup></span></label>
<select id="mentioning" v-model="$parent.notification.mentioning" class="form-select" required>
<option value="nobody">{{ $t("Don't mention people") }}</option>
<option value="everyone">{{ $t("Mention group", { group: "@everyone" }) }}</option>
</select>
</div>
</template>
<script lang="ts">
import HiddenInput from "../HiddenInput.vue";
export default {
components: { HiddenInput },
mounted() {
if (typeof this.$parent.notification.mentioning === "undefined") {
this.$parent.notification.mentioning = "nobody";
}
}
};
</script>

View File

@ -0,0 +1,42 @@
<template>
<div class="mb-3">
<label for="webhook-url" class="form-label">{{ $t("Host URL") }}</label>
<input
id="webhook-url"
v-model="$parent.notification.webhookURL"
type="url"
pattern="https?://.+"
class="form-control"
required
/>
<div class="form-text">
<i18n-t tag="p" keypath="Read more:">
<a href="https://docs.keephq.dev/providers/documentation/uptimekuma-provider" target="_blank">https://docs.keephq.dev/providers/documentation/uptimekuma-provider</a>
</i18n-t>
</div>
</div>
<div class="mb-3">
<label for="webhook-apikey" class="form-label">{{
$t("API Key")
}}</label>
<HiddenInput
id="webhook-apikey"
v-model="$parent.notification.webhookAPIKey"
:required="true"
></HiddenInput>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
mounted() {
this.$parent.notification.webhookURL ||= "";
},
};
</script>

View File

@ -0,0 +1,31 @@
<template>
<div class="mb-3">
<label for="sevenio-api-key" class="form-label">{{ $t("apiKeySevenIO") }}</label>
<HiddenInput id="sevenio-api-key" v-model="$parent.notification.sevenioApiKey" :required="true" autocomplete="new-password"></HiddenInput>
<div class="form-text">
{{ $t("wayToGetSevenIOApiKey") }}
</div>
</div>
<div class="mb-3">
<label for="sevenio-sender" class="form-label">{{ $t("senderSevenIO") }}</label>
<input id="sevenio-sender" v-model="$parent.notification.sevenioSender" type="text" class="form-control" autocomplete="false" placeholder="Uptime Kuma">
</div>
<div class="mb-3">
<label for="sevenio-receiver" class="form-label">{{ $t("receiverSevenIO") }}</label>
<input id="sevenio-receiver" v-model="$parent.notification.sevenioReceiver" type="number" class="form-control" required autocomplete="false" placeholder="0123456789">
<div class="form-text">
{{ $t("receiverInfoSevenIO") }}
</div>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View File

@ -17,6 +17,7 @@ import GrafanaOncall from "./GrafanaOncall.vue";
import GtxMessaging from "./GtxMessaging.vue";
import HomeAssistant from "./HomeAssistant.vue";
import HeiiOnCall from "./HeiiOnCall.vue";
import Keep from "./Keep.vue";
import Kook from "./Kook.vue";
import Line from "./Line.vue";
import LineNotify from "./LineNotify.vue";
@ -55,6 +56,7 @@ import WeCom from "./WeCom.vue";
import GoAlert from "./GoAlert.vue";
import ZohoCliq from "./ZohoCliq.vue";
import Splunk from "./Splunk.vue";
import SevenIO from "./SevenIO.vue";
import Whapi from "./Whapi.vue";
import Cellsynt from "./Cellsynt.vue";
@ -81,6 +83,7 @@ const NotificationFormList = {
"GrafanaOncall": GrafanaOncall,
"HomeAssistant": HomeAssistant,
"HeiiOnCall": HeiiOnCall,
"Keep": Keep,
"Kook": Kook,
"line": Line,
"LineNotify": LineNotify,
@ -119,6 +122,7 @@ const NotificationFormList = {
"GoAlert": GoAlert,
"ServerChan": ServerChan,
"ZohoCliq": ZohoCliq,
"SevenIO": SevenIO,
"whapi": Whapi,
"gtxmessaging": GtxMessaging,
"Cellsynt": Cellsynt,

View File

@ -63,6 +63,7 @@
"Friendly Name": "Friendly Name",
"URL": "URL",
"Hostname": "Hostname",
"Host URL": "Host URL",
"locally configured mail transfer agent": "locally configured mail transfer agent",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Either enter the hostname of the server you want to connect to or {localhost} if you intend to use a {local_mta}",
"Port": "Port",
@ -667,6 +668,9 @@
"WebHookUrl": "WebHookUrl",
"SecretKey": "SecretKey",
"For safety, must use secret key": "For safety, must use secret key",
"Mentioning": "Mentioning",
"Don't mention people": "Don't mention people",
"Mention group": "Mention {group}",
"Device Token": "Device Token",
"Platform": "Platform",
"Huawei": "Huawei",
@ -887,6 +891,11 @@
"deleteRemoteBrowserMessage": "Are you sure want to delete this Remote Browser for all monitors?",
"GrafanaOncallUrl": "Grafana Oncall URL",
"Browser Screenshot": "Browser Screenshot",
"wayToGetSevenIOApiKey": "Visit the dashboard under app.seven.io > developer > api key > the green add button",
"senderSevenIO": "Sending number or name",
"receiverSevenIO": "Receiving number",
"receiverInfoSevenIO": "If the receiving number is not located in Germany, you have to add the country code in front of the number (e.g. for the country code 1 from the US use 117612121212 instead of 017612121212)",
"apiKeySevenIO": "SevenIO API Key",
"wayToWriteWhapiRecipient": "The phone number with the international prefix, but without the plus sign at the start ({0}), the Contact ID ({1}) or the Group ID ({2}).",
"wayToGetWhapiUrlAndToken": "You can get the API URL and the token by going into your desired channel from {0}",
"whapiRecipient": "Phone Number / Contact ID / Group ID",