Skip to content

Commit

Permalink
Merge branch 'master' into 5066_add_rabbitmq_support
Browse files Browse the repository at this point in the history
  • Loading branch information
Suven-p authored Oct 18, 2024
2 parents 009b004 + d7ffa33 commit fd3dbff
Show file tree
Hide file tree
Showing 12 changed files with 1,173 additions and 95 deletions.
1,016 changes: 937 additions & 79 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@playwright/test": "~1.39.0",
"@popperjs/core": "~2.10.2",
"@testcontainers/hivemq": "^10.13.1",
"@types/bootstrap": "~5.1.9",
"@types/node": "^20.8.6",
"@typescript-eslint/eslint-plugin": "^6.7.5",
Expand Down Expand Up @@ -189,6 +190,7 @@
"stylelint-config-standard": "~25.0.0",
"terser": "~5.15.0",
"test": "~3.3.0",
"testcontainers": "^10.13.1",
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~5.2.8",
Expand Down
15 changes: 6 additions & 9 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1511,10 +1511,8 @@ class Monitor extends BeanModel {
return await R.getAll(`
SELECT monitor_notification.monitor_id, monitor_notification.notification_id
FROM monitor_notification
WHERE monitor_notification.monitor_id IN (?)
`, [
monitorIDs,
]);
WHERE monitor_notification.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
`, monitorIDs);
}

/**
Expand All @@ -1524,13 +1522,11 @@ class Monitor extends BeanModel {
*/
static async getMonitorTag(monitorIDs) {
return await R.getAll(`
SELECT monitor_tag.monitor_id, tag.name, tag.color
SELECT monitor_tag.monitor_id, monitor_tag.tag_id, tag.name, tag.color
FROM monitor_tag
JOIN tag ON monitor_tag.tag_id = tag.id
WHERE monitor_tag.monitor_id IN (?)
`, [
monitorIDs,
]);
WHERE monitor_tag.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
`, monitorIDs);
}

/**
Expand Down Expand Up @@ -1570,6 +1566,7 @@ class Monitor extends BeanModel {
tagsMap.set(row.monitor_id, []);
}
tagsMap.get(row.monitor_id).push({
tag_id: row.tag_id,
name: row.name,
color: row.color
});
Expand Down
65 changes: 65 additions & 0 deletions server/notification-providers/send-grid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");

class SendGrid extends NotificationProvider {
name = "SendGrid";

/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";

try {
let config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${notification.sendgridApiKey}`,
},
};

let personalizations = {
to: [{ email: notification.sendgridToEmail }],
};

// Add CC recipients if provided
if (notification.sendgridCcEmail) {
personalizations.cc = notification.sendgridCcEmail
.split(",")
.map((email) => ({ email: email.trim() }));
}

// Add BCC recipients if provided
if (notification.sendgridBccEmail) {
personalizations.bcc = notification.sendgridBccEmail
.split(",")
.map((email) => ({ email: email.trim() }));
}

let data = {
personalizations: [ personalizations ],
from: { email: notification.sendgridFromEmail.trim() },
subject:
notification.sendgridSubject ||
"Notification from Your Uptime Kuma",
content: [
{
type: "text/plain",
value: msg,
},
],
};

await axios.post(
"https://api.sendgrid.com/v3/mail/send",
data,
config
);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}

module.exports = SendGrid;
6 changes: 3 additions & 3 deletions server/notification-providers/slack.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class Slack extends NotificationProvider {
* @param {object} monitorJSON The monitor config
* @returns {Array} The relevant action objects
*/
static buildActions(baseURL, monitorJSON) {
buildActions(baseURL, monitorJSON) {
const actions = [];

if (baseURL) {
Expand Down Expand Up @@ -73,7 +73,7 @@ class Slack extends NotificationProvider {
* @param {string} msg The message body
* @returns {Array<object>} The rich content blocks for the Slack message
*/
static buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg) {
buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg) {

//create an array to dynamically add blocks
const blocks = [];
Expand Down Expand Up @@ -150,7 +150,7 @@ class Slack extends NotificationProvider {
data.attachments.push(
{
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
"blocks": Slack.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
"blocks": this.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
}
);
} else {
Expand Down
2 changes: 2 additions & 0 deletions server/notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const GtxMessaging = require("./notification-providers/gtx-messaging");
const Cellsynt = require("./notification-providers/cellsynt");
const Onesender = require("./notification-providers/onesender");
const Wpush = require("./notification-providers/wpush");
const SendGrid = require("./notification-providers/send-grid");

class Notification {

Expand Down Expand Up @@ -153,6 +154,7 @@ class Notification {
new GtxMessaging(),
new Cellsynt(),
new Wpush(),
new SendGrid()
];
for (let item of list) {
if (! item.name) {
Expand Down
1 change: 1 addition & 0 deletions src/components/NotificationDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export default {
"whapi": "WhatsApp (Whapi)",
"gtxmessaging": "GtxMessaging",
"Cellsynt": "Cellsynt",
"SendGrid": "SendGrid"
};
// Put notifications here if it's not supported in most regions or its documentation is not in English
Expand Down
4 changes: 2 additions & 2 deletions src/components/PublicGroupList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<template #item="monitor">
<div class="item" data-testid="monitor">
<div class="row">
<div class="col-6 col-md-4 small-padding">
<div class="col-9 col-md-8 small-padding">
<div class="info">
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
Expand Down Expand Up @@ -71,7 +71,7 @@
</div>
</div>
</div>
<div :key="$root.userHeartbeatBar" class="col-6 col-md-8">
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
</div>
</div>
Expand Down
47 changes: 47 additions & 0 deletions src/components/notifications/SendGrid.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<template>
<div class="mb-3">
<label for="sendgrid-api-key" class="form-label">{{ $t("SendGrid API Key") }}</label>
<HiddenInput id="push-api-key" v-model="$parent.notification.sendgridApiKey" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="sendgrid-from-email" class="form-label">{{ $t("From Email") }}</label>
<input id="sendgrid-from-email" v-model="$parent.notification.sendgridFromEmail" type="email" class="form-control" required>
</div>
<div class="mb-3">
<label for="sendgrid-to-email" class="form-label">{{ $t("To Email") }}</label>
<input id="sendgrid-to-email" v-model="$parent.notification.sendgridToEmail" type="email" class="form-control" required>
</div>
<div class="mb-3">
<label for="sendgrid-cc-email" class="form-label">{{ $t("smtpCC") }}</label>
<input id="sendgrid-cc-email" v-model="$parent.notification.sendgridCcEmail" type="email" class="form-control">
<div class="form-text">{{ $t("Separate multiple email addresses with commas") }}</div>
</div>
<div class="mb-3">
<label for="sendgrid-bcc-email" class="form-label">{{ $t("smtpBCC") }}</label>
<input id="sendgrid-bcc-email" v-model="$parent.notification.sendgridBccEmail" type="email" class="form-control">
<small class="form-text text-muted">{{ $t("Separate multiple email addresses with commas") }}</small>
</div>
<div class="mb-3">
<label for="sendgrid-subject" class="form-label">{{ $t("Subject:") }}</label>
<input id="sendgrid-subject" v-model="$parent.notification.sendgridSubject" type="text" class="form-control">
<small class="form-text text-muted">{{ $t("leave blank for default subject") }}</small>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://docs.sendgrid.com/api-reference/mail-send/mail-send" target="_blank">https://docs.sendgrid.com/api-reference/mail-send/mail-send</a>
</i18n-t>
</template>

<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
mounted() {
if (typeof this.$parent.notification.sendgridSubject === "undefined") {
this.$parent.notification.sendgridSubject = "Notification from Your Uptime Kuma";
}
},
};
</script>
4 changes: 3 additions & 1 deletion src/components/notifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import Whapi from "./Whapi.vue";
import Cellsynt from "./Cellsynt.vue";
import WPush from "./WPush.vue";
import SIGNL4 from "./SIGNL4.vue";
import SendGrid from "./SendGrid.vue";

/**
* Manage all notification form.
Expand Down Expand Up @@ -139,7 +140,8 @@ const NotificationFormList = {
"whapi": Whapi,
"gtxmessaging": GtxMessaging,
"Cellsynt": Cellsynt,
"WPush": WPush
"WPush": WPush,
"SendGrid": SendGrid,
};

export default NotificationFormList;
4 changes: 3 additions & 1 deletion src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1057,5 +1057,7 @@
"rabbitmqNodesRequired": "Please set the nodes for this monitor.",
"rabbitmqNodesInvalid": "Please use a complete URL for RabbitMQ nodes.",
"RabbitMQ Username": "RabbitMQ Username",
"RabbitMQ Password": "RabbitMQ Password"
"RabbitMQ Password": "RabbitMQ Password",
"SendGrid API Key": "SendGrid API Key",
"Separate multiple email addresses with commas": "Separate multiple email addresses with commas"
}
102 changes: 102 additions & 0 deletions test/backend-test/test-mqtt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const { HiveMQContainer } = require("@testcontainers/hivemq");
const mqtt = require("mqtt");
const { MqttMonitorType } = require("../../server/monitor-types/mqtt");
const { UP, PENDING } = require("../../src/util");

/**
* Runs an MQTT test with the
* @param {string} mqttSuccessMessage the message that the monitor expects
* @param {null|"keyword"|"json-query"} mqttCheckType the type of check we perform
* @param {string} receivedMessage what message is recieved from the mqtt channel
* @returns {Promise<Heartbeat>} the heartbeat produced by the check
*/
async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) {
const hiveMQContainer = await new HiveMQContainer().start();
const connectionString = hiveMQContainer.getConnectionString();
const mqttMonitorType = new MqttMonitorType();
const monitor = {
jsonPath: "firstProp", // always return firstProp for the json-query monitor
hostname: connectionString.split(":", 2).join(":"),
mqttTopic: "test",
port: connectionString.split(":")[2],
mqttUsername: null,
mqttPassword: null,
interval: 20, // controls the timeout
mqttSuccessMessage: mqttSuccessMessage, // for keywords
expectedValue: mqttSuccessMessage, // for json-query
mqttCheckType: mqttCheckType,
};
const heartbeat = {
msg: "",
status: PENDING,
};

const testMqttClient = mqtt.connect(hiveMQContainer.getConnectionString());
testMqttClient.on("connect", () => {
testMqttClient.subscribe("test", (error) => {
if (!error) {
testMqttClient.publish("test", receivedMessage);
}
});
});

try {
await mqttMonitorType.check(monitor, heartbeat, {});
} finally {
testMqttClient.end();
hiveMQContainer.stop();
}
return heartbeat;
}

describe("MqttMonitorType", {
concurrency: true,
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64")
}, () => {
test("valid keywords (type=default)", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});

test("valid keywords (type=keyword)", async () => {
const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});
test("invalid keywords (type=default)", async () => {
await assert.rejects(
testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
);
});

test("invalid keyword (type=keyword)", async () => {
await assert.rejects(
testMqtt("NOT_PRESENT", "keyword", "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
);
});
test("valid json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Message received, expected value is found");
});
test("invalid (because query fails) json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects(
testMqtt("[not_relevant]", "json-query", "{}"),
new Error("Message received but value is not equal to expected value, value was: [undefined]"),
);
});
test("invalid (because successMessage fails) json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects(
testMqtt("[wrong_success_messsage]", "json-query", "{\"firstProp\":\"present\"}"),
new Error("Message received but value is not equal to expected value, value was: [present]")
);
});
});

0 comments on commit fd3dbff

Please sign in to comment.