mirror of
https://github.com/koichixD/uptime-kuma.git
synced 2026-09-04 01:03:21 +00:00
feat: fluxer notification provider (#7109)
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Fluxer extends NotificationProvider {
|
||||
name = "fluxer";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
const fluxerDisplayName = notification.fluxerUsername || "Uptime Kuma";
|
||||
const webhookUrl = new URL(notification.fluxerWebhookUrl);
|
||||
|
||||
// Check if the webhook has an avatar
|
||||
let webhookHasAvatar = true;
|
||||
try {
|
||||
const webhookInfo = await axios.get(webhookUrl.toString(), config);
|
||||
webhookHasAvatar = !!webhookInfo.data.avatar;
|
||||
} catch (e) {
|
||||
// If we can't verify, we assume he has an avatar to avoid forcing the default avatar
|
||||
webhookHasAvatar = true;
|
||||
}
|
||||
|
||||
const messageFormat =
|
||||
notification.fluxerMessageFormat || (notification.fluxerUseMessageTemplate ? "custom" : "normal");
|
||||
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let content = msg;
|
||||
if (messageFormat === "minimalist") {
|
||||
content = "Test: " + msg;
|
||||
} else if (messageFormat === "custom") {
|
||||
const customMessage = notification.fluxerMessageTemplate?.trim() || "";
|
||||
if (customMessage !== "") {
|
||||
content = await this.renderTemplate(customMessage, msg, monitorJSON, heartbeatJSON);
|
||||
}
|
||||
}
|
||||
let fluxertestdata = {
|
||||
username: fluxerDisplayName,
|
||||
content: content,
|
||||
};
|
||||
if (!webhookHasAvatar) {
|
||||
fluxertestdata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||
}
|
||||
await axios.post(webhookUrl.toString(), fluxertestdata, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
let addess = this.extractAddress(monitorJSON);
|
||||
|
||||
// Minimalist: status + name only (is down / is up; no "back up" — may be first trigger)
|
||||
if (messageFormat === "minimalist") {
|
||||
const content =
|
||||
heartbeatJSON["status"] === DOWN
|
||||
? "🔴 " + monitorJSON["name"] + " is down."
|
||||
: "🟢 " + monitorJSON["name"] + " is up.";
|
||||
let payload = {
|
||||
username: fluxerDisplayName,
|
||||
content: content,
|
||||
};
|
||||
if (!webhookHasAvatar) {
|
||||
payload.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl.toString(), payload, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
// Custom template: send only content (no embeds)
|
||||
const useCustomTemplate =
|
||||
messageFormat === "custom" && (notification.fluxerMessageTemplate?.trim() || "") !== "";
|
||||
if (useCustomTemplate) {
|
||||
const content = await this.renderTemplate(
|
||||
notification.fluxerMessageTemplate.trim(),
|
||||
msg,
|
||||
monitorJSON,
|
||||
heartbeatJSON
|
||||
);
|
||||
let payload = {
|
||||
username: fluxerDisplayName,
|
||||
content: content,
|
||||
};
|
||||
if (!webhookHasAvatar) {
|
||||
payload.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl.toString(), payload, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
const wentOfflineTimestamp = Math.floor(new Date(heartbeatJSON["time"]).getTime() / 1000);
|
||||
|
||||
let fluxerdowndata = {
|
||||
username: fluxerDisplayName,
|
||||
embeds: [
|
||||
{
|
||||
title: "❌ Your service " + monitorJSON["name"] + " went down. ❌",
|
||||
color: 16711680,
|
||||
fields: [
|
||||
{
|
||||
name: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
...(!notification.disableUrl && addess
|
||||
? [
|
||||
{
|
||||
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||
value: addess,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: "Went Offline",
|
||||
// F for full date/time
|
||||
value: `<t:${wentOfflineTimestamp}:F>`,
|
||||
},
|
||||
{
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
value: heartbeatJSON["localDateTime"],
|
||||
},
|
||||
{
|
||||
name: "Error",
|
||||
value: heartbeatJSON["msg"] == null ? "N/A" : heartbeatJSON["msg"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
if (!webhookHasAvatar) {
|
||||
fluxerdowndata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||
}
|
||||
if (notification.fluxerPrefixMessage) {
|
||||
fluxerdowndata.content = notification.fluxerPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl.toString(), fluxerdowndata, config);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
const backOnlineTimestamp = Math.floor(new Date(heartbeatJSON["time"]).getTime() / 1000);
|
||||
let downtimeDuration = null;
|
||||
let wentOfflineTimestamp = null;
|
||||
if (heartbeatJSON["lastDownTime"]) {
|
||||
wentOfflineTimestamp = Math.floor(new Date(heartbeatJSON["lastDownTime"]).getTime() / 1000);
|
||||
downtimeDuration = this.formatDuration(backOnlineTimestamp - wentOfflineTimestamp);
|
||||
}
|
||||
|
||||
let fluxerupdata = {
|
||||
username: fluxerDisplayName,
|
||||
embeds: [
|
||||
{
|
||||
title: "✅ Your service " + monitorJSON["name"] + " is up! ✅",
|
||||
color: 65280,
|
||||
fields: [
|
||||
{
|
||||
name: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
...(!notification.disableUrl && addess
|
||||
? [
|
||||
{
|
||||
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||
value: addess,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(wentOfflineTimestamp
|
||||
? [
|
||||
{
|
||||
name: "Went Offline",
|
||||
// F for full date/time
|
||||
value: `<t:${wentOfflineTimestamp}:F>`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(downtimeDuration
|
||||
? [
|
||||
{
|
||||
name: "Downtime Duration",
|
||||
value: downtimeDuration,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Show server timezone for parity with the DOWN notification embed
|
||||
{
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
value: heartbeatJSON["localDateTime"],
|
||||
},
|
||||
...(heartbeatJSON["ping"] != null
|
||||
? [
|
||||
{
|
||||
name: "Ping",
|
||||
value: heartbeatJSON["ping"] + " ms",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
if (!webhookHasAvatar) {
|
||||
fluxerupdata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||
}
|
||||
if (notification.fluxerPrefixMessage) {
|
||||
fluxerupdata.content = notification.fluxerPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl.toString(), fluxerupdata, config);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration as human-readable string (e.g., "1h 23m", "45m 30s")
|
||||
* TODO: Update below to `Intl.DurationFormat("en", { style: "short" }).format(duration)` once we are on a newer node version
|
||||
* @param {number} timeInSeconds The time in seconds to format a duration for
|
||||
* @returns {string} The formatted duration
|
||||
*/
|
||||
formatDuration(timeInSeconds) {
|
||||
const hours = Math.floor(timeInSeconds / 3600);
|
||||
const minutes = Math.floor((timeInSeconds % 3600) / 60);
|
||||
const seconds = timeInSeconds % 60;
|
||||
|
||||
const durationParts = [];
|
||||
if (hours > 0) {
|
||||
durationParts.push(`${hours}h`);
|
||||
}
|
||||
if (minutes > 0) {
|
||||
durationParts.push(`${minutes}m`);
|
||||
}
|
||||
if (seconds > 0 && hours === 0) {
|
||||
// Only show seconds if less than an hour
|
||||
durationParts.push(`${seconds}s`);
|
||||
}
|
||||
|
||||
return durationParts.length > 0 ? durationParts.join(" ") : "0s";
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Fluxer;
|
||||
@@ -12,6 +12,7 @@ const CallMeBot = require("./notification-providers/call-me-bot");
|
||||
const SMSC = require("./notification-providers/smsc");
|
||||
const DingDing = require("./notification-providers/dingding");
|
||||
const Discord = require("./notification-providers/discord");
|
||||
const Fluxer = require("./notification-providers/fluxer");
|
||||
const Elks = require("./notification-providers/46elks");
|
||||
const Feishu = require("./notification-providers/feishu");
|
||||
const Notifery = require("./notification-providers/notifery");
|
||||
@@ -117,6 +118,7 @@ class Notification {
|
||||
new SMSC(),
|
||||
new DingDing(),
|
||||
new Discord(),
|
||||
new Fluxer(),
|
||||
new Elks(),
|
||||
new Feishu(),
|
||||
new FreeMobile(),
|
||||
|
||||
@@ -215,6 +215,7 @@ export default {
|
||||
bale: "Bale",
|
||||
Bitrix24: "Bitrix24",
|
||||
discord: "Discord",
|
||||
fluxer: "Fluxer",
|
||||
GoogleChat: "Google Chat (Google Workspace)",
|
||||
gorush: "Gorush",
|
||||
gotify: "Gotify",
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="fluxer-webhook-url" class="form-label">{{ $t("Fluxer Webhook URL") }}</label>
|
||||
<HiddenInput
|
||||
id="fluxer-webhook-url"
|
||||
v-model="$parent.notification.fluxerWebhookUrl"
|
||||
type="url"
|
||||
class="form-control"
|
||||
required
|
||||
autocomplete="false"
|
||||
/>
|
||||
<div class="form-text">
|
||||
{{ $t("wayToGetFluxerURL") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fluxer-username" class="form-label">{{ $t("Bot Display Name") }}</label>
|
||||
<input
|
||||
id="fluxer-username"
|
||||
v-model="$parent.notification.fluxerUsername"
|
||||
type="text"
|
||||
class="form-control"
|
||||
autocomplete="false"
|
||||
:placeholder="$root.appName"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fluxer-prefix-message" class="form-label">{{ $t("Prefix Custom Message") }}</label>
|
||||
<input
|
||||
id="fluxer-prefix-message"
|
||||
v-model="$parent.notification.fluxerPrefixMessage"
|
||||
type="text"
|
||||
class="form-control"
|
||||
autocomplete="false"
|
||||
:placeholder="$t('Hello @everyone is...')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fluxer-message-format" class="form-label">{{ $t("fluxerMessageFormat") }}</label>
|
||||
<select id="fluxer-message-format" v-model="$parent.notification.fluxerMessageFormat" class="form-select">
|
||||
<option value="normal">{{ $t("fluxerMessageFormatNormal") }}</option>
|
||||
<option value="minimalist">{{ $t("fluxerMessageFormatMinimalist") }}</option>
|
||||
<option value="custom">{{ $t("fluxerMessageFormatCustom") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-show="$parent.notification.fluxerMessageFormat === 'custom'">
|
||||
<div class="mb-3">
|
||||
<label for="fluxer-message-template" class="form-label">{{ $t("fluxerMessageTemplate") }}</label>
|
||||
<TemplatedTextarea
|
||||
id="fluxer-message-template"
|
||||
v-model="$parent.notification.fluxerMessageTemplate"
|
||||
:required="false"
|
||||
placeholder=""
|
||||
></TemplatedTextarea>
|
||||
<div class="form-text">{{ $t("fluxerUseMessageTemplateDescription") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
import TemplatedTextarea from "../TemplatedTextarea.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TemplatedTextarea,
|
||||
HiddenInput,
|
||||
},
|
||||
mounted() {
|
||||
if (!this.$parent.notification.fluxerChannelType) {
|
||||
this.$parent.notification.fluxerChannelType = "channel";
|
||||
}
|
||||
if (this.$parent.notification.disableUrl === undefined) {
|
||||
this.$parent.notification.disableUrl = false;
|
||||
}
|
||||
// Message format: default "normal"; migrate from old checkbox
|
||||
if (typeof this.$parent.notification.fluxerMessageFormat === "undefined") {
|
||||
const hadCustom =
|
||||
this.$parent.notification.fluxerUseMessageTemplate === true ||
|
||||
!!this.$parent.notification.fluxerMessageTemplate?.trim();
|
||||
this.$parent.notification.fluxerMessageFormat = hadCustom ? "custom" : "normal";
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -11,6 +11,7 @@ import CallMeBot from "./CallMeBot.vue";
|
||||
import SMSC from "./SMSC.vue";
|
||||
import DingDing from "./DingDing.vue";
|
||||
import Discord from "./Discord.vue";
|
||||
import Fluxer from "./Fluxer.vue";
|
||||
import Elks from "./46elks.vue";
|
||||
import Feishu from "./Feishu.vue";
|
||||
import FreeMobile from "./FreeMobile.vue";
|
||||
@@ -105,6 +106,7 @@ const NotificationFormList = {
|
||||
smsir: SMSIR,
|
||||
DingDing: DingDing,
|
||||
discord: Discord,
|
||||
fluxer: Fluxer,
|
||||
Elks: Elks,
|
||||
Feishu: Feishu,
|
||||
FreeMobile: FreeMobile,
|
||||
|
||||
@@ -1358,6 +1358,15 @@
|
||||
"discordUseMessageTemplate": "Use custom message template",
|
||||
"discordUseMessageTemplateDescription": "If enabled, the message will be sent using a custom template (LiquidJS). Leave blank to use the default Uptime Kuma format.",
|
||||
"discordMessageTemplate": "Message Template",
|
||||
"fluxerMessageFormat": "Message Format",
|
||||
"fluxerMessageFormatNormal": "Normal (rich embeds)",
|
||||
"fluxerMessageFormatMinimalist": "Minimalist (short status)",
|
||||
"fluxerMessageFormatCustom": "Custom template",
|
||||
"fluxerUseMessageTemplate": "Use custom message template",
|
||||
"fluxerUseMessageTemplateDescription": "If enabled, the message will be sent using a custom template (LiquidJS). Leave blank to use the default Uptime Kuma format.",
|
||||
"fluxerMessageTemplate": "Message Template",
|
||||
"Fluxer Webhook URL": "Fluxer Webhook URL",
|
||||
"wayToGetFluxerURL": "You can get this by going to the target channel's settings > Webhooks > Create Webhook > Copy Webhook URL.",
|
||||
"Ip Family": "IP Family",
|
||||
"ipFamilyDescriptionAutoSelect": "Uses the {happyEyeballs} for determining the IP family.",
|
||||
"Happy Eyeballs algorithm": "Happy Eyeballs algorithm",
|
||||
|
||||
Reference in New Issue
Block a user