This commit is contained in:
Alex Holliday committed 2026-02-16 21:49:42 +00:00
1 parent 0e1dc3e3fe
commit 6f55e4934f
2 files changed
+157

No files matched your search

@@ -3,6 +3,7 @@ import got from "got";
import type { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import {
buildHardwareAlerts,
buildHardwareNotificationMessage,
@@ -113,4 +114,151 @@ export class MatrixProvider implements INotificationProvider {
return false;
}
};
/**
* New unified message format - builds Matrix HTML message from NotificationMessage
*/
sendMessage = async (notification: Notification, message: NotificationMessage): Promise<boolean> => {
const { homeserverUrl, accessToken, roomId } = notification;
if (!homeserverUrl || !accessToken || !roomId) {
return false;
}
const { plainText, htmlText } = this.buildMatrixMessage(message);
const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`;
const body = {
msgtype: "m.text",
body: plainText,
format: "org.matrix.custom.html",
formatted_body: htmlText,
};
try {
await got.post(url, {
json: body,
headers: {
"Content-Type": "application/json",
},
});
this.logger.info({
message: "[NEW] Matrix notification sent via sendMessage",
service: SERVICE_NAME,
method: "sendMessage",
});
return true;
} catch (error) {
const err = error as Error;
this.logger.warn({
message: "[NEW] Matrix alert failed via sendMessage",
service: SERVICE_NAME,
method: "sendMessage",
stack: err?.stack,
});
return false;
}
};
/**
* Build Matrix message from NotificationMessage
* Returns both plain text and HTML formatted versions
* Matrix supports HTML subset for rich formatting
*/
private buildMatrixMessage(message: NotificationMessage): { plainText: string; htmlText: string } {
const plainLines: string[] = [];
const htmlLines: string[] = [];
// Determine color based on severity
const colorMap = {
critical: "#FF0000", // Red
warning: "#FFA500", // Orange
success: "#00FF00", // Green
info: "#0000FF", // Blue
};
const color = colorMap[message.severity] || "#808080";
// Title
plainLines.push(`# ${message.content.title}`);
htmlLines.push(`<h2 style="color: ${color};">${this.escapeHtml(message.content.title)}</h2>`);
// Summary
plainLines.push("");
plainLines.push(message.content.summary);
htmlLines.push(`<p>${this.escapeHtml(message.content.summary)}</p>`);
// Monitor details
plainLines.push("");
plainLines.push("## Monitor Details");
plainLines.push(`- Name: ${message.monitor.name}`);
plainLines.push(`- URL: ${message.monitor.url}`);
plainLines.push(`- Type: ${message.monitor.type}`);
plainLines.push(`- Status: ${message.monitor.status}`);
htmlLines.push(`<h3>Monitor Details</h3>`);
htmlLines.push(`<ul>`);
htmlLines.push(`<li><strong>Name:</strong> ${this.escapeHtml(message.monitor.name)}</li>`);
htmlLines.push(`<li><strong>URL:</strong> <a href="${this.escapeHtml(message.monitor.url)}">${this.escapeHtml(message.monitor.url)}</a></li>`);
htmlLines.push(`<li><strong>Type:</strong> ${this.escapeHtml(message.monitor.type)}</li>`);
htmlLines.push(`<li><strong>Status:</strong> ${this.escapeHtml(message.monitor.status)}</li>`);
htmlLines.push(`</ul>`);
// Threshold breaches (if any)
if (message.content.thresholds && message.content.thresholds.length > 0) {
plainLines.push("");
plainLines.push("## Threshold Breaches");
htmlLines.push(`<h3>Threshold Breaches</h3>`);
htmlLines.push(`<ul>`);
message.content.thresholds.forEach((breach) => {
plainLines.push(`- ${breach.metric.toUpperCase()}: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`);
htmlLines.push(
`<li><strong>${this.escapeHtml(breach.metric.toUpperCase())}:</strong> ${this.escapeHtml(breach.formattedValue)} (threshold: ${breach.threshold}${this.escapeHtml(breach.unit)})</li>`
);
});
htmlLines.push(`</ul>`);
}
// Additional details (if any)
if (message.content.details && message.content.details.length > 0) {
plainLines.push("");
plainLines.push("## Additional Information");
htmlLines.push(`<h3>Additional Information</h3>`);
htmlLines.push(`<ul>`);
message.content.details.forEach((detail) => {
plainLines.push(`- ${detail}`);
htmlLines.push(`<li>${this.escapeHtml(detail)}</li>`);
});
htmlLines.push(`</ul>`);
}
// Incident link (if incident exists)
if (message.content.incident) {
const incidentUrl = `${message.clientHost}/infrastructure/${message.monitor.id}`;
plainLines.push("");
plainLines.push(`View Incident: ${incidentUrl}`);
htmlLines.push(`<p><a href="${this.escapeHtml(incidentUrl)}">View Incident</a></p>`);
}
// Footer with timestamp
plainLines.push("");
plainLines.push(`Checkmate | ${new Date(message.content.timestamp).toUTCString()}`);
htmlLines.push(`<hr>`);
htmlLines.push(`<p><small>Checkmate | ${new Date(message.content.timestamp).toUTCString()}</small></p>`);
return {
plainText: plainLines.join("\n"),
htmlText: htmlLines.join(""),
};
}
/**
* Escape HTML special characters for safe rendering
*/
private escapeHtml(text: string): string {
return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
}
@@ -90,6 +90,15 @@ export class NotificationsService implements INotificationsService {
return await this.slackProvider.sendMessage(notification, notificationMessage);
}
if (notification.type === "matrix" && this.matrixProvider.sendMessage && notificationMessage) {
this.logger.info({
message: "[NEW] Using sendMessage for matrix",
service: SERVICE_NAME,
method: "send",
});
return await this.matrixProvider.sendMessage(notification, notificationMessage);
}
// Fallback to existing sendAlert for all providers
switch (notification.type) {
case "email":