diff --git a/common/Defs.ts b/common/Defs.ts index 3f1f5cd6..850872e0 100644 --- a/common/Defs.ts +++ b/common/Defs.ts @@ -1,87 +1,114 @@ -export enum SubStatus { - NONE = "0", - WAITING = "1", - SUBSCRIBED = "2", - FAILED = "3", -} - -export enum MuteStatus { - UNMUTED = 1, - MUTED = 2, - UNKNOWN = 3, -} - -export enum ExistStatus { - EXISTS = 1, - NOT_EXISTS = 2, - NEVER_EXISTED = 3, - UNKNOWN = 4, -} - -export const VideoQualityArray: string[] = ["best", "1080p60", "1080p", "720p60", "720p", "480p", "360p", "160p", "140p", "worst", "audio_only"]; - -export const nonGameCategories = [ - "Just Chatting", - "IRL", - "Art", - "Music", - "Pools, Hot Tubs, and Beaches", - "Sports", - "ASMR", - "Talk Shows & Podcasts", - "Special Events", - "Beauty & Body Art", - "Animals, Aquariums, and Zoos", - "Travel & Outdoors", - "Makers & Crafting", - "Software and Game Development", - "Science & Technology", - "Food & Drink", - "Politics", - "Fitness & Health", -]; - -export type Providers = "base" | "twitch" | "youtube" | "kick"; - -// twitch vod age is 14 days then it's deleted -export const TwitchVodAge = 14 * 24 * 60 * 60 * 1000; - -export enum NotificationProvider { - /** Websocket to all browser clients */ - WEBSOCKET = 1 << 0, - /** Telegram bot */ - TELEGRAM = 1 << 1, - /** Discord webhook */ - DISCORD = 1 << 2, - /** Pushover */ - PUSHOVER = 1 << 3, -} - -export const NotificationProvidersList = [ - { id: NotificationProvider.WEBSOCKET, name: "WebSocket" }, - { id: NotificationProvider.TELEGRAM, name: "Telegram" }, - { id: NotificationProvider.DISCORD, name: "Discord" }, - { id: NotificationProvider.PUSHOVER, name: "Pushover" }, -]; - -export const NotificationCategories = [ - { "id": "offlineStatusChange", name: "Offline status change" }, - { "id": "streamOnline", name: "Stream online" }, - { "id": "streamOffline", name: "Stream offline" }, - { "id": "streamStatusChange", name: "Stream status change" }, - { "id": "streamStatusChangeFavourite", name: "Stream status change with favourite game" }, - { "id": "vodMuted", name: "VOD muted" }, - { "id": "vodDeleted", name: "VOD deleted" }, - { "id": "debug", name: "Debug" }, - { "id": "system", name: "System" }, -]; - -export type NotificationCategory = "offlineStatusChange" | "streamOnline" | "streamOffline" | "streamStatusChange" | "streamStatusChangeFavourite" | "vodMuted" | "vodDeleted" | "debug" | "system"; - -export enum JobStatus { - NONE = "NONE", - WAITING = "WAITING", - RUNNING = "RUNNING", - STOPPED = "STOPPED", - ERROR = "ERROR", -} \ No newline at end of file +export enum SubStatus { + NONE = "0", + WAITING = "1", + SUBSCRIBED = "2", + FAILED = "3", +} + +export enum MuteStatus { + UNMUTED = 1, + MUTED = 2, + UNKNOWN = 3, +} + +export enum ExistStatus { + EXISTS = 1, + NOT_EXISTS = 2, + NEVER_EXISTED = 3, + UNKNOWN = 4, +} + +export const VideoQualityArray: string[] = [ + "best", + "1080p60", + "1080p", + "720p60", + "720p", + "480p", + "360p", + "160p", + "140p", + "worst", + "audio_only", +]; + +export const nonGameCategories = [ + "Just Chatting", + "IRL", + "Art", + "Music", + "Pools, Hot Tubs, and Beaches", + "Sports", + "ASMR", + "Talk Shows & Podcasts", + "Special Events", + "Beauty & Body Art", + "Animals, Aquariums, and Zoos", + "Travel & Outdoors", + "Makers & Crafting", + "Software and Game Development", + "Science & Technology", + "Food & Drink", + "Politics", + "Fitness & Health", +]; + +export type Providers = "base" | "twitch" | "youtube" | "kick"; + +// twitch vod age is 14 days then it's deleted +export const TwitchVodAge = 14 * 24 * 60 * 60 * 1000; + +export enum NotificationProvider { + /** Websocket to all browser clients */ + WEBSOCKET = 1 << 0, + /** Telegram bot */ + TELEGRAM = 1 << 1, + /** Discord webhook */ + DISCORD = 1 << 2, + /** Pushover */ + PUSHOVER = 1 << 3, + /** Ntfy */ + NTFY = 1 << 4, +} + +export const NotificationProvidersList = [ + { id: NotificationProvider.WEBSOCKET, name: "WebSocket" }, + { id: NotificationProvider.TELEGRAM, name: "Telegram" }, + { id: NotificationProvider.DISCORD, name: "Discord" }, + { id: NotificationProvider.PUSHOVER, name: "Pushover" }, + { id: NotificationProvider.NTFY, name: "Ntfy" }, +]; + +export const NotificationCategories = [ + { id: "offlineStatusChange", name: "Offline status change" }, + { id: "streamOnline", name: "Stream online" }, + { id: "streamOffline", name: "Stream offline" }, + { id: "streamStatusChange", name: "Stream status change" }, + { + id: "streamStatusChangeFavourite", + name: "Stream status change with favourite game", + }, + { id: "vodMuted", name: "VOD muted" }, + { id: "vodDeleted", name: "VOD deleted" }, + { id: "debug", name: "Debug" }, + { id: "system", name: "System" }, +]; + +export type NotificationCategory = + | "offlineStatusChange" + | "streamOnline" + | "streamOffline" + | "streamStatusChange" + | "streamStatusChangeFavourite" + | "vodMuted" + | "vodDeleted" + | "debug" + | "system"; + +export enum JobStatus { + NONE = "NONE", + WAITING = "WAITING", + RUNNING = "RUNNING", + STOPPED = "STOPPED", + ERROR = "ERROR", +} diff --git a/common/ServerConfig.ts b/common/ServerConfig.ts index e75a8136..e6f6570b 100644 --- a/common/ServerConfig.ts +++ b/common/ServerConfig.ts @@ -605,6 +605,19 @@ export const settingsFields: Record = { type: "string", }, + "notify.ntfy.enabled": { + group: "Notifications (Ntfy)", + text: "Enable Ntfy notifications", + type: "boolean", + default: false, + }, + + "notify.ntfy.url": { + group: "Notifications (Ntfy)", + text: "Ntfy url with topic", + type: "string", + }, + // discord discord_enabled: { group: "Notifications (Discord)", diff --git a/server/.vscode/settings.json b/server/.vscode/settings.json index dc6d1717..438d4880 100644 --- a/server/.vscode/settings.json +++ b/server/.vscode/settings.json @@ -37,6 +37,6 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" } } diff --git a/server/src/Core/ClientBroker.ts b/server/src/Core/ClientBroker.ts index 19089d7f..9e6d46bc 100644 --- a/server/src/Core/ClientBroker.ts +++ b/server/src/Core/ClientBroker.ts @@ -14,6 +14,7 @@ import { BaseConfigPath } from "./BaseConfig"; import { Config } from "./Config"; import { LiveStreamDVR } from "./LiveStreamDVR"; import { LOGLEVEL, log } from "./Log"; +import TelegramNotify from "./Notifiers/Telegram"; interface Client { id: string; @@ -24,19 +25,6 @@ interface Client { authenticated: boolean; } -interface TelegramSendMessagePayload { - chat_id: number; - text: string; - parse_mode?: "MarkdownV2" | "Markdown" | "HTML"; - entities?: unknown; - disable_web_page_preview?: boolean; - disable_notification?: boolean; - protect_content?: boolean; - reply_to_message_id?: number; - allow_sending_without_reply?: boolean; - reply_markup?: unknown; -} - interface DiscordSendMessagePayload { content: string; username?: string; @@ -351,121 +339,17 @@ export class ClientBroker { NotificationProvider.TELEGRAM ) ) { - // escape with backslash - // const escaped_title = title.replace(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/g, "\\$&"); - // const escaped_body = body.replace(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/g, "\\$&"); + TelegramNotify(title, body, icon, category, url, tts); + } - const token = Config.getInstance().cfg("telegram_token"); - const chatId = Config.getInstance().cfg("telegram_chat_id"); - - if (token && chatId) { - axios - .post(`https://api.telegram.org/bot${token}/sendMessage`, { - chat_id: chatId, - text: - `${title}\n` + - `${body}` + - `${url ? `\n\n${url}` : ""}`, - parse_mode: "HTML", - } as TelegramSendMessagePayload) - .then((res) => { - log( - LOGLEVEL.DEBUG, - "clientBroker.notify", - "Telegram 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", - `Telegram axios error response: ${err.message} (${err.response.data})`, - { err: err, response: err.response.data } - ); - console.error( - chalk.bgRed.whiteBright( - `Telegram axios error response : ${err.message} (${err.response.data})` - ), - JSON.stringify(err, null, 2) - ); - } else if (err.request) { - log( - LOGLEVEL.ERROR, - "clientBroker.notify", - `Telegram axios error request: ${err.message} (${err.request})`, - { err: err, request: err.request } - ); - console.error( - chalk.bgRed.whiteBright( - `Telegram axios error request: ${err.message} (${err.request})` - ), - JSON.stringify(err, null, 2) - ); - } else { - log( - LOGLEVEL.ERROR, - "clientBroker.notify", - `Telegram axios error: ${err.message}`, - err - ); - console.error( - chalk.bgRed.whiteBright( - `Telegram axios error: ${err.message}` - ), - JSON.stringify(err, null, 2) - ); - } - } else { - log( - LOGLEVEL.ERROR, - "clientBroker.notify", - `Telegram error: ${err.message}`, - err - ); - console.error( - chalk.bgRed.whiteBright( - `Telegram error: ${err.message}` - ) - ); - } - }); - } else if (!token && chatId) { - log( - LOGLEVEL.ERROR, - "clientBroker.notify", - "Telegram token not set" - ); - console.error( - chalk.bgRed.whiteBright("Telegram token not set") - ); - } else if (!chatId && token) { - log( - LOGLEVEL.ERROR, - "clientBroker.notify", - "Telegram chat ID not set" - ); - console.error( - chalk.bgRed.whiteBright("Telegram chat ID not set") - ); - } else { - log( - LOGLEVEL.ERROR, - "clientBroker.notify", - "Telegram token and chat ID not set" - ); - console.error( - chalk.bgRed.whiteBright( - "Telegram token and chat ID not set" - ) - ); - } + if ( + Config.getInstance().cfg("notify.ntfy.enabled") && + ClientBroker.getNotificationSettingForProvider( + category, + NotificationProvider.NTFY + ) + ) { + TelegramNotify(title, body, icon, category, url, tts); } if ( diff --git a/server/src/Core/Notifiers/Ntfy.ts b/server/src/Core/Notifiers/Ntfy.ts new file mode 100644 index 00000000..9928c3bb --- /dev/null +++ b/server/src/Core/Notifiers/Ntfy.ts @@ -0,0 +1,91 @@ +import type { NotificationCategory } from "@common/Defs"; +import axios from "axios"; +import chalk from "chalk"; +import { Config } from "../Config"; +import { LOGLEVEL, log } from "../Log"; + +export default function notify( + title: string, + body = "", + icon = "", + category: NotificationCategory, // change this? + url = "", + tts = false +) { + const ntfyUrl = Config.getInstance().cfg("notify.ntfy.url"); + + if (ntfyUrl) { + axios + .request({ + url: ntfyUrl, + headers: { + Title: title, + }, + data: body, + method: "POST", + }) + .then((res) => { + log( + LOGLEVEL.DEBUG, + "clientBroker.notify", + "Ntfy response", + res.data + ); + }) + .catch((err: Error) => { + if (axios.isAxiosError(err)) { + if (err.response) { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Ntfy axios error response: ${err.message} (${err.response.data})`, + { err: err, response: err.response.data } + ); + console.error( + chalk.bgRed.whiteBright( + `Ntfy axios error response : ${err.message} (${err.response.data})` + ), + JSON.stringify(err, null, 2) + ); + } else if (err.request) { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Ntfy axios error request: ${err.message} (${err.request})`, + { err: err, request: err.request } + ); + console.error( + chalk.bgRed.whiteBright( + `Ntfy axios error request : ${err.message} (${err.request})` + ), + JSON.stringify(err, null, 2) + ); + } else { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Ntfy axios error: ${err.message}`, + { err: err } + ); + console.error( + chalk.bgRed.whiteBright( + `Ntfy axios error : ${err.message}` + ), + JSON.stringify(err, null, 2) + ); + } + } else { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Ntfy error: ${err.message}`, + { err: err } + ); + console.error( + chalk.bgRed.whiteBright(`Ntfy error : ${err.message}`), + JSON.stringify(err, null, 2) + ); + } + }); + } +} diff --git a/server/src/Core/Notifiers/Telegram.ts b/server/src/Core/Notifiers/Telegram.ts new file mode 100644 index 00000000..c0c9b042 --- /dev/null +++ b/server/src/Core/Notifiers/Telegram.ts @@ -0,0 +1,129 @@ +import type { NotificationCategory } from "@common/Defs"; +import axios from "axios"; +import chalk from "chalk"; +import { Config } from "../Config"; +import { LOGLEVEL, log } from "../Log"; + +interface TelegramSendMessagePayload { + chat_id: number; + text: string; + parse_mode?: "MarkdownV2" | "Markdown" | "HTML"; + entities?: unknown; + disable_web_page_preview?: boolean; + disable_notification?: boolean; + protect_content?: boolean; + reply_to_message_id?: number; + allow_sending_without_reply?: boolean; + reply_markup?: unknown; +} + +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, "\\$&"); + + const token = Config.getInstance().cfg("telegram_token"); + const chatId = Config.getInstance().cfg("telegram_chat_id"); + + if (token && chatId) { + axios + .post(`https://api.telegram.org/bot${token}/sendMessage`, { + chat_id: chatId, + text: + `${title}\n` + + `${body}` + + `${url ? `\n\n${url}` : ""}`, + parse_mode: "HTML", + } as TelegramSendMessagePayload) + .then((res) => { + log( + LOGLEVEL.DEBUG, + "clientBroker.notify", + "Telegram 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", + `Telegram axios error response: ${err.message} (${err.response.data})`, + { err: err, response: err.response.data } + ); + console.error( + chalk.bgRed.whiteBright( + `Telegram axios error response : ${err.message} (${err.response.data})` + ), + JSON.stringify(err, null, 2) + ); + } else if (err.request) { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Telegram axios error request: ${err.message} (${err.request})`, + { err: err, request: err.request } + ); + console.error( + chalk.bgRed.whiteBright( + `Telegram axios error request: ${err.message} (${err.request})` + ), + JSON.stringify(err, null, 2) + ); + } else { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Telegram axios error: ${err.message}`, + err + ); + console.error( + chalk.bgRed.whiteBright( + `Telegram axios error: ${err.message}` + ), + JSON.stringify(err, null, 2) + ); + } + } else { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + `Telegram error: ${err.message}`, + err + ); + console.error( + chalk.bgRed.whiteBright( + `Telegram error: ${err.message}` + ) + ); + } + }); + } else if (!token && chatId) { + log(LOGLEVEL.ERROR, "clientBroker.notify", "Telegram token not set"); + console.error(chalk.bgRed.whiteBright("Telegram token not set")); + } else if (!chatId && token) { + log(LOGLEVEL.ERROR, "clientBroker.notify", "Telegram chat ID not set"); + console.error(chalk.bgRed.whiteBright("Telegram chat ID not set")); + } else { + log( + LOGLEVEL.ERROR, + "clientBroker.notify", + "Telegram token and chat ID not set" + ); + console.error( + chalk.bgRed.whiteBright("Telegram token and chat ID not set") + ); + } +}