feat: Add Matrix notification support Fixes #3034

This commit is contained in:
Tudor Gradinaru committed 2025-10-25 18:35:15 +03:00
1 parent daf5797fed
commit 1fad85d48d
9 files changed
+210 -6

No files matched your search

@@ -223,6 +223,42 @@ const CreateNotifications = () => {
<Typography component="p">{t(DESCRIPTION_MAP[type])}</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
{type === "matrix" ? (
<>
<TextInput
label={t("createNotifications.matrixSettings.homeserverLabel")}
name="homeserverUrl"
placeholder={t(
"createNotifications.matrixSettings.homeserverPlaceholder"
)}
value={notification.homeserverUrl || ""}
onChange={onChange}
error={Boolean(errors.homeserverUrl)}
helperText={errors["homeserverUrl"]}
/>
<TextInput
label={t("createNotifications.matrixSettings.roomIdLabel")}
name="roomId"
placeholder={t("createNotifications.matrixSettings.roomIdPlaceholder")}
value={notification.roomId || ""}
onChange={onChange}
error={Boolean(errors.roomId)}
helperText={errors["roomId"]}
/>
<TextInput
label={t("createNotifications.matrixSettings.accessTokenLabel")}
name="accessToken"
type="password"
placeholder={t(
"createNotifications.matrixSettings.accessTokenPlaceholder"
)}
value={notification.accessToken || ""}
onChange={onChange}
error={Boolean(errors.accessToken)}
helperText={errors["accessToken"]}
/>
</>
) : (
<TextInput
label={t(LABEL_MAP[type])}
name="address"
@@ -232,9 +268,9 @@ const CreateNotifications = () => {
error={Boolean(errors.address)}
helperText={errors["address"]}
/>
)}
</Stack>
</ConfigBox>
</ConfigBox>{" "}
<Stack
direction="row"
justifyContent="flex-end"
@@ -4,6 +4,7 @@ export const NOTIFICATION_TYPES = [
{ _id: 3, name: "PagerDuty", value: "pager_duty" },
{ _id: 4, name: "Webhook", value: "webhook" },
{ _id: 5, name: "Discord", value: "discord" },
{ _id: 6, name: "Matrix", value: "matrix" },
];
export const TITLE_MAP = {
@@ -12,6 +13,7 @@ export const TITLE_MAP = {
pager_duty: "createNotifications.pagerdutySettings.title",
webhook: "createNotifications.webhookSettings.title",
discord: "createNotifications.discordSettings.title",
matrix: "createNotifications.matrixSettings.title",
};
export const DESCRIPTION_MAP = {
@@ -20,6 +22,7 @@ export const DESCRIPTION_MAP = {
pager_duty: "createNotifications.pagerdutySettings.description",
webhook: "createNotifications.webhookSettings.description",
discord: "createNotifications.discordSettings.description",
matrix: "createNotifications.matrixSettings.description",
};
export const LABEL_MAP = {
@@ -28,6 +31,7 @@ export const LABEL_MAP = {
pager_duty: "createNotifications.pagerdutySettings.integrationKeyLabel",
webhook: "createNotifications.webhookSettings.webhookLabel",
discord: "createNotifications.discordSettings.webhookLabel",
matrix: "createNotifications.matrixSettings.homeserverLabel",
};
export const PLACEHOLDER_MAP = {
@@ -36,4 +40,5 @@ export const PLACEHOLDER_MAP = {
pager_duty: "createNotifications.pagerdutySettings.integrationKeyPlaceholder",
webhook: "createNotifications.webhookSettings.webhookPlaceholder",
discord: "createNotifications.discordSettings.webhookPlaceholder",
matrix: "createNotifications.matrixSettings.homeserverPlaceholder",
};
+33 -1
View File
@@ -458,7 +458,7 @@ const notificationValidation = joi.object({
type: joi
.string()
.valid("email", "webhook", "slack", "discord", "pager_duty")
.valid("email", "webhook", "slack", "discord", "pager_duty", "matrix")
.required()
.messages({
"string.empty": "Notification type is required",
@@ -495,8 +495,40 @@ const notificationValidation = joi.object({
"string.uri": "Please enter a valid Webhook URL",
}),
},
{
is: "matrix",
then: joi.string().allow("").optional(),
},
],
}),
homeserverUrl: joi.when("type", {
is: "matrix",
then: joi.string().uri().required().messages({
"string.empty": "Homeserver URL cannot be empty",
"any.required": "Homeserver URL is required",
"string.uri": "Please enter a valid Homeserver URL",
}),
otherwise: joi.string().allow("").optional(),
}),
roomId: joi.when("type", {
is: "matrix",
then: joi.string().required().messages({
"string.empty": "Room ID cannot be empty",
"any.required": "Room ID is required",
}),
otherwise: joi.string().allow("").optional(),
}),
accessToken: joi.when("type", {
is: "matrix",
then: joi.string().required().messages({
"string.empty": "Access Token cannot be empty",
"any.required": "Access Token is required",
}),
otherwise: joi.string().allow("").optional(),
}),
});
const editUserValidation = joi.object({
+10
View File
@@ -736,6 +736,16 @@
"webhookLabel": "Discord Webhook URL",
"webhookPlaceholder": "https://your-server.com/webhook"
},
"matrixSettings": {
"title": "Matrix",
"description": "Configure your Matrix notification here",
"homeserverLabel": "Homeserver URL",
"homeserverPlaceholder": "https://matrix.org",
"roomIdLabel": "Room ID",
"roomIdPlaceholder": "!QMdRCpUIfLwsfjxye6:home.server",
"accessTokenLabel": "Access Token",
"accessTokenPlaceholder": "Your Matrix access token"
},
"webhookSettings": {
"title": "Webhook",
"description": "Configure your webhook here",
+11 -1
View File
@@ -16,7 +16,7 @@ const NotificationSchema = mongoose.Schema(
},
type: {
type: String,
enum: ["email", "slack", "discord", "webhook", "pager_duty"],
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"],
},
notificationName: {
type: String,
@@ -28,6 +28,16 @@ const NotificationSchema = mongoose.Schema(
phone: {
type: String,
},
// Matrix-specific fields
homeserverUrl: {
type: String,
},
roomId: {
type: String,
},
accessToken: {
type: String,
},
},
{
timestamps: true,
@@ -529,6 +529,42 @@ class NetworkService {
throw error;
}
}
async requestMatrix({ homeserverUrl, accessToken, roomId, message }) {
try {
const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`;
const body = {
msgtype: "m.text",
body: message,
format: "org.matrix.custom.html",
formatted_body: message,
};
const response = await this.axios.post(url, body, {
headers: {
"Content-Type": "application/json",
},
});
return {
status: true,
code: response.status,
message: "Successfully sent Matrix notification",
};
} catch (error) {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
method: "requestMatrix",
});
return {
status: false,
code: error.response?.status || this.NETWORK_ERROR,
message: "Failed to send Matrix notification",
payload: error.response?.data,
};
}
}
}
export default NetworkService;
@@ -0,0 +1,34 @@
const SERVICE_NAME = "Matrix";
class Matrix {
static SERVICE_NAME = SERVICE_NAME;
constructor({ networkService, logger }) {
this.networkService = networkService;
this.logger = logger;
}
get serviceName() {
return Matrix.SERVICE_NAME;
}
async send({ friendlyName, homeserverUrl, accessToken, roomId, message, monitorName }) {
const title = `Checkmate status for ${monitorName}`;
const formattedMessage = `## ${title}\n${message}`;
try {
await this.networkService.requestMatrix({
homeserverUrl,
accessToken,
roomId,
message: formattedMessage,
});
this.logger.info(`Successfully sent Matrix notification for ${friendlyName}`);
return true;
} catch (error) {
this.logger.error(`Failed to send Matrix notification for ${friendlyName}: ${error.message}`);
return false;
}
}
}
export default Matrix;
@@ -1,4 +1,5 @@
const SERVICE_NAME = "NotificationService";
import Matrix from "./notificationProviders/matrix.js";
class NotificationService {
static SERVICE_NAME = SERVICE_NAME;
@@ -46,6 +47,14 @@ class NotificationService {
return response;
}
if (type === "matrix") {
const { friendlyName, homeserverUrl, accessToken, roomId } = notification;
const monitorName = subject;
const message = content;
const matrix = new Matrix({ networkService: this.networkService, logger: this.logger });
const success = await matrix.send({ friendlyName, homeserverUrl, accessToken, roomId, message, monitorName });
return success;
}
};
async handleNotifications(networkResponse) {
+34 -2
View File
@@ -575,10 +575,10 @@ const createNotificationBodyValidation = joi.object({
"any.required": "Notification name is required",
}),
type: joi.string().valid("email", "webhook", "slack", "discord", "pager_duty").required().messages({
type: joi.string().valid("email", "webhook", "slack", "discord", "pager_duty", "matrix").required().messages({
"string.empty": "Notification type is required",
"any.required": "Notification type is required",
"any.only": "Notification type must be email, webhook, or pager_duty",
"any.only": "Notification type must be email, webhook, slack, discord, pager_duty, or matrix",
}),
address: joi.when("type", {
@@ -606,8 +606,40 @@ const createNotificationBodyValidation = joi.object({
"string.uri": "Please enter a valid Webhook URL",
}),
},
{
is: "matrix",
then: joi.string().allow("").optional(),
},
],
}),
homeserverUrl: joi.when("type", {
is: "matrix",
then: joi.string().uri().required().messages({
"string.empty": "Homeserver URL cannot be empty",
"any.required": "Homeserver URL is required",
"string.uri": "Please enter a valid Homeserver URL",
}),
otherwise: joi.string().allow("").optional(),
}),
roomId: joi.when("type", {
is: "matrix",
then: joi.string().required().messages({
"string.empty": "Room ID cannot be empty",
"any.required": "Room ID is required",
}),
otherwise: joi.string().allow("").optional(),
}),
accessToken: joi.when("type", {
is: "matrix",
then: joi.string().required().messages({
"string.empty": "Access Token cannot be empty",
"any.required": "Access Token is required",
}),
otherwise: joi.string().allow("").optional(),
}),
});
//****************************************