Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Discord webhook notification support #1612

Merged
merged 42 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
28f5d35
Add Discord webhook notification support
Skorpios604 Jan 22, 2025
172597e
Add Slack webhook integration.
Skorpios604 Jan 23, 2025
d3810db
Added telegram notifications
Skorpios604 Jan 26, 2025
d83773a
Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-…
Skorpios604 Jan 26, 2025
84fe58b
Refactor webhook integrations functions.
Skorpios604 Jan 26, 2025
92d9573
Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-…
Skorpios604 Feb 2, 2025
cc48f1d
Use the NetworkService for making network requests.
Skorpios604 Feb 2, 2025
8667f74
Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-…
Skorpios604 Feb 3, 2025
0bb7eae
Got rid of imports.
Skorpios604 Feb 3, 2025
d3fb41a
Refactor BASE URL.
Skorpios604 Feb 3, 2025
8f94062
Store bot token and chat id in their own respective fields.
Skorpios604 Feb 3, 2025
1d9f94c
Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-…
Skorpios604 Feb 4, 2025
e44cbf3
Stop execution in the event of an unwanted platform.
Skorpios604 Feb 4, 2025
aa977ce
Refactor out to a template for easier message maintenance.
Skorpios604 Feb 4, 2025
964a923
Returned comments.
Skorpios604 Feb 4, 2025
e9bcf66
Resolved merge conflict in Server/index.js
Skorpios604 Feb 9, 2025
64c5aa9
Added notifcation controller and route.
Skorpios604 Feb 9, 2025
01e73fe
Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-…
Skorpios604 Feb 9, 2025
d8379f7
Configured a config object in the notification schema.
Skorpios604 Feb 9, 2025
f46b076
Refactored notification schema config object.
Skorpios604 Feb 9, 2025
ccd6c58
Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-…
Skorpios604 Feb 11, 2025
ef41712
Got rid of falsey value.
Skorpios604 Feb 11, 2025
fa50706
Simplified network response.
Skorpios604 Feb 11, 2025
67da574
Secured notifications route.
Skorpios604 Feb 11, 2025
023d943
Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-…
Skorpios604 Feb 13, 2025
012483d
Automated validation via joi.
Skorpios604 Feb 13, 2025
8007daa
Used response handling middleware for the response format.
Skorpios604 Feb 13, 2025
70ffe1b
Use middleware for handling errors.
Skorpios604 Feb 13, 2025
12d9f15
Got rid context bind in the route.
Skorpios604 Feb 13, 2025
07882e0
Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-…
Skorpios604 Feb 17, 2025
8409571
Defined new schema for the config object.
Skorpios604 Feb 17, 2025
55c1a1c
Moved validation to the controller.
Skorpios604 Feb 17, 2025
ff2b3b4
Removed timing request for the webhook.
Skorpios604 Feb 17, 2025
6152679
Update Docs.
Skorpios604 Feb 17, 2025
e4e3f9c
Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-…
Skorpios604 Feb 17, 2025
f22b67a
Use the localization service for user facing strings.
Skorpios604 Feb 17, 2025
d710ec0
Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-…
Skorpios604 Feb 17, 2025
c4168a5
Removed null and replace with undefined.
Skorpios604 Feb 17, 2025
afc9d46
Refactored notification messsages into an array of acceptable types.
Skorpios604 Feb 17, 2025
03a938d
Check if platform type is accepted before formatting the message.
Skorpios604 Feb 17, 2025
5be17af
Used 1 validation schema for all platforms.
Skorpios604 Feb 18, 2025
4ef2b29
Used string service instead of hardcoded value.
Skorpios604 Feb 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Server/db/models/Notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const NotificationSchema = mongoose.Schema(
},
type: {
type: String,
enum: ["email", "sms"],
enum: ["email", "sms", "discord", "slack", "telegram"],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need all these types anymore do we? They are all of type "webhook" now

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup! You are right. All done.

},
address: {
type: String,
Expand Down
14 changes: 13 additions & 1 deletion Server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ import QueueController from "./controllers/queueController.js";
import DistributedUptimeRoutes from "./routes/distributedUptimeRoute.js";
import DistributedUptimeController from "./controllers/distributedUptimeController.js";

import NotificationRoutes from "./routes/notificationRoute.js"; // Add this line

import NotificationController from "./controllers/notificationController.js";

//JobQueue service and dependencies
import JobQueue from "./service/jobQueue.js";
import { Queue, Worker } from "bullmq";
Expand Down Expand Up @@ -174,7 +178,7 @@ const startApp = async () => {
);
const networkService = new NetworkService(axios, ping, logger, http, Docker, net);
const statusService = new StatusService(db, logger);
const notificationService = new NotificationService(emailService, db, logger);
const notificationService = new NotificationService(emailService, db, logger, networkService);

const jobQueue = new JobQueue(
db,
Expand Down Expand Up @@ -244,6 +248,10 @@ const startApp = async () => {
ServiceRegistry.get(MongoDB.SERVICE_NAME)
);

const notificationController = new NotificationController(
ServiceRegistry.get(NotificationService.SERVICE_NAME)
);

const distributedUptimeController = new DistributedUptimeController();

//Create routes
Expand All @@ -260,6 +268,9 @@ const startApp = async () => {
const distributedUptimeRoutes = new DistributedUptimeRoutes(
distributedUptimeController
);

const notificationRoutes = new NotificationRoutes(notificationController);

// Init job queue
await jobQueue.initJobQueue();
// Middleware
Expand All @@ -284,6 +295,7 @@ const startApp = async () => {
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
app.use("/api/v1/distributed-uptime", distributedUptimeRoutes.getRouter());
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use("/api/v1/notifications", notificationRoutes.getRouter()); // Add this line
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed now, this was used for testing correct?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure to secure this route

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All secured.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Yo, secure this endpoint!

The notification routes are exposed without JWT verification, which could allow unauthorized access.

-    app.use("/api/v1/notifications", notificationRoutes.getRouter());
+    app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.use("/api/v1/notifications", notificationRoutes.getRouter()); // Add this line
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter()); // Add this line

app.use(handleErrors);
};

Expand Down
46 changes: 46 additions & 0 deletions Server/service/networkService.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,52 @@ class NetworkService {
throw err;
}

async requestWebhook(platform, url, message) {
try {
const { response, responseTime, error } = await this.timeRequest(() =>
this.axios.post(url, message, {
headers: {
'Content-Type': 'application/json'
}
})
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we timing the webook request? I don't think we're interested in how long it took to carry out the webhook request are we?

We can just make the axios request directly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. No need. Got rid of it.


const webhookResponse = {
type: 'webhook',
responseTime,
payload: response?.data
};

if (error) {
webhookResponse.status = false;
webhookResponse.code = error.response?.status || this.NETWORK_ERROR;
webhookResponse.message = `Failed to send ${platform} notification`;
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
method: 'requestWebhook',
url,
platform,
error: error.message,
statusCode: error.response?.status,
responseData: error.response?.data,
requestPayload: message
});
return webhookResponse;
}

webhookResponse.status = true;
webhookResponse.code = response.status;
webhookResponse.message = `Successfully sent ${platform} notification`;
return webhookResponse;
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = 'requestWebhook';
throw error;
}
}


/**
* Gets the status of a job based on its type and returns the appropriate response.
*
Expand Down
61 changes: 52 additions & 9 deletions Server/service/notificationService.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const SERVICE_NAME = "NotificationService";
import NetworkService from "./networkService.js";


class NotificationService {
static SERVICE_NAME = SERVICE_NAME;
Expand All @@ -9,13 +11,55 @@ class NotificationService {
* @param {Object} db - The database instance for storing notification data.
* @param {Object} logger - The logger instance for logging activities.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be sure to update the JSdoc since this now takes a networkService parameter

*/
constructor(emailService, db, logger) {
constructor(emailService, db, logger, networkService) {
this.SERVICE_NAME = SERVICE_NAME;
this.emailService = emailService;
this.db = db;
this.logger = logger;
this.networkService = networkService;
}

async sendWebhookNotification(networkResponse, address, platform) {
const { monitor, status } = networkResponse;
let message;
let url = address;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of first trying to build a message and then checking if the message is undefined or not, let's not even try to build a message if the webhook type is not supported.

Just check if the type is in the acceptable type array described above, if not return early.

We should always try to return as early as possible to do as little work as necessary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.
Implemented.

if (platform === 'slack') {
message = { text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` };
} else if (platform === 'discord') {
message = { content: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` };
} else if (platform === 'telegram') {
const [botToken, chatId] = address.split('|').map(part => part?.trim());
if (!botToken || !chatId) {
return false;
}
message = {
chat_id: chatId,
text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}`
};
url = `https://api.telegram.org/bot${botToken}/sendMessage`;
}

try {
const response = await this.networkService.requestWebhook(platform, url, message);
return response.status;
} catch (error) {
this.logger.error({
message: `Error sending ${platform} notification`,
service: this.SERVICE_NAME,
method: 'sendWebhookNotification',
error: error.message,
stack: error.stack,
url,
platform,
requestPayload: message
});
return false;
ajhollid marked this conversation as resolved.
Show resolved Hide resolved
}
}



/**
* Sends an email notification for hardware infrastructure alerts
*
Expand Down Expand Up @@ -57,21 +101,20 @@ class NotificationService {

async handleStatusNotifications(networkResponse) {
try {
//If status hasn't changed, we're done
if (networkResponse.statusChanged === false) return false;

// if prevStatus is undefined, monitor is resuming, we're done
ajhollid marked this conversation as resolved.
Show resolved Hide resolved
if (networkResponse.prevStatus === undefined) return false;
const notifications = await this.db.getNotificationsByMonitorId(
networkResponse.monitorId
);


const notifications = await this.db.getNotificationsByMonitorId(networkResponse.monitorId);

for (const notification of notifications) {
if (notification.type === "email") {
this.sendEmail(networkResponse, notification.address);
}
} else if (["discord", "slack", "telegram"].includes(notification.type)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified to just if(notification.type === "webhook") as these all function the same way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion. Thank you. All done.

this.sendWebhookNotification(networkResponse, notification.address, notification.type);

// Handle other types of notifications here
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Yo, there's a syntax error in this loop!

The for loop is missing a closing brace.

Here's the fix:

  for (const notification of notifications) {
    if (notification.type === "email") {
      this.sendEmail(networkResponse, notification.address);
    } else if (["discord", "slack", "telegram"].includes(notification.type)) {
      this.sendWebhookNotification(networkResponse, notification.address, notification.type);
    }
-   // Handle other types of notifications here
  }
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const notification of notifications) {
if (notification.type === "email") {
this.sendEmail(networkResponse, notification.address);
}
} else if (["discord", "slack", "telegram"].includes(notification.type)) {
this.sendWebhookNotification(networkResponse, notification.address, notification.type);
// Handle other types of notifications here
}
}
for (const notification of notifications) {
if (notification.type === "email") {
this.sendEmail(networkResponse, notification.address);
} else if (["discord", "slack", "telegram"].includes(notification.type)) {
this.sendWebhookNotification(networkResponse, notification.address, notification.type);
}
// Handle other types of notifications here
}
}

return true;
} catch (error) {
this.logger.warn({
Expand Down