diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts index f2543886d..63b24d7b7 100644 --- a/server/src/service/infrastructure/notificationProviders/matrix.ts +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -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 => { + 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(`

${this.escapeHtml(message.content.title)}

`); + + // Summary + plainLines.push(""); + plainLines.push(message.content.summary); + htmlLines.push(`

${this.escapeHtml(message.content.summary)}

`); + + // 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(`

Monitor Details

`); + htmlLines.push(``); + + // Threshold breaches (if any) + if (message.content.thresholds && message.content.thresholds.length > 0) { + plainLines.push(""); + plainLines.push("## Threshold Breaches"); + htmlLines.push(`

Threshold Breaches

`); + htmlLines.push(``); + } + + // Additional details (if any) + if (message.content.details && message.content.details.length > 0) { + plainLines.push(""); + plainLines.push("## Additional Information"); + htmlLines.push(`

Additional Information

`); + htmlLines.push(``); + } + + // 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(`

View Incident

`); + } + + // Footer with timestamp + plainLines.push(""); + plainLines.push(`Checkmate | ${new Date(message.content.timestamp).toUTCString()}`); + htmlLines.push(`
`); + htmlLines.push(`

Checkmate | ${new Date(message.content.timestamp).toUTCString()}

`); + + return { + plainText: plainLines.join("\n"), + htmlText: htmlLines.join(""), + }; + } + + /** + * Escape HTML special characters for safe rendering + */ + private escapeHtml(text: string): string { + return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); + } } diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 253102f4e..0c6918510 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -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":