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 26 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
37 changes: 37 additions & 0 deletions Server/controllers/notificationController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import logger from '../utils/logger.js';

class NotificationController {
constructor(notificationService) {
this.notificationService = notificationService;
}

async triggerNotification(req, res) {
try {
const { monitorId, type, config } = req.body;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Validation should be done on this body at this level


const networkResponse = {
monitor: { _id: monitorId, name: "Test Monitor", url: "http://www.google.com" },
Copy link
Contributor

Choose a reason for hiding this comment

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

I see the name and url in networkResponse object are hardcoded. Was this just for testing?

status: false,
statusChanged: true,
prevStatus: true,
};
if (type === "webhook") {
await this.notificationService.sendWebhookNotification(
networkResponse,
config
);
}
res.json({ success: true, msg: "Notification sent successfully" });
} catch (error) {
logger.error({
message: error.message,
service: "NotificationController",
method: "triggerNotification",
stack: error.stack,
});
res.status(500).json({ success: false, msg: "Failed to send notification" });
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same for errors, please use the error handling middleware

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

}
}
}

export default NotificationController;
9 changes: 8 additions & 1 deletion Server/db/models/Notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ const NotificationSchema = mongoose.Schema(
},
type: {
type: String,
enum: ["email", "sms"],
enum: ["email", "sms", "webhook"],
},
config: {
type: String,
webhookUrl: String,
botToken: String,
chatId: String
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

Aplogoies, I looked at this too quickly on my phone last time I reviewed. This is not the correct way to define an object schema with Mongoose. You need to define a separate schema for Config like

const configSchema = mongoose.Schema({
    webhookUrl:  {type: String}
    ...
})

Then you would use the sub-schema in the notificaiton schema

config: {
type: configSchema
default: {...}
}


You can see an example of this in the `HardwareCheck` schema file.

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 done!

address: {
type: String,
},
Expand Down Expand Up @@ -76,4 +82,5 @@ NotificationSchema.pre("findOneAndUpdate", function (next) {
}
next();
});

export default mongoose.model("Notification", NotificationSchema);
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 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";
Expand Down Expand Up @@ -183,7 +187,7 @@
logger
);
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(
Expand Down Expand Up @@ -269,6 +273,10 @@
ServiceRegistry.get(StringService.SERVICE_NAME)
);

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

const distributedUptimeController = new DistributedUptimeController(
ServiceRegistry.get(MongoDB.SERVICE_NAME),
http,
Expand All @@ -289,6 +297,9 @@
const distributedUptimeRoutes = new DistributedUptimeRoutes(
distributedUptimeController
);

const notificationRoutes = new NotificationRoutes(notificationController);

// Init job queue
await jobQueue.initJobQueue();
// Middleware
Expand All @@ -311,6 +322,7 @@
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());

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix AI 1 day ago

To fix the problem, we need to introduce rate limiting to the Express application. The best way to do this is by using the express-rate-limit package, which allows us to set a maximum number of requests that can be made to the server within a specified time window. We will apply this rate limiter to the /api/v1/notifications route to prevent abuse.

  1. Install the express-rate-limit package.
  2. Import the express-rate-limit package in the Server/index.js file.
  3. Set up a rate limiter with appropriate configuration (e.g., maximum of 100 requests per 15 minutes).
  4. Apply the rate limiter to the /api/v1/notifications route.
Suggested changeset 2
Server/index.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/Server/index.js b/Server/index.js
--- a/Server/index.js
+++ b/Server/index.js
@@ -3,3 +3,3 @@
 import swaggerUi from "swagger-ui-express";
-
+import rateLimit from "express-rate-limit";
 import express from "express";
@@ -44,2 +44,8 @@
 
+// set up rate limiter: maximum of 100 requests per 15 minutes
+const limiter = rateLimit({
+    windowMs: 15 * 60 * 1000, // 15 minutes
+    max: 100, // limit each IP to 100 requests per windowMs
+});
+
 //JobQueue service and dependencies
@@ -307,3 +313,3 @@
 	app.use("/api/v1/status-page", statusPageRoutes.getRouter());
-	app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
+	app.use("/api/v1/notifications", limiter, verifyJWT, notificationRoutes.getRouter());
 	app.use(handleErrors);
EOF
@@ -3,3 +3,3 @@
import swaggerUi from "swagger-ui-express";

import rateLimit from "express-rate-limit";
import express from "express";
@@ -44,2 +44,8 @@

// set up rate limiter: maximum of 100 requests per 15 minutes
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
});

//JobQueue service and dependencies
@@ -307,3 +313,3 @@
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/notifications", limiter, verifyJWT, notificationRoutes.getRouter());
app.use(handleErrors);
Server/package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/Server/package.json b/Server/package.json
--- a/Server/package.json
+++ b/Server/package.json
@@ -40,3 +40,4 @@
 		"swagger-ui-express": "5.0.1",
-		"winston": "^3.13.0"
+		"winston": "^3.13.0",
+		"express-rate-limit": "^7.5.0"
 	},
EOF
@@ -40,3 +40,4 @@
"swagger-ui-express": "5.0.1",
"winston": "^3.13.0"
"winston": "^3.13.0",
"express-rate-limit": "^7.5.0"
},
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 7.5.0 None
Copilot is powered by AI and may make mistakes. Always verify output.
Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add rate limiting to protect against abuse.

The notification route is properly secured with JWT verification, but it lacks rate limiting which could make it vulnerable to abuse.

Apply this diff to add rate limiting:

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

And add the rate limiter middleware at the top of the file:

import rateLimit from 'express-rate-limit';

const rateLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});
🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 308-308: Missing rate limiting
This route handler performs authorization, but is not rate-limited.

app.use(handleErrors);
};

Expand Down
48 changes: 48 additions & 0 deletions Server/routes/notificationRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import express from 'express';
import { verifyJWT } from '../middleware/verifyJWT.js';
import { triggerNotificationBodyValidation } from '../validation/joi.js';

class NotificationRoutes {
constructor(notificationController) {
this.notificationController = notificationController;
this.router = express.Router();
this.initializeRoutes();
}

validateRequest(schema) {
return (req, res, next) => {
const { error } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});

if (error) {
const errorMessage = error.details
.map(detail => detail.message)
.join(', ');

return res.status(400).json({
success: false,
msg: errorMessage
});
}

next();
};
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Lets move the validation to the Controller, all validation in this application is done at the controller level. Please do follow the try/catch pattern of other validation schemas as well so we have predicatble error handling behaviour.

I don't think there's any reason to use middleware for this validation either. If we are going to use middleware for validation then all validation should be refactored to match. I don't think we want to do that right now, but perhaps in the future.

We should always follow convention as much as possible so we know where to look for things in the application.

Copy link
Member Author

Choose a reason for hiding this comment

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

Complete.


initializeRoutes() {
this.router.post(
'/trigger',
verifyJWT,
Fixed Show fixed Hide fixed

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
this.validateRequest(triggerNotificationBodyValidation),
this.notificationController.triggerNotification.bind(this.notificationController)
);
}

getRouter() {
return this.router;
}
}

export default NotificationRoutes;
46 changes: 46 additions & 0 deletions Server/service/networkService.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,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
73 changes: 66 additions & 7 deletions Server/service/notificationService.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const SERVICE_NAME = "NotificationService";
const TELEGRAM_API_BASE_URL = "https://api.telegram.org/bot";

class NotificationService {
static SERVICE_NAME = SERVICE_NAME;
Expand All @@ -9,13 +10,70 @@ 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;
}

formatNotificationMessage(monitor, status, platform, chatId) {
const messageText = `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}`;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Localization service neeed here

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.


if (platform === 'telegram') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

let's refactor these types into either an Array of acceptable types. This gives us the benefit of being able to easily add more types or change this value easily. Nothing worse than looking for hardcoded strings all over the place.

const PLATFORM_TYPES = ["telegram", "slack", "discord"];

Then you can just check if the value exists in the array or not.

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 done.

return { chat_id: chatId, text: messageText };
}
if (platform === 'slack') {
return { text: messageText };
}
if (platform === 'discord') {
return { content: messageText };
}
return null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you return undefined here instead of null? I'm trying to move towards using undefined everywhere for emtpy data.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure. All done.

}

async sendWebhookNotification(networkResponse, config) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We need JSdocs for these methods

const { monitor, status } = networkResponse;
const { type, webhookUrl, botToken, chatId } = config;
let url = webhookUrl;

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.

const message = this.formatNotificationMessage(monitor, status, type, chatId);
if (message === null) {
this.logger.warn({
message: `Unsupported webhook type: ${type}`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Make sure to use the localization service for user facing strings

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

service: this.SERVICE_NAME,
method: 'sendWebhookNotification',
type
});
return false;
}

if (type === 'telegram') {
if (!botToken || !chatId) {
return false;
}
url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`;
}

try {
const response = await this.networkService.requestWebhook(type, url, message);
return response.status;
} catch (error) {
this.logger.error({
message: `Error sending ${type} notification`,
service: this.SERVICE_NAME,
method: 'sendWebhookNotification',
error: error.message,
stack: error.stack,
url,
type,
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 @@ -59,19 +117,20 @@ class NotificationService {
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 (notification.type === "webhook") {
this.sendWebhookNotification(networkResponse, notification.address, notification.type);

// Handle other types of notifications here
}
}
return true;
} catch (error) {
this.logger.warn({
Expand Down
59 changes: 58 additions & 1 deletion Server/validation/joi.js
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,59 @@ const imageValidation = joi
"any.required": "Image file is required",
});

const telegramWebhookConfigValidation = joi.object({
type: joi.string().valid('telegram').required(),
botToken: joi.string().required().messages({
'string.empty': 'Telegram bot token is required',
'any.required': 'Telegram bot token is required'
}),
chatId: joi.string().required().messages({
'string.empty': 'Telegram chat ID is required',
'any.required': 'Telegram chat ID is required'
})
});

const discordWebhookConfigValidation = joi.object({
type: joi.string().valid('discord').required(),
webhookUrl: joi.string().uri().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'
})
});

const slackWebhookConfigValidation = joi.object({
type: joi.string().valid('slack').required(),
webhookUrl: joi.string().uri().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'
})
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't need three different validation schemas, one is sufficient since they are all the same

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.


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'
}),
config: joi.alternatives()
.conditional('type', {
is: 'webhook',
then: joi.alternatives().try(
telegramWebhookConfigValidation,
discordWebhookConfigValidation,
slackWebhookConfigValidation
).required().messages({
'any.required': 'Webhook configuration is required'
})
})
});

export {
roleValidatior,
loginValidation,
Expand Down Expand Up @@ -533,5 +586,9 @@ export {
createStatusPageBodyValidation,
getStatusPageParamValidation,
getStatusPageQueryValidation,
imageValidation,
imageValidation,
triggerNotificationBodyValidation,
telegramWebhookConfigValidation,
discordWebhookConfigValidation,
slackWebhookConfigValidation
};