diff --git a/README.md b/README.md index fab229e9..1e003d3e 100644 --- a/README.md +++ b/README.md @@ -375,6 +375,14 @@ Account browser proxies support `http://`, `https://`, `socks4://`, and `socks5: | `webhook.ntfy.title` | string | `"Microsoft-Rewards-Script"` | Notification title | `CONFIG_NTFY_TITLE` | | `webhook.ntfy.tags` | string[] | `["bot", "notify"]` | Notification tags | `CONFIG_NTFY_TAGS` \* | | `webhook.ntfy.priority` | number | `3` | Notification priority (1-5) | `CONFIG_NTFY_PRIORITY` | +| `webhook.bark.enabled` | boolean | `false` | Enable Bark notifications | `CONFIG_BARK_ENABLED` | +| `webhook.bark.url` | string | `"https://api.day.app"` | Bark server URL | `CONFIG_BARK_URL` | +| `webhook.bark.deviceKey` | string | `""` | Bark device key | `CONFIG_BARK_DEVICEKEY` | +| `webhook.bark.title` | string | `"Microsoft-Rewards-Script"` | Notification title | `CONFIG_BARK_TITLE` | +| `webhook.bark.group` | string | `"Microsoft-Rewards"` | Notification group | `CONFIG_BARK_GROUP` | +| `webhook.bark.icon` | string | `""` | Custom notification icon URL | `CONFIG_BARK_ICON` | +| `webhook.bark.sound` | string | `""` | Custom notification sound | `CONFIG_BARK_SOUND` | +| `webhook.bark.level` | string | `"active"` | Notification interruption level | `CONFIG_BARK_LEVEL` | | `webhook.webhookLogFilter.enabled` | boolean | `false` | Enable webhook log filtering | `CONFIG_WEBHOOK_LOG_FILTER_ENABLED` | | `webhook.webhookLogFilter.mode` | string | `"whitelist"` | Filter mode (whitelist/blacklist) | `CONFIG_WEBHOOK_LOG_FILTER_MODE` | | `webhook.webhookLogFilter.levels` | string[] | `["error"]` | Log levels to send | `CONFIG_WEBHOOK_LOG_FILTER_LEVELS` \* | @@ -385,7 +393,7 @@ Account browser proxies support `http://`, `https://`, `socks4://`, and `socks5: > \* Docker `CONFIG_*` array values are comma-separated strings e.g. `"error,warn"`. Regex patterns must be set directly in `config.json`. > [!WARNING] -> **NTFY** users set the `webhookLogFilter` to `enabled`, or you will receive push notifications for _all_ logs. +> **NTFY** and **Bark** users set the `webhookLogFilter` to `enabled`, or you will receive push notifications for _all_ logs. > When enabled, only account start, 2FA codes, and account completion summaries are delivered as push notifications. > Customize which notifications you receive with the `keywords` options. diff --git a/config.example.json b/config.example.json index 32c96499..7737e5d4 100644 --- a/config.example.json +++ b/config.example.json @@ -98,6 +98,16 @@ "tags": ["bot", "notify"], "priority": 3 }, + "bark": { + "enabled": false, + "url": "https://api.day.app", + "deviceKey": "", + "title": "Microsoft-Rewards-Script", + "group": "Microsoft-Rewards", + "icon": "", + "sound": "", + "level": "active" + }, "webhookLogFilter": { "enabled": false, "mode": "whitelist", diff --git a/scripts/api/configEditor.js b/scripts/api/configEditor.js index 444890a6..685a6898 100644 --- a/scripts/api/configEditor.js +++ b/scripts/api/configEditor.js @@ -177,7 +177,7 @@ function structuralValidate(cfg) { if ('webhookLogFilter' in cfg.webhook) { validateLogFilter(cfg.webhook.webhookLogFilter, 'webhook.webhookLogFilter', errors) } - for (const key of ['discord', 'ntfy', 'telegram']) { + for (const key of ['discord', 'ntfy', 'telegram', 'bark']) { if (key in cfg.webhook && !isObject(cfg.webhook[key])) { errors.push(`webhook.${key} must be an object`) } diff --git a/scripts/api/lib.js b/scripts/api/lib.js index 7dcb8101..d238d69f 100644 --- a/scripts/api/lib.js +++ b/scripts/api/lib.js @@ -123,6 +123,8 @@ export function redactSecrets(config) { if (wh.ntfy?.token) wh.ntfy.token = mask if (wh.telegram?.botToken) wh.telegram.botToken = mask if (wh.telegram?.chatId) wh.telegram.chatId = mask + if (wh.bark?.deviceKey) wh.bark.deviceKey = mask + if (wh.bark?.url) wh.bark.url = mask } return clone } diff --git a/src/index.ts b/src/index.ts index ae19c491..798b0bec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ import HttpClient from './util/Http' import { sendDiscord, flushDiscordQueue } from './logging/Discord' import { sendNtfy, flushNtfyQueue } from './logging/Ntfy' import { sendTelegram, flushTelegramQueue } from './logging/Telegram' +import { sendBark, flushBarkQueue } from './logging/Bark' import type { DashboardData } from './interface/DashboardData' import type { AppDashboardData } from './interface/AppDashBoardData' import type { AppEarnablePoints } from './interface/Points' @@ -69,7 +70,12 @@ export function getCurrentContext(): ExecutionContext { } async function flushAllWebhooks(timeoutMs = 5000): Promise { - await Promise.allSettled([flushDiscordQueue(timeoutMs), flushNtfyQueue(timeoutMs), flushTelegramQueue(timeoutMs)]) + await Promise.allSettled([ + flushDiscordQueue(timeoutMs), + flushNtfyQueue(timeoutMs), + flushTelegramQueue(timeoutMs), + flushBarkQueue(timeoutMs) + ]) closeSessionStore() } @@ -320,6 +326,9 @@ export class MicrosoftRewardsBot { if (webhook.telegram?.enabled && webhook.telegram.botToken && webhook.telegram.chatId) { sendTelegram(webhook.telegram, content, level) } + if (webhook.bark?.enabled && (webhook.bark.deviceKey || webhook.bark.url)) { + sendBark(webhook.bark, content, level) + } } }) } diff --git a/src/interface/Config.ts b/src/interface/Config.ts index d8261f32..28079e7b 100644 --- a/src/interface/Config.ts +++ b/src/interface/Config.ts @@ -79,6 +79,7 @@ export interface ConfigWebhook { discord?: WebhookDiscordConfig ntfy?: WebhookNtfyConfig telegram?: WebhookTelegramConfig + bark?: WebhookBarkConfig webhookLogFilter: LogFilter } @@ -110,3 +111,14 @@ export interface WebhookTelegramConfig { botToken: string chatId: string | number } + +export interface WebhookBarkConfig { + enabled?: boolean + url?: string + deviceKey?: string + title?: string + group?: string + icon?: string + sound?: string + level?: 'active' | 'timeSensitive' | 'passive' +} diff --git a/src/logging/Bark.ts b/src/logging/Bark.ts new file mode 100644 index 00000000..4659f91e --- /dev/null +++ b/src/logging/Bark.ts @@ -0,0 +1,68 @@ +import { httpRequest } from '../util/Http' +import type { HttpRequestConfig } from '../util/Http' +import PQueue from 'p-queue' +import type { WebhookBarkConfig } from '../interface/Config' +import type { LogLevel } from './Logger' +import { flushQueue } from './Queue' + +const barkQueue = new PQueue({ + interval: 1000, + intervalCap: 2, + carryoverConcurrencyCount: true +}) + +export async function sendBark(config: WebhookBarkConfig, content: string, level: LogLevel): Promise { + if (!config?.url && !config?.deviceKey) return + + const baseUrl = (config.url?.trim() || 'https://api.day.app').replace(/\/+$/, '') + let targetUrl: string + + const title = config.title || 'Microsoft-Rewards-Script' + + let interruptionLevel = config.level + if (!interruptionLevel) { + if (level === 'error') { + interruptionLevel = 'timeSensitive' + } else if (level === 'warn') { + interruptionLevel = 'active' + } + } + + const payload: Record = { + title, + body: content + } + + if (config.deviceKey) { + payload.device_key = config.deviceKey + targetUrl = baseUrl.endsWith('/push') ? baseUrl : `${baseUrl}/push` + } else { + targetUrl = baseUrl + } + + if (config.group) payload.group = config.group + if (config.icon) payload.icon = config.icon + if (config.sound) payload.sound = config.sound + if (interruptionLevel) payload.level = interruptionLevel + + const request: HttpRequestConfig = { + method: 'POST', + url: targetUrl, + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + data: payload, + timeout: 10000 + } + + await barkQueue.add(async () => { + try { + await httpRequest(request) + } catch (err) { + const status = (err as { response?: { status?: number } })?.response?.status + if (status === 429) return + } + }) +} + +export function flushBarkQueue(timeoutMs = 5000): Promise { + return flushQueue(barkQueue, timeoutMs) +} diff --git a/src/logging/Logger.ts b/src/logging/Logger.ts index 93900e1e..13b0d50b 100644 --- a/src/logging/Logger.ts +++ b/src/logging/Logger.ts @@ -3,6 +3,7 @@ import cluster from 'cluster' import { sendDiscord } from './Discord' import { sendNtfy } from './Ntfy' import { sendTelegram } from './Telegram' +import { sendBark } from './Bark' import type { MicrosoftRewardsBot } from '../index' import { errorDiagnostic } from '../util/ErrorDiagnostic' import type { LogFilter } from '../interface/Config' @@ -115,7 +116,8 @@ export class Logger { const hasWebhook = Boolean( (config.webhook.discord?.enabled && config.webhook.discord.url) || (config.webhook.ntfy?.enabled && config.webhook.ntfy.url) || - (config.webhook.telegram?.enabled && config.webhook.telegram.botToken && config.webhook.telegram.chatId) + (config.webhook.telegram?.enabled && config.webhook.telegram.botToken && config.webhook.telegram.chatId) || + (config.webhook.bark?.enabled && (config.webhook.bark.deviceKey || config.webhook.bark.url)) ) if (!hasWebhook) return @@ -133,6 +135,9 @@ export class Logger { ) { sendTelegram(config.webhook.telegram, cleanMsg, level) } + if (config.webhook.bark?.enabled && (config.webhook.bark.deviceKey || config.webhook.bark.url)) { + sendBark(config.webhook.bark, cleanMsg, level) + } } else { process.send?.({ __ipcLog: { content: cleanMsg, level } }) } diff --git a/src/util/ConfigEnvOverrides.ts b/src/util/ConfigEnvOverrides.ts index 10dc65e3..683405d0 100644 --- a/src/util/ConfigEnvOverrides.ts +++ b/src/util/ConfigEnvOverrides.ts @@ -88,6 +88,16 @@ export const ENV_OVERRIDES: EnvOverrideEntry[] = [ { env: 'CONFIG_NTFY_PRIORITY', path: 'webhook.ntfy.priority', type: 'number' }, { env: 'CONFIG_NTFY_TAGS', path: 'webhook.ntfy.tags', type: 'array' }, + // Bark webhook + { env: 'CONFIG_BARK_ENABLED', path: 'webhook.bark.enabled', type: 'bool' }, + { env: 'CONFIG_BARK_URL', path: 'webhook.bark.url', type: 'string' }, + { env: 'CONFIG_BARK_DEVICEKEY', path: 'webhook.bark.deviceKey', type: 'string' }, + { env: 'CONFIG_BARK_TITLE', path: 'webhook.bark.title', type: 'string' }, + { env: 'CONFIG_BARK_GROUP', path: 'webhook.bark.group', type: 'string' }, + { env: 'CONFIG_BARK_ICON', path: 'webhook.bark.icon', type: 'string' }, + { env: 'CONFIG_BARK_SOUND', path: 'webhook.bark.sound', type: 'string' }, + { env: 'CONFIG_BARK_LEVEL', path: 'webhook.bark.level', type: 'string' }, + // Webhook log filter { env: 'CONFIG_WEBHOOK_LOG_FILTER_ENABLED', path: 'webhook.webhookLogFilter.enabled', type: 'bool' }, { env: 'CONFIG_WEBHOOK_LOG_FILTER_MODE', path: 'webhook.webhookLogFilter.mode', type: 'string' }, diff --git a/src/util/Validator.ts b/src/util/Validator.ts index 907cc998..92c740ff 100644 --- a/src/util/Validator.ts +++ b/src/util/Validator.ts @@ -98,6 +98,18 @@ const WebhookSchema = z.object({ chatId: z.string() }) .optional(), + bark: z + .object({ + enabled: z.boolean().optional(), + url: z.string().optional(), + deviceKey: z.string().optional(), + title: z.string().optional(), + group: z.string().optional(), + icon: z.string().optional(), + sound: z.string().optional(), + level: z.enum(['active', 'timeSensitive', 'passive']).optional() + }) + .optional(), webhookLogFilter: LogFilterSchema })