1.0.37
This commit is contained in:
101 files changed
+5097
-642
No files matched your search
@@ -14,7 +14,7 @@
|
||||
*
|
||||
* Setup docs: https://github.com/caronc/apprise-api
|
||||
*/
|
||||
import { drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
import { notificationFetch, drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
|
||||
export async function sendApprise(appriseUrl: string, payload: NotificationPayload): Promise<NotificationResult> {
|
||||
const isSecure = appriseUrl.startsWith('apprises');
|
||||
@@ -65,7 +65,7 @@ export async function sendApprise(appriseUrl: string, payload: NotificationPaylo
|
||||
if (format) body.format = format; // text | markdown | html
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/notify/${encodeURIComponent(key)}`, {
|
||||
const response = await notificationFetch(`${baseUrl}/notify/${encodeURIComponent(key)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* ?group=, ?icon=, ?url=, ?badge=N, ?copy=, ?subtitle=,
|
||||
* ?volume=, ?ttl=, ?call=1, ?autoCopy=1, ?isArchive=1, ?action=none
|
||||
*/
|
||||
import { notificationFetch } from './shared';
|
||||
import type { NotificationPayload, NotificationResult } from './shared';
|
||||
|
||||
export async function sendBark(appriseUrl: string, payload: NotificationPayload): Promise<NotificationResult> {
|
||||
@@ -93,7 +94,7 @@ export async function sendBark(appriseUrl: string, payload: NotificationPayload)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/push`, {
|
||||
const response = await notificationFetch(`${baseUrl}/push`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
body: JSON.stringify(body)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Discord webhook notifications. discord:// or discords://. */
|
||||
import { drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
import { notificationFetch, drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
|
||||
export async function sendDiscord(appriseUrl: string, payload: NotificationPayload): Promise<NotificationResult> {
|
||||
// discord://webhook_id/webhook_token or discords://...
|
||||
@@ -7,7 +7,7 @@ export async function sendDiscord(appriseUrl: string, payload: NotificationPaylo
|
||||
const titleWithEnv = payload.environmentName ? `${payload.title} [${payload.environmentName}]` : payload.title;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await notificationFetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/** Generic JSON webhook. json:// or jsons:// (HTTPS). */
|
||||
import { drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
import { notificationFetch, drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
|
||||
export async function sendGenericWebhook(appriseUrl: string, payload: NotificationPayload): Promise<NotificationResult> {
|
||||
// json://hostname/path or jsons://hostname/path
|
||||
const url = appriseUrl.replace(/^jsons?:\/\//, appriseUrl.startsWith('jsons') ? 'https://' : 'http://');
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await notificationFetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Gotify. gotify:// or gotifys:// (HTTPS). */
|
||||
import { buildGotifyUrl } from '$lib/utils/notification-parsers';
|
||||
import { drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
import { notificationFetch, drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
|
||||
export async function sendGotify(appriseUrl: string, payload: NotificationPayload): Promise<NotificationResult> {
|
||||
const parsed = buildGotifyUrl(appriseUrl);
|
||||
@@ -12,7 +12,7 @@ export async function sendGotify(appriseUrl: string, payload: NotificationPayloa
|
||||
const defaultPriority = payload.type === 'error' ? 8 : payload.type === 'warning' ? 5 : 2;
|
||||
|
||||
try {
|
||||
const response = await fetch(parsed.url, {
|
||||
const response = await notificationFetch(parsed.url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Mattermost incoming webhook. mmost:// or mmosts:// (HTTPS). */
|
||||
import { drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
import { notificationFetch, drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
|
||||
export async function sendMattermost(appriseUrl: string, payload: NotificationPayload): Promise<NotificationResult> {
|
||||
// mmost://[botname@]hostname[:port][/path]/token or mmosts://...
|
||||
@@ -37,7 +37,7 @@ export async function sendMattermost(appriseUrl: string, payload: NotificationPa
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await notificationFetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** ntfy.sh + self-hosted ntfy. ntfy:// or ntfys:// (HTTPS). */
|
||||
import { drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
import { notificationFetch, drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
|
||||
// HTTP header values must be ByteString (every char ≤ 0xFF). Reject anything
|
||||
// else so we never blow up fetch() with a 65533 (U+FFFD) replacement char.
|
||||
@@ -35,7 +35,7 @@ export async function sendNtfy(appriseUrl: string, payload: NotificationPayload)
|
||||
// ntfy://user:pass@host/topic (custom server with basic auth)
|
||||
// ntfy://token@host/topic (custom server with bearer token)
|
||||
// ntfy://host/topic?auth=TOKEN (raw token, e.g. tk_..., or base64-encoded "Bearer <token>")
|
||||
// Query params: ?tags=ship,whale &title=Custom &priority=5
|
||||
// Query params: ?tags=ship,whale &title=Custom &priority=5 &email=me@example.com
|
||||
// ntfys:// variants for HTTPS
|
||||
const isSecure = appriseUrl.startsWith('ntfys');
|
||||
const path = appriseUrl.replace(/^ntfys?:\/\//, '');
|
||||
@@ -47,6 +47,7 @@ export async function sendNtfy(appriseUrl: string, payload: NotificationPayload)
|
||||
let queryTags: string | null = null;
|
||||
let queryTitle: string | null = null;
|
||||
let queryPriority: string | null = null;
|
||||
let queryEmail: string | null = null;
|
||||
let cleanPath = path;
|
||||
const qIndex = path.indexOf('?');
|
||||
if (qIndex !== -1) {
|
||||
@@ -55,6 +56,7 @@ export async function sendNtfy(appriseUrl: string, payload: NotificationPayload)
|
||||
queryTags = params.get('tags');
|
||||
queryTitle = params.get('title');
|
||||
queryPriority = params.get('priority');
|
||||
queryEmail = params.get('email');
|
||||
cleanPath = path.substring(0, qIndex);
|
||||
}
|
||||
|
||||
@@ -95,8 +97,13 @@ export async function sendNtfy(appriseUrl: string, payload: NotificationPayload)
|
||||
headers['Authorization'] = authHeader;
|
||||
}
|
||||
|
||||
// ?email=<address> → ntfy's Email header so ntfy forwards the message as email (#1231)
|
||||
if (queryEmail && isHeaderSafe(queryEmail)) {
|
||||
headers['Email'] = queryEmail;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await notificationFetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: payload.message
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Pushover. pushover://user_key/api_token. */
|
||||
import { drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
import { notificationFetch, drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
|
||||
export async function sendPushover(appriseUrl: string, payload: NotificationPayload): Promise<NotificationResult> {
|
||||
const match = appriseUrl.match(/^pushover:\/\/([^/]+)\/(.+)/);
|
||||
@@ -12,7 +12,7 @@ export async function sendPushover(appriseUrl: string, payload: NotificationPayl
|
||||
const titleWithEnv = payload.environmentName ? `${payload.title} [${payload.environmentName}]` : payload.title;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await notificationFetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -19,6 +19,25 @@ export interface NotificationResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeout for an outbound notification request, overridable via
|
||||
* NOTIFICATION_TIMEOUT_MS. Falls back to 10s if unset or invalid (a bad value
|
||||
* must not silently disable the timeout).
|
||||
*/
|
||||
const NOTIFICATION_TIMEOUT_MS = (() => {
|
||||
const parsed = Number(process.env.NOTIFICATION_TIMEOUT_MS);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 10_000;
|
||||
})();
|
||||
|
||||
/**
|
||||
* fetch() with a per-request timeout. Notification destinations are
|
||||
* user-configured (and a potential SSRF target); without a timeout a hung
|
||||
* endpoint pins the request indefinitely and blocks the rest of the dispatch.
|
||||
*/
|
||||
export function notificationFetch(input: string | URL, init: RequestInit = {}): Promise<Response> {
|
||||
return fetch(input, { ...init, signal: init.signal ?? AbortSignal.timeout(NOTIFICATION_TIMEOUT_MS) });
|
||||
}
|
||||
|
||||
/** Drain a response body to release the underlying socket/TLS connection. */
|
||||
export async function drainResponse(response: Response): Promise<void> {
|
||||
if (!response.bodyUsed) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* numbers (numeric, '+' gets added) or group IDs (signal-cli's "group.<base64>"
|
||||
* form, passed through untouched).
|
||||
*/
|
||||
import { drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
import { notificationFetch, drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
|
||||
export async function sendSignal(appriseUrl: string, payload: NotificationPayload): Promise<NotificationResult> {
|
||||
const isSecure = appriseUrl.startsWith('signals');
|
||||
@@ -49,7 +49,7 @@ export async function sendSignal(appriseUrl: string, payload: NotificationPayloa
|
||||
|
||||
const baseUrl = `${isSecure ? 'https' : 'http'}://${hostPort}`;
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/v2/send`, {
|
||||
const response = await notificationFetch(`${baseUrl}/v2/send`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Slack incoming webhook. slack:// or slacks:// or a raw hooks.slack.com URL. */
|
||||
import { drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
import { notificationFetch, drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
|
||||
export async function sendSlack(appriseUrl: string, payload: NotificationPayload): Promise<NotificationResult> {
|
||||
// slack://token_a/token_b/token_c or webhook URL
|
||||
@@ -14,7 +14,7 @@ export async function sendSlack(appriseUrl: string, payload: NotificationPayload
|
||||
const envTag = payload.environmentName ? ` \`${payload.environmentName}\`` : '';
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await notificationFetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Telegram bot. tgram://bot_token/chat_id[:topic_id]. */
|
||||
import { escapeTelegramMarkdown, parseTelegramUrl } from '$lib/utils/notification-parsers';
|
||||
import { drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
import { notificationFetch, drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
|
||||
export async function sendTelegram(appriseUrl: string, payload: NotificationPayload): Promise<NotificationResult> {
|
||||
const parsed = parseTelegramUrl(appriseUrl);
|
||||
@@ -16,7 +16,7 @@ export async function sendTelegram(appriseUrl: string, payload: NotificationPayl
|
||||
const envTag = payload.environmentName ? ` [${escapeTelegramMarkdown(payload.environmentName)}]` : '';
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await notificationFetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Microsoft Power Automate Workflows (e.g. Microsoft Teams). workflows://. */
|
||||
import { parseWorkflowsUrl, buildWorkflowsHttpUrl } from '$lib/utils/notification-parsers';
|
||||
import { drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
import { notificationFetch, drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
|
||||
export async function sendWorkflows(appriseUrl: string, payload: NotificationPayload): Promise<NotificationResult> {
|
||||
const parsed = parseWorkflowsUrl(appriseUrl);
|
||||
@@ -12,7 +12,7 @@ export async function sendWorkflows(appriseUrl: string, payload: NotificationPay
|
||||
const titleWithEnv = payload.environmentName ? `${payload.title} [${payload.environmentName}]` : payload.title;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await notificationFetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
Reference in new issue
Block a user