Files
homarr/packages/request-handler/src/duckduckgo-bangs.ts
T

73 lines
2.0 KiB
TypeScript

import { z } from "zod";
import { env } from "@homarr/common/env";
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createRequestHandler } from "@homarr/request-handler/lib/request-handler";
export type DuckDuckGoBang = z.infer<typeof duckDuckGoBangSchema>;
const DUCKDUCKGO_BANGS_URL = "https://duckduckgo.com/bang.js";
/**
* DuckDuckGo `bang.js` uses short keys (to reduce the payload size):
* - `t`: bang token (e.g. `"yt"`)
* - `s`: display name
* - `d`: domain (optional)
* - `u`: URL template (contains `{{{s}}}`)
* - `c`: category (optional)
* - `sc`: subcategory (optional)
* - `r`: rank (optional)
*/
export const duckDuckGoBangSchema = z.object({
t: z.string(),
s: z.string(),
d: z.string().optional(),
u: z.string(),
c: z.string().optional(),
sc: z.string().optional(),
r: z.number().optional(),
});
const duckDuckGoBangsResponseSchema = z.array(duckDuckGoBangSchema);
const normalizeBangToken = (token: string) => token.toLowerCase().trim();
const compareBangTokenAsc = (left: DuckDuckGoBang, right: DuckDuckGoBang) => left.t.localeCompare(right.t);
export const duckDuckGoBangsRequestHandler = createRequestHandler({
async requestAsync(_) {
if (env.NO_EXTERNAL_CONNECTION) {
return [];
}
const res = await withTimeoutAsync(async (signal) => {
return await fetchWithTrustedCertificatesAsync(DUCKDUCKGO_BANGS_URL, {
signal,
headers: {
accept: "application/json,text/plain,*/*",
},
});
});
if (!res.ok) {
throw new ResponseError(res);
}
const json: unknown = await res.json();
const bangs = await duckDuckGoBangsResponseSchema.parseAsync(json);
const normalized = bangs
.map((bang) => ({
...bang,
t: normalizeBangToken(bang.t),
}))
.filter((bang) => bang.t.length > 0);
normalized.sort(compareBangTokenAsc);
return normalized;
},
});