diff --git a/common/Defs.ts b/common/Defs.ts index 850872e0..79d77ae6 100644 --- a/common/Defs.ts +++ b/common/Defs.ts @@ -79,7 +79,10 @@ export const NotificationProvidersList = [ { id: NotificationProvider.NTFY, name: "Ntfy" }, ]; -export const NotificationCategories = [ +export const NotificationCategories: { + id: NotificationCategory; + name: string; +}[] = [ { id: "offlineStatusChange", name: "Offline status change" }, { id: "streamOnline", name: "Stream online" }, { id: "streamOffline", name: "Stream offline" }, diff --git a/common/ServerConfig.ts b/common/ServerConfig.ts index e6f6570b..f2f50b32 100644 --- a/common/ServerConfig.ts +++ b/common/ServerConfig.ts @@ -605,14 +605,14 @@ export const settingsFields: Record = { type: "string", }, - "notify.ntfy.enabled": { + "notifications.ntfy.enabled": { group: "Notifications (Ntfy)", text: "Enable Ntfy notifications", type: "boolean", default: false, }, - "notify.ntfy.url": { + "notifications.ntfy.url": { group: "Notifications (Ntfy)", text: "Ntfy url with topic", type: "string", diff --git a/server/.eslintrc.js b/server/.eslintrc.js index 83139d1e..e1c6b3fe 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -3,10 +3,7 @@ module.exports = { // "browser": true, es2021: true, }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended-type-checked", - ], + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], parser: "@typescript-eslint/parser", parserOptions: { ecmaVersion: "latest", diff --git a/server/src/Core/ClientBroker.ts b/server/src/Core/ClientBroker.ts index 9e6d46bc..be96cc33 100644 --- a/server/src/Core/ClientBroker.ts +++ b/server/src/Core/ClientBroker.ts @@ -3,8 +3,6 @@ import { debugLog } from "@/Helpers/Console"; import type { NotificationCategory } from "@common/Defs"; import { NotificationCategories, NotificationProvider } from "@common/Defs"; import type { NotifyData } from "@common/Webhook"; -import type { AxiosError } from "axios"; -import axios from "axios"; import chalk from "chalk"; import type express from "express"; import fs from "node:fs"; @@ -14,6 +12,9 @@ import { BaseConfigPath } from "./BaseConfig"; import { Config } from "./Config"; import { LiveStreamDVR } from "./LiveStreamDVR"; import { LOGLEVEL, log } from "./Log"; +import DiscordNotify from "./Notifiers/Discord"; +import NtfyNotify from "./Notifiers/Ntfy"; +import PushoverNotify from "./Notifiers/Pushover"; import TelegramNotify from "./Notifiers/Telegram"; interface Client { @@ -25,107 +26,23 @@ interface Client { authenticated: boolean; } -interface DiscordSendMessagePayload { - content: string; - username?: string; - avatar_url?: string; - tts?: boolean; - embeds?: DiscordEmbed[]; - allowed_mentions?: unknown; - components?: unknown; - files?: unknown; - payload_json?: string; - attachments?: unknown; - flags?: number; -} - -interface DiscordEmbed { - title?: string; - type?: string; - description?: string; - url?: string; - timestamp?: string; - color?: number; - footer?: DiscordEmbedFooter; - image?: DiscordEmbedImage; - thumbnail?: DiscordEmbedThumbnail; - video?: DiscordEmbedVideo; - provider?: DiscordEmbedProvider; - author?: DiscordEmbedAuthor; - fields?: DiscordEmbedField[]; -} - -interface DiscordEmbedFooter { - text: string; - icon_url?: string; - proxy_icon_url?: string; -} - -interface DiscordEmbedImage { - url?: string; - proxy_url?: string; - height?: number; - width?: number; -} - -interface DiscordEmbedThumbnail { - url?: string; - proxy_url?: string; - height?: number; - width?: number; -} - -interface DiscordEmbedVideo { - url?: string; - proxy_url?: string; - height?: number; - width?: number; -} - -interface DiscordEmbedProvider { - name?: string; - url?: string; -} - -interface DiscordEmbedAuthor { - name?: string; - url?: string; - icon_url?: string; - proxy_icon_url?: string; -} - -interface DiscordEmbedField { - name: string; - value: string; - inline?: boolean; -} - -interface PushoverSendMessagePayload { - token: string; - user: string; - message: string; - attachment?: string; - attachment_base64?: string; - attachment_type?: string; - device?: string; - html?: 1; - priority?: -2 | -1 | 0 | 1 | 2; - sound?: string; - timestamp?: number; - title?: string; - ttl?: number; - url?: string; - url_title?: string; -} - export class ClientBroker { public static clients: Client[] = []; public static wss: WebSocket.Server | undefined = undefined; // bitmask of notification categories and providers - public static notificationSettings: Record = - {} as Record; + public static notificationSettings: Record = { + offlineStatusChange: 0, + streamOnline: 0, + streamOffline: 0, + streamStatusChange: 0, + streamStatusChangeFavourite: 0, + vodMuted: 0, + vodDeleted: 0, + debug: 0, + system: 0, + }; public static attach(server: WebSocket.Server): void { log( @@ -343,13 +260,13 @@ export class ClientBroker { } if ( - Config.getInstance().cfg("notify.ntfy.enabled") && + Config.getInstance().cfg("notifications.ntfy.enabled") && ClientBroker.getNotificationSettingForProvider( category, NotificationProvider.NTFY ) ) { - TelegramNotify(title, body, icon, category, url, tts); + NtfyNotify(title, body, icon, category, url, tts); } if ( @@ -359,40 +276,7 @@ export class ClientBroker { NotificationProvider.DISCORD ) ) { - axios - .post(Config.getInstance().cfg("discord_webhook"), { - content: `**${title}**\n${body}${url ? `\n\n${url}` : ""}`, - avatar_url: - icon && icon.startsWith("https") ? icon : undefined, // only allow https - tts: tts, - } as DiscordSendMessagePayload) - .then((res) => { - log( - LOGLEVEL.DEBUG, - "clientBroker.notify", - "Discord response", - res.data - ); - }) - .catch((err: AxiosError) => { - if (axios.isAxiosError(err)) { - log( - LOGLEVEL.ERROR, - "clientBroker.notify", - `Discord axios error: ${ - err.message - } (${JSON.stringify(err.response?.data)})`, - { err: err, response: err.response?.data } - ); - } else { - log( - LOGLEVEL.ERROR, - "clientBroker.notify", - `Discord error: ${(err as Error).message}`, - err - ); - } - }); + DiscordNotify(title, body, icon, category, url, tts); } if ( @@ -402,91 +286,7 @@ export class ClientBroker { NotificationProvider.PUSHOVER ) ) { - // escape with backslash - // const escaped_title = title.replace(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/g, "\\$&"); - // const escaped_body = body.replace(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/g, "\\$&"); - - axios - .post("https://api.pushover.net/1/messages.json", { - token: Config.getInstance().cfg( - "notifications.pushover.token" - ), - user: Config.getInstance().cfg( - "notifications.pushover.user" - ), - title: title, - message: body, - url: url, - // html: 1, - } as PushoverSendMessagePayload) - .then((res) => { - log( - LOGLEVEL.DEBUG, - "clientBroker.notify", - "Pushover response", - res.data - ); - }) - .catch((err: Error) => { - if (axios.isAxiosError(err)) { - // const data = err.response?.data; - // TwitchlogAdvanced(LOGLEVEL.ERROR, "notify", `Telegram axios error: ${err.message} (${data})`, { err: err, response: data }); - // console.error(chalk.bgRed.whiteBright(`Telegram axios error: ${err.message} (${data})`), JSON.stringify(err, null, 2)); - - if (err.response) { - log( - LOGLEVEL.ERROR, - "clientBroker.notify", - `Pushover axios error response: ${err.message} (${err.response.data})`, - { err: err, response: err.response.data } - ); - console.error( - chalk.bgRed.whiteBright( - `Pushover axios error response : ${err.message} (${err.response.data})` - ), - JSON.stringify(err, null, 2) - ); - } else if (err.request) { - log( - LOGLEVEL.ERROR, - "clientBroker.notify", - `Pushover axios error request: ${err.message} (${err.request})`, - { err: err, request: err.request } - ); - console.error( - chalk.bgRed.whiteBright( - `Pushover axios error request: ${err.message} (${err.request})` - ), - JSON.stringify(err, null, 2) - ); - } else { - log( - LOGLEVEL.ERROR, - "clientBroker.notify", - `Pushover axios error: ${err.message}`, - err - ); - console.error( - chalk.bgRed.whiteBright( - `Pushover axios error: ${err.message}` - ), - JSON.stringify(err, null, 2) - ); - } - } else { - log( - LOGLEVEL.ERROR, - "clientBroker.notify", - `Pushover error: ${err.message}`, - err - ); - console.error( - chalk.bgRed.whiteBright( - `Pushover error: ${err.message}` - ) - ); - } - }); + PushoverNotify(title, body, icon, category, url, tts); } } @@ -529,20 +329,20 @@ export class ClientBroker { const settings = JSON.parse(data); for (const category of NotificationCategories) { - if (settings[category.id as NotificationCategory]) { - this.notificationSettings[category.id as NotificationCategory] = - settings[category.id as NotificationCategory]; + if (!category.id || !category.name) continue; + if (settings[category.id]) { + this.notificationSettings[category.id] = settings[category.id]; } else { - this.notificationSettings[ - category.id as NotificationCategory - ] = 0; + this.notificationSettings[category.id] = 0; } } } public static saveNotificationSettings() { - const data = JSON.stringify(this.notificationSettings); - fs.writeFileSync(BaseConfigPath.notifications, data); + fs.writeFileSync( + BaseConfigPath.notifications, + JSON.stringify(this.notificationSettings) + ); } private static onConnect( diff --git a/server/src/Core/Notifiers/Discord.ts b/server/src/Core/Notifiers/Discord.ts new file mode 100644 index 00000000..1c4e5f00 --- /dev/null +++ b/server/src/Core/Notifiers/Discord.ts @@ -0,0 +1,123 @@ +import type { NotificationCategory } from "@common/Defs"; +import type { AxiosError } from "axios"; +import axios from "axios"; +import { Config } from "../Config"; +import { LOGLEVEL, log } from "../Log"; + +interface DiscordSendMessagePayload { + content: string; + username?: string; + avatar_url?: string; + tts?: boolean; + embeds?: DiscordEmbed[]; + allowed_mentions?: unknown; + components?: unknown; + files?: unknown; + payload_json?: string; + attachments?: unknown; + flags?: number; +} + +interface DiscordEmbed { + title?: string; + type?: string; + description?: string; + url?: string; + timestamp?: string; + color?: number; + footer?: DiscordEmbedFooter; + image?: DiscordEmbedImage; + thumbnail?: DiscordEmbedThumbnail; + video?: DiscordEmbedVideo; + provider?: DiscordEmbedProvider; + author?: DiscordEmbedAuthor; + fields?: DiscordEmbedField[]; +} + +interface DiscordEmbedFooter { + text: string; + icon_url?: string; + proxy_icon_url?: string; +} + +interface DiscordEmbedImage { + url?: string; + proxy_url?: string; + height?: number; + width?: number; +} + +interface DiscordEmbedThumbnail { + url?: string; + proxy_url?: string; + height?: number; + width?: number; +} + +interface DiscordEmbedVideo { + url?: string; + proxy_url?: string; + height?: number; + width?: number; +} + +interface DiscordEmbedProvider { + name?: string; + url?: string; +} + +interface DiscordEmbedAuthor { + name?: string; + url?: string; + icon_url?: string; + proxy_icon_url?: string; +} + +interface DiscordEmbedField { + name: string; + value: string; + inline?: boolean; +} + +export default function notify( + title: string, + body = "", + icon = "", + category: NotificationCategory, // change this? + url = "", + tts = false +) { + axios + .post(Config.getInstance().cfg("discord_webhook"), { + content: `**${title}**\n${body}${url ? `\n\n${url}` : ""}`, + avatar_url: icon && icon.startsWith("https") ? icon : undefined, // only allow https + tts: tts, + } as DiscordSendMessagePayload) + .then((res) => { + log( + LOGLEVEL.DEBUG, + "clientBroker.notify", + "Discord response", + res.data + ); + }) + .catch((err: AxiosError) => { + if (axios.isAxiosError(err)) { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Discord axios error: ${err.message} (${JSON.stringify( + err.response?.data + )})`, + { err: err, response: err.response?.data } + ); + } else { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Discord error: ${(err as Error).message}`, + err + ); + } + }); +} diff --git a/server/src/Core/Notifiers/Ntfy.ts b/server/src/Core/Notifiers/Ntfy.ts index 2c674233..03431854 100644 --- a/server/src/Core/Notifiers/Ntfy.ts +++ b/server/src/Core/Notifiers/Ntfy.ts @@ -12,7 +12,7 @@ export default function notify( url = "", tts = false ) { - const ntfyUrl = Config.getInstance().cfg("notify.ntfy.url"); + const ntfyUrl = Config.getInstance().cfg("notifications.ntfy.url"); if (ntfyUrl) { axios diff --git a/server/src/Core/Notifiers/Pushover.ts b/server/src/Core/Notifiers/Pushover.ts new file mode 100644 index 00000000..ac242749 --- /dev/null +++ b/server/src/Core/Notifiers/Pushover.ts @@ -0,0 +1,112 @@ +import type { NotificationCategory } from "@common/Defs"; +import axios from "axios"; +import chalk from "chalk"; +import { Config } from "../Config"; +import { LOGLEVEL, log } from "../Log"; + +interface PushoverSendMessagePayload { + token: string; + user: string; + message: string; + attachment?: string; + attachment_base64?: string; + attachment_type?: string; + device?: string; + html?: 1; + priority?: -2 | -1 | 0 | 1 | 2; + sound?: string; + timestamp?: number; + title?: string; + ttl?: number; + url?: string; + url_title?: string; +} + +export default function notify( + title: string, + body = "", + icon = "", + category: NotificationCategory, // change this? + url = "", + tts = false +) { + // escape with backslash + // const escaped_title = title.replace(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/g, "\\$&"); + // const escaped_body = body.replace(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/g, "\\$&"); + + axios + .post("https://api.pushover.net/1/messages.json", { + token: Config.getInstance().cfg("notifications.pushover.token"), + user: Config.getInstance().cfg("notifications.pushover.user"), + title: title, + message: body, + url: url, + // html: 1, + } as PushoverSendMessagePayload) + .then((res) => { + log( + LOGLEVEL.DEBUG, + "clientBroker.notify", + "Pushover response", + res.data + ); + }) + .catch((err: Error) => { + if (axios.isAxiosError(err)) { + // const data = err.response?.data; + // TwitchlogAdvanced(LOGLEVEL.ERROR, "notify", `Telegram axios error: ${err.message} (${data})`, { err: err, response: data }); + // console.error(chalk.bgRed.whiteBright(`Telegram axios error: ${err.message} (${data})`), JSON.stringify(err, null, 2)); + + if (err.response) { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Pushover axios error response: ${err.message} (${err.response.data})`, + { err: err, response: err.response.data } + ); + console.error( + chalk.bgRed.whiteBright( + `Pushover axios error response : ${err.message} (${err.response.data})` + ), + JSON.stringify(err, null, 2) + ); + } else if (err.request) { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Pushover axios error request: ${err.message} (${err.request})`, + { err: err, request: err.request } + ); + console.error( + chalk.bgRed.whiteBright( + `Pushover axios error request: ${err.message} (${err.request})` + ), + JSON.stringify(err, null, 2) + ); + } else { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Pushover axios error: ${err.message}`, + err + ); + console.error( + chalk.bgRed.whiteBright( + `Pushover axios error: ${err.message}` + ), + JSON.stringify(err, null, 2) + ); + } + } else { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Pushover error: ${err.message}`, + err + ); + console.error( + chalk.bgRed.whiteBright(`Pushover error: ${err.message}`) + ); + } + }); +}