100 lines
2.3 KiB
TypeScript
100 lines
2.3 KiB
TypeScript
/** pi-ntfy HTTP transport. */
|
|
|
|
import type { ChannelResult } from "./types.ts";
|
|
|
|
export interface HttpResult {
|
|
ok: boolean;
|
|
status: number;
|
|
body: string;
|
|
error?: string;
|
|
ms: number;
|
|
}
|
|
|
|
export interface HttpRequestInit {
|
|
method?: string;
|
|
headers?: Record<string, string>;
|
|
body?: string;
|
|
}
|
|
|
|
const MAX_BODY_ECHO = 300;
|
|
|
|
export async function httpRequest(
|
|
url: string,
|
|
init: HttpRequestInit,
|
|
timeoutMs: number,
|
|
): Promise<HttpResult> {
|
|
const startedAt = Date.now();
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: init.method ?? "POST",
|
|
headers: init.headers,
|
|
body: init.body,
|
|
// Deliberately NOT ctx.signal — see file header.
|
|
signal: AbortSignal.timeout(timeoutMs),
|
|
});
|
|
const text = await response.text();
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
body: text.slice(0, MAX_BODY_ECHO),
|
|
error: response.ok ? undefined : `HTTP ${response.status}`,
|
|
ms: Date.now() - startedAt,
|
|
};
|
|
} catch (error) {
|
|
const name = error instanceof Error ? error.name : "";
|
|
const message =
|
|
name === "TimeoutError" || name === "AbortError"
|
|
? `timeout after ${timeoutMs}ms`
|
|
: error instanceof Error
|
|
? error.message
|
|
: String(error);
|
|
return {
|
|
ok: false,
|
|
status: 0,
|
|
body: "",
|
|
error: message,
|
|
ms: Date.now() - startedAt,
|
|
};
|
|
}
|
|
}
|
|
|
|
/** A local failure (bad config) shaped like an HttpResult. */
|
|
export function localFailure(message: string): HttpResult {
|
|
return { ok: false, status: 0, body: "", error: message, ms: 0 };
|
|
}
|
|
|
|
export function toChannelResult(
|
|
name: string,
|
|
result: HttpResult,
|
|
): ChannelResult {
|
|
return {
|
|
channel: name,
|
|
ok: result.ok,
|
|
status: result.status,
|
|
error: result.error,
|
|
ms: result.ms,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Group-bot endpoints answer HTTP 200 even when they reject the message
|
|
* (missing keyword, bad signature, rate limit). Fold that into `ok`.
|
|
*/
|
|
export function applyApiStatus(
|
|
result: HttpResult,
|
|
pick: (
|
|
payload: Record<string, unknown>,
|
|
) => { ok: boolean; message?: string } | undefined,
|
|
): HttpResult {
|
|
if (!result.ok || !result.body) return result;
|
|
let payload: Record<string, unknown>;
|
|
try {
|
|
payload = JSON.parse(result.body) as Record<string, unknown>;
|
|
} catch {
|
|
return result;
|
|
}
|
|
const verdict = pick(payload);
|
|
if (!verdict || verdict.ok) return result;
|
|
return { ...result, ok: false, error: verdict.message ?? "API 返回失败" };
|
|
}
|