mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-08 10:49:39 -06:00
Merge pull request #1612 from bluewave-labs/feat/be/webhook-integrations
Add Discord webhook notification support
This commit is contained in:
59
Server/controllers/notificationController.js
Normal file
59
Server/controllers/notificationController.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
triggerNotificationBodyValidation,
|
||||
} from '../validation/joi.js';
|
||||
import { handleError, handleValidationError } from './controllerUtils.js';
|
||||
|
||||
const SERVICE_NAME = "NotificationController";
|
||||
|
||||
class NotificationController {
|
||||
constructor(notificationService, stringService) {
|
||||
this.notificationService = notificationService;
|
||||
this.stringService = stringService;
|
||||
this.triggerNotification = this.triggerNotification.bind(this);
|
||||
}
|
||||
|
||||
async triggerNotification(req, res, next) {
|
||||
try {
|
||||
await triggerNotificationBodyValidation.validateAsync(req.body, {
|
||||
abortEarly: false,
|
||||
stripUnknown: true
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { monitorId, type, platform, config } = req.body;
|
||||
|
||||
const networkResponse = {
|
||||
monitor: { _id: monitorId, name: "Test Monitor", url: "http://www.google.com" },
|
||||
status: false,
|
||||
statusChanged: true,
|
||||
prevStatus: true,
|
||||
};
|
||||
|
||||
if (type === "webhook") {
|
||||
const notification = {
|
||||
type,
|
||||
platform,
|
||||
config
|
||||
};
|
||||
|
||||
await this.notificationService.sendWebhookNotification(
|
||||
networkResponse,
|
||||
notification
|
||||
);
|
||||
}
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.webhookSendSuccess
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "triggerNotification"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationController;
|
||||
@@ -1,4 +1,11 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const configSchema = mongoose.Schema({
|
||||
webhookUrl: { type: String },
|
||||
botToken: { type: String },
|
||||
chatId: { type: String }
|
||||
}, { _id: false });
|
||||
|
||||
const NotificationSchema = mongoose.Schema(
|
||||
{
|
||||
monitorId: {
|
||||
@@ -8,8 +15,12 @@ const NotificationSchema = mongoose.Schema(
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ["email", "sms"],
|
||||
enum: ["email", "sms", "webhook"],
|
||||
},
|
||||
config: {
|
||||
type: configSchema,
|
||||
default: () => ({})
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
},
|
||||
@@ -76,4 +87,5 @@ NotificationSchema.pre("findOneAndUpdate", function (next) {
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
export default mongoose.model("Notification", NotificationSchema);
|
||||
|
||||
@@ -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";
|
||||
|
||||
import NotificationController from "./controllers/notificationController.js";
|
||||
|
||||
//JobQueue service and dependencies
|
||||
import JobQueue from "./service/jobQueue.js";
|
||||
import { Queue, Worker } from "bullmq";
|
||||
@@ -166,7 +170,7 @@ const startApp = async () => {
|
||||
logger
|
||||
);
|
||||
const statusService = new StatusService(db, logger);
|
||||
const notificationService = new NotificationService(emailService, db, logger);
|
||||
const notificationService = new NotificationService(emailService, db, logger, networkService, stringService);
|
||||
|
||||
const jobQueue = new JobQueue(
|
||||
db,
|
||||
@@ -251,6 +255,11 @@ const startApp = async () => {
|
||||
ServiceRegistry.get(StringService.SERVICE_NAME)
|
||||
);
|
||||
|
||||
const notificationController = new NotificationController(
|
||||
ServiceRegistry.get(NotificationService.SERVICE_NAME),
|
||||
ServiceRegistry.get(StringService.SERVICE_NAME)
|
||||
);
|
||||
|
||||
const distributedUptimeController = new DistributedUptimeController(
|
||||
ServiceRegistry.get(MongoDB.SERVICE_NAME),
|
||||
http,
|
||||
@@ -271,6 +280,9 @@ const startApp = async () => {
|
||||
const distributedUptimeRoutes = new DistributedUptimeRoutes(
|
||||
distributedUptimeController
|
||||
);
|
||||
|
||||
const notificationRoutes = new NotificationRoutes(notificationController);
|
||||
|
||||
// Init job queue
|
||||
await jobQueue.initJobQueue();
|
||||
// Middleware
|
||||
@@ -293,6 +305,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", verifyJWT, notificationRoutes.getRouter());
|
||||
app.use(handleErrors);
|
||||
};
|
||||
|
||||
|
||||
24
Server/routes/notificationRoute.js
Normal file
24
Server/routes/notificationRoute.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import express from 'express';
|
||||
import { verifyJWT } from '../middleware/verifyJWT.js';
|
||||
|
||||
class NotificationRoutes {
|
||||
constructor(notificationController) {
|
||||
this.notificationController = notificationController;
|
||||
this.router = express.Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
initializeRoutes() {
|
||||
this.router.post(
|
||||
'/trigger',
|
||||
verifyJWT,
|
||||
this.notificationController.triggerNotification
|
||||
);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
return this.router;
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationRoutes;
|
||||
@@ -433,6 +433,46 @@ class NetworkService {
|
||||
throw err;
|
||||
}
|
||||
|
||||
async requestWebhook(platform, url, message) {
|
||||
try {
|
||||
const response = await this.axios.post(url, message, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'webhook',
|
||||
status: true,
|
||||
code: response.status,
|
||||
message: `Successfully sent ${platform} notification`,
|
||||
payload: response.data
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
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 {
|
||||
type: 'webhook',
|
||||
status: false,
|
||||
code: error.response?.status || this.NETWORK_ERROR,
|
||||
message: `Failed to send ${platform} notification`,
|
||||
payload: error.response?.data
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the status of a job based on its type and returns the appropriate response.
|
||||
*
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
const SERVICE_NAME = "NotificationService";
|
||||
const TELEGRAM_API_BASE_URL = "https://api.telegram.org/bot";
|
||||
const PLATFORM_TYPES = ['telegram', 'slack', 'discord'];
|
||||
|
||||
const MESSAGE_FORMATTERS = {
|
||||
telegram: (messageText, chatId) => ({ chat_id: chatId, text: messageText }),
|
||||
slack: (messageText) => ({ text: messageText }),
|
||||
discord: (messageText) => ({ content: messageText })
|
||||
};
|
||||
|
||||
class NotificationService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
@@ -8,14 +16,111 @@ class NotificationService {
|
||||
* @param {Object} emailService - The email service used for sending notifications.
|
||||
* @param {Object} db - The database instance for storing notification data.
|
||||
* @param {Object} logger - The logger instance for logging activities.
|
||||
* @param {Object} networkService - The network service for sending webhook notifications.
|
||||
*/
|
||||
constructor(emailService, db, logger) {
|
||||
constructor(emailService, db, logger, networkService, stringService) {
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
this.emailService = emailService;
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.networkService = networkService;
|
||||
this.stringService = stringService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a notification message based on the monitor status and platform.
|
||||
*
|
||||
* @param {Object} monitor - The monitor object.
|
||||
* @param {string} monitor.name - The name of the monitor.
|
||||
* @param {string} monitor.url - The URL of the monitor.
|
||||
* @param {boolean} status - The current status of the monitor (true for up, false for down).
|
||||
* @param {string} platform - The notification platform (e.g., "telegram", "slack", "discord").
|
||||
* @param {string} [chatId] - The chat ID for platforms that require it (e.g., Telegram).
|
||||
* @returns {Object|null} The formatted message object for the specified platform, or null if the platform is unsupported.
|
||||
*/
|
||||
|
||||
formatNotificationMessage(monitor, status, platform, chatId) {
|
||||
const messageText = this.stringService.getMonitorStatus(
|
||||
monitor.name,
|
||||
status,
|
||||
monitor.url
|
||||
);
|
||||
|
||||
if (!PLATFORM_TYPES.includes(platform)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return MESSAGE_FORMATTERS[platform](messageText, chatId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a webhook notification to a specified platform.
|
||||
*
|
||||
* @param {Object} networkResponse - The response object from the network.
|
||||
* @param {Object} networkResponse.monitor - The monitor object.
|
||||
* @param {boolean} networkResponse.status - The monitor's status (true for up, false for down).
|
||||
* @param {Object} notification - The notification settings.
|
||||
* @param {string} notification.platform - The target platform ("telegram", "slack", "discord").
|
||||
* @param {Object} notification.config - The configuration object for the webhook.
|
||||
* @param {string} notification.config.webhookUrl - The webhook URL for the platform.
|
||||
* @param {string} [notification.config.botToken] - The bot token for Telegram notifications.
|
||||
* @param {string} [notification.config.chatId] - The chat ID for Telegram notifications.
|
||||
* @returns {Promise<boolean>} A promise that resolves to true if the notification was sent successfully, otherwise false.
|
||||
*/
|
||||
|
||||
async sendWebhookNotification(networkResponse, notification) {
|
||||
const { monitor, status } = networkResponse;
|
||||
const { platform } = notification;
|
||||
const { webhookUrl, botToken, chatId } = notification.config;
|
||||
|
||||
// Early return if platform is not supported
|
||||
if (!PLATFORM_TYPES.includes(platform)) {
|
||||
this.logger.warn({
|
||||
message: this.stringService.getWebhookUnsupportedPlatform(platform),
|
||||
service: this.SERVICE_NAME,
|
||||
method: 'sendWebhookNotification',
|
||||
platform
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Early return for telegram if required fields are missing
|
||||
if (platform === 'telegram' && (!botToken || !chatId)) {
|
||||
this.logger.warn({
|
||||
message: 'Missing required fields for Telegram notification',
|
||||
service: this.SERVICE_NAME,
|
||||
method: 'sendWebhookNotification',
|
||||
platform
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
let url = webhookUrl;
|
||||
if (platform === 'telegram') {
|
||||
url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`;
|
||||
}
|
||||
|
||||
// Now that we know the platform is valid, format the message
|
||||
const message = this.formatNotificationMessage(monitor, status, platform, chatId);
|
||||
|
||||
try {
|
||||
const response = await this.networkService.requestWebhook(platform, url, message);
|
||||
return response.status;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: this.stringService.getWebhookSendError(platform),
|
||||
service: this.SERVICE_NAME,
|
||||
method: 'sendWebhookNotification',
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
url,
|
||||
platform,
|
||||
requestPayload: message
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email notification for hardware infrastructure alerts
|
||||
*
|
||||
@@ -57,18 +162,18 @@ class NotificationService {
|
||||
|
||||
async handleStatusNotifications(networkResponse) {
|
||||
try {
|
||||
//If status hasn't changed, we're done
|
||||
// If status hasn't changed, we're done
|
||||
if (networkResponse.statusChanged === false) return false;
|
||||
|
||||
// if prevStatus is undefined, monitor is resuming, we're done
|
||||
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);
|
||||
await this.sendEmail(networkResponse, notification.address);
|
||||
} else if (notification.type === "webhook") {
|
||||
await this.sendWebhookNotification(networkResponse, notification);
|
||||
}
|
||||
// Handle other types of notifications here
|
||||
}
|
||||
|
||||
@@ -165,6 +165,36 @@ class StringService {
|
||||
return this.translationService.getTranslation('maintenanceWindowEdit');
|
||||
}
|
||||
|
||||
// Webhook Messages
|
||||
get webhookUnsupportedPlatform() {
|
||||
return this.translationService.getTranslation('webhookUnsupportedPlatform');
|
||||
}
|
||||
|
||||
get webhookSendError() {
|
||||
return this.translationService.getTranslation('webhookSendError');
|
||||
}
|
||||
|
||||
get webhookSendSuccess() {
|
||||
return this.translationService.getTranslation('webhookSendSuccess');
|
||||
}
|
||||
|
||||
getWebhookUnsupportedPlatform(platform) {
|
||||
return this.translationService.getTranslation('webhookUnsupportedPlatform')
|
||||
.replace('{platform}', platform);
|
||||
}
|
||||
|
||||
getWebhookSendError(platform) {
|
||||
return this.translationService.getTranslation('webhookSendError')
|
||||
.replace('{platform}', platform);
|
||||
}
|
||||
|
||||
getMonitorStatus(name, status, url) {
|
||||
return this.translationService.getTranslation('monitorStatus')
|
||||
.replace('{name}', name)
|
||||
.replace('{status}', status ? "up" : "down")
|
||||
.replace('{url}', url);
|
||||
}
|
||||
|
||||
// Error Messages
|
||||
get unknownError() {
|
||||
return this.translationService.getTranslation('unknownError');
|
||||
|
||||
@@ -475,6 +475,72 @@ const imageValidation = joi
|
||||
"any.required": "Image file is required",
|
||||
});
|
||||
|
||||
const webhookConfigValidation = joi.object({
|
||||
webhookUrl: joi.string().uri()
|
||||
.when('$platform', {
|
||||
switch: [
|
||||
{
|
||||
is: 'telegram',
|
||||
then: joi.optional()
|
||||
},
|
||||
{
|
||||
is: 'discord',
|
||||
then: joi.required().messages({
|
||||
'string.empty': 'Discord webhook URL is required',
|
||||
'string.uri': 'Discord webhook URL must be a valid URL',
|
||||
'any.required': 'Discord webhook URL is required'
|
||||
})
|
||||
},
|
||||
{
|
||||
is: 'slack',
|
||||
then: joi.required().messages({
|
||||
'string.empty': 'Slack webhook URL is required',
|
||||
'string.uri': 'Slack webhook URL must be a valid URL',
|
||||
'any.required': 'Slack webhook URL is required'
|
||||
})
|
||||
}
|
||||
]
|
||||
}),
|
||||
botToken: joi.string()
|
||||
.when('$platform', {
|
||||
is: 'telegram',
|
||||
then: joi.required().messages({
|
||||
'string.empty': 'Telegram bot token is required',
|
||||
'any.required': 'Telegram bot token is required'
|
||||
}),
|
||||
otherwise: joi.optional()
|
||||
}),
|
||||
chatId: joi.string()
|
||||
.when('$platform', {
|
||||
is: 'telegram',
|
||||
then: joi.required().messages({
|
||||
'string.empty': 'Telegram chat ID is required',
|
||||
'any.required': 'Telegram chat ID is required'
|
||||
}),
|
||||
otherwise: joi.optional()
|
||||
})
|
||||
}).required();
|
||||
|
||||
const triggerNotificationBodyValidation = joi.object({
|
||||
monitorId: joi.string().required().messages({
|
||||
'string.empty': 'Monitor ID is required',
|
||||
'any.required': 'Monitor ID is required'
|
||||
}),
|
||||
type: joi.string().valid('webhook').required().messages({
|
||||
'string.empty': 'Notification type is required',
|
||||
'any.required': 'Notification type is required',
|
||||
'any.only': 'Notification type must be webhook'
|
||||
}),
|
||||
platform: joi.string().valid('telegram', 'discord', 'slack').required().messages({
|
||||
'string.empty': 'Platform type is required',
|
||||
'any.required': 'Platform type is required',
|
||||
'any.only': 'Platform must be telegram, discord, or slack'
|
||||
}),
|
||||
config: webhookConfigValidation.required().messages({
|
||||
'any.required': 'Webhook configuration is required'
|
||||
})
|
||||
});
|
||||
|
||||
export {
|
||||
roleValidatior,
|
||||
loginValidation,
|
||||
@@ -533,5 +599,7 @@ export {
|
||||
createStatusPageBodyValidation,
|
||||
getStatusPageParamValidation,
|
||||
getStatusPageQueryValidation,
|
||||
imageValidation,
|
||||
imageValidation,
|
||||
triggerNotificationBodyValidation,
|
||||
webhookConfigValidation,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user