conversion to type script
Some checks failed
Build & Push Docker (latest) / build (push) Has been cancelled
Build & Push Docker (latest) / verify (push) Has been cancelled

This commit is contained in:
Aiden Wilson
2026-05-29 23:55:31 +10:00
parent 30cd5c7b13
commit 959f6590c3
16 changed files with 695 additions and 44 deletions

View File

@@ -1,9 +1,27 @@
import { findProvider, loadProviders } from "./providers.js";
import { findProvider, loadProviders, type Provider } from "./providers.js";
const DEFAULT_TIMEOUT_MS = 8000;
const DEFAULT_MAX_WIDTH = 500;
function normalizeMaxWidth(endpoint, maxWidth) {
type HttpError = Error & { status?: number };
type FetchImpl = typeof fetch;
type FetchOembedOptions = {
fetchImpl?: FetchImpl;
providers?: Provider[];
timeoutMs?: number;
maxWidth?: number;
maxHeight?: number;
};
function httpError(message: string, status: number): HttpError {
const error: HttpError = new Error(message);
error.status = status;
return error;
}
function normalizeMaxWidth(_endpoint: string, maxWidth?: number) {
if (!maxWidth) {
return maxWidth;
}
@@ -11,7 +29,7 @@ function normalizeMaxWidth(endpoint, maxWidth) {
return Math.min(maxWidth, DEFAULT_MAX_WIDTH);
}
export function buildOembedUrl(endpoint, targetUrl, maxWidth, maxHeight) {
export function buildOembedUrl(endpoint: string, targetUrl: string, maxWidth?: number, maxHeight?: number) {
const requestUrl = new URL(endpoint);
const normalizedMaxWidth = normalizeMaxWidth(endpoint, maxWidth);
requestUrl.searchParams.set("url", targetUrl);
@@ -28,36 +46,30 @@ export function buildOembedUrl(endpoint, targetUrl, maxWidth, maxHeight) {
return requestUrl;
}
export async function fetchOembed(targetUrl, {
export async function fetchOembed(targetUrl: string, {
fetchImpl = fetch,
providers,
timeoutMs = Number(process.env.OEMBED_TIMEOUT_MS || DEFAULT_TIMEOUT_MS),
maxWidth,
maxHeight,
} = {}) {
}: FetchOembedOptions = {}) {
let parsed;
try {
parsed = new URL(targetUrl);
} catch {
const error = new Error("The url parameter must be an absolute URL.");
error.status = 400;
throw error;
throw httpError("The url parameter must be an absolute URL.", 400);
}
if (!["http:", "https:"].includes(parsed.protocol)) {
const error = new Error("Only http and https URLs are supported.");
error.status = 400;
throw error;
throw httpError("Only http and https URLs are supported.", 400);
}
const availableProviders = providers || await loadProviders({ fetchImpl });
const provider = findProvider(targetUrl, availableProviders);
if (!provider) {
const error = new Error("No oEmbed provider matched this URL.");
error.status = 404;
throw error;
throw httpError("No oEmbed provider matched this URL.", 404);
}
const controller = new AbortController();
@@ -73,9 +85,7 @@ export async function fetchOembed(targetUrl, {
});
if (!response.ok) {
const error = new Error(`oEmbed provider returned ${response.status}.`);
error.status = response.status;
throw error;
throw httpError(`oEmbed provider returned ${response.status}.`, response.status);
}
const data = await response.json();

View File

@@ -1,22 +1,39 @@
const DEFAULT_PROVIDERS_URL = "https://oembed.com/providers.json";
const DEFAULT_TTL_MS = 12 * 60 * 60 * 1000;
let providerCache = {
expiresAt: 0,
providers: [],
export type RawProvider = {
provider_name: string;
provider_url: string;
endpoints?: Array<{
schemes?: string[];
url: string;
}>;
};
export function wildcardToRegExp(pattern) {
export type Provider = {
providerName: string;
providerUrl: string;
endpoint: string;
scheme: string;
regex: RegExp;
};
let providerCache = {
expiresAt: 0,
providers: [] as Provider[],
};
export function wildcardToRegExp(pattern: string) {
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
return new RegExp(`^${escaped}$`, "i");
}
export function normalizeEndpoint(endpoint) {
export function normalizeEndpoint(endpoint: string) {
return endpoint.replace("{format}", "json");
}
export function flattenProviders(rawProviders) {
const providers = [];
export function flattenProviders(rawProviders: RawProvider[]) {
const providers: Provider[] = [];
for (const provider of rawProviders) {
for (const endpoint of provider.endpoints || []) {
@@ -37,7 +54,7 @@ export function flattenProviders(rawProviders) {
return providers;
}
export function findProvider(url, providers) {
export function findProvider(url: string, providers: Provider[]) {
return providers.find((provider) => provider.regex.test(url));
}
@@ -46,6 +63,11 @@ export async function loadProviders({
ttlMs = Number(process.env.PROVIDERS_TTL_MS || DEFAULT_TTL_MS),
fetchImpl = fetch,
force = false,
}: {
providersUrl?: string;
ttlMs?: number;
fetchImpl?: typeof fetch;
force?: boolean;
} = {}) {
const now = Date.now();
@@ -64,7 +86,7 @@ export async function loadProviders({
throw new Error(`Could not load providers: ${response.status} ${response.statusText}`);
}
const rawProviders = await response.json();
const rawProviders = await response.json() as RawProvider[];
const providers = flattenProviders(rawProviders);
providerCache = {

View File

@@ -1,4 +1,5 @@
import { createServer } from "node:http";
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { extname, join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -9,7 +10,8 @@ import { casparTemplatePage, collagePage, errorPage, graphicPage, homePage } fro
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const rootDir = join(__dirname, "..");
const publicDir = join(rootDir, "public");
const compiledPublicDir = join(rootDir, "dist", "public");
const publicDir = existsSync(compiledPublicDir) ? compiledPublicDir : join(rootDir, "public");
const port = Number(process.env.PORT || 3000);
const host = process.env.HOST || "0.0.0.0";