UNPKG

rsshub

Version:
1,379 lines 64.2 kB
import { n as Ze, t as rofetch } from "./ofetch-C3ts-Hud.mjs"; import { t as config } from "./config-CCmw1BNE.mjs"; import { t as logger } from "./logger-BKy98Y8B.mjs"; import { n as generateHeaders, r as generatedHeaders } from "./header-generator-DACPoxdp.mjs"; import { t as proxyExport } from "./proxy-Dbs7fWph.mjs"; import { a as requestMetric, c as gitHash, i as tracer, l as getDebugInfo, n as ensureAllLoaded, o as Layout, r as namespaces, s as gitDate, t as app$2, u as setDebugInfo } from "./registry-C29J4Iwi.mjs"; import { t as md5 } from "./md5-CpIHIxCO.mjs"; import { t as cache_default } from "./cache-BkqOokyU.mjs"; import { t as NotFoundError } from "./not-found-BvVU2U31.mjs"; import { t as RejectError } from "./reject-RVevp-dP.mjs"; import { t as RequestInProgressError } from "./request-in-progress-BjSkP8tJ.mjs"; import { a as time, n as getRouteNameFromPath, t as getPath } from "./helpers-5QIj7EjI.mjs"; import { n as convertDateToISO8601, t as collapseWhitespace } from "./common-utils-DtQojudb.mjs"; import x, { FormData, Headers, Request, Response } from "undici"; import dayjs from "dayjs"; import http from "node:http"; import https from "node:https"; import { RateLimiterMemory, RateLimiterQueue } from "rate-limiter-flexible"; import { Hono } from "hono"; import { compress } from "hono/compress"; import { jsxRenderer } from "hono/jsx-renderer"; import { trimTrailingSlash } from "hono/trailing-slash"; import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; import { Scalar } from "@scalar/hono-api-reference"; import { routePath } from "hono/route"; import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime"; import { parse } from "tldts"; import xxhash from "xxhash-wasm"; import Honeybadger from "@honeybadger-io/js"; import { load } from "cheerio"; import etagCalculate from "etag"; import * as entities from "entities"; import { convert } from "html-to-text"; import markdownit from "markdown-it"; import { RE2JS } from "re2js"; import sanitizeHtml from "sanitize-html"; import { simplecc } from "simplecc-wasm"; //#region lib/utils/request-rewriter/fetch.ts const limiterQueue = new RateLimiterQueue(new RateLimiterMemory({ points: 10, duration: 1, execEvenly: true }), { maxQueueSize: 4800 }); const useCustomHeader = (headers) => { process.env.NODE_ENV === "dev" && Ze((req) => { for (const [key, value] of headers.entries()) req.requestHeaders[key] = value; return req; }); }; const wrappedFetch = async (input, init) => { const request = new Request(input, init); const options = {}; logger.debug(`Outgoing request: ${request.method} ${request.url}`); if (config.isDefaultUA || init?.headerGeneratorOptions) { const generatedHeaders$2 = generateHeaders(init?.headerGeneratorOptions); if (!request.headers.get("user-agent")) request.headers.set("user-agent", generatedHeaders$2["user-agent"]); for (const header of generatedHeaders) { const headerValue = generatedHeaders$2[header]; if (!request.headers.has(header) && headerValue) request.headers.set(header, headerValue); } } else if (!request.headers.get("user-agent")) request.headers.set("user-agent", config.ua); if (!request.headers.get("referer")) try { const urlHandler = new URL(request.url); request.headers.set("referer", urlHandler.origin); } catch {} let isRetry = false; if (request.headers.get("x-prefer-proxy")) { isRetry = true; request.headers.delete("x-prefer-proxy"); } config.enableRemoteDebugging && useCustomHeader(request.headers); if (!init?.dispatcher && (proxyExport.proxyObj.strategy !== "on_retry" || isRetry)) { const proxyRegex = new RegExp(proxyExport.proxyObj.url_regex); let urlHandler; try { urlHandler = new URL(request.url); } catch {} if (proxyRegex.test(request.url) && request.url.startsWith("http") && !(urlHandler && urlHandler.host === proxyExport.proxyUrlHandler?.host)) { const currentProxy = proxyExport.getCurrentProxy(); if (currentProxy) { const dispatcher = proxyExport.getDispatcherForProxy(currentProxy); if (dispatcher) { options.dispatcher = dispatcher; logger.debug(`Proxying request via ${currentProxy.uri}: ${request.url}`); } } } } await limiterQueue.removeTokens(1); const maxRetries = proxyExport.multiProxy?.allProxies.length || 1; const attemptRequest = async (attempt) => { try { return await x.fetch(request, options); } catch (error) { if (options.dispatcher && proxyExport.multiProxy && attempt < maxRetries - 1) { const currentProxy = proxyExport.getCurrentProxy(); if (currentProxy) { logger.warn(`Request failed with proxy ${currentProxy.uri}, trying next proxy: ${error}`); proxyExport.markProxyFailed(currentProxy.uri); const nextProxy = proxyExport.getCurrentProxy(); if (nextProxy && nextProxy.uri !== currentProxy.uri) { const nextDispatcher = proxyExport.getDispatcherForProxy(nextProxy); if (nextDispatcher) options.dispatcher = nextDispatcher; logger.debug(`Retrying request with proxy ${nextProxy.uri}: ${request.url}`); return attemptRequest(attempt + 1); } logger.warn("No more proxies available, trying without proxy"); delete options.dispatcher; return attemptRequest(attempt + 1); } } throw error; } }; return attemptRequest(0); }; //#endregion //#region lib/utils/request-rewriter/get.ts const getWrappedGet = (origin) => function(...args) { let url; let options = {}; let callback; if (typeof args[0] === "string" || args[0] instanceof URL) { url = new URL(args[0]); if (typeof args[1] === "object") { options = args[1]; callback = args[2]; } else if (typeof args[1] === "function") { options = {}; callback = args[1]; } } else { options = args[0]; try { url = new URL(options.href || `${options.protocol || "http:"}//${options.hostname || options.host}${options.path}${options.search || (options.query ? `?${options.query}` : "")}`); } catch { url = null; } if (typeof args[1] === "function") callback = args[1]; } if (!url) return Reflect.apply(origin, this, args); logger.debug(`Outgoing request: ${options.method || "GET"} ${url}`); options.headers ||= {}; const headersLowerCaseKeys = new Set(Object.keys(options.headers).map((key) => key.toLowerCase())); if (config.isDefaultUA || options.headerGeneratorOptions) { const generatedHeaders$1 = generateHeaders(options.headerGeneratorOptions); if (!headersLowerCaseKeys.has("user-agent")) options.headers["user-agent"] = generatedHeaders$1["user-agent"]; for (const header of generatedHeaders) { const generatedHeader = generatedHeaders$1[header]; if (!headersLowerCaseKeys.has(header) && generatedHeader) options.headers[header] = generatedHeader; } } else if (!headersLowerCaseKeys.has("user-agent")) options.headers["user-agent"] = config.ua; if (!headersLowerCaseKeys.has("referer")) options.headers.referer = url.origin; if (!options.agent && proxyExport.agent) { if (new RegExp(proxyExport.proxyObj.url_regex).test(url.toString()) && url.protocol.startsWith("http") && url.host !== proxyExport.proxyUrlHandler?.host && url.host !== "localhost" && !url.host.startsWith("127.") && [config.playwrightWSEndpoint, config.playwrightCDPEndpoint].every((endpoint) => !endpoint?.includes(url.host))) options.agent = proxyExport.agent; } const { headerGeneratorOptions, ...cleanOptions } = options; return Reflect.apply(origin, this, [ url, cleanOptions, callback ]); }; //#endregion //#region lib/utils/request-rewriter/index.ts Object.defineProperties(globalThis, { fetch: { value: wrappedFetch, writable: true, configurable: true }, Headers: { value: Headers, writable: true, configurable: true }, FormData: { value: FormData, writable: true, configurable: true }, Request: { value: Request, writable: true, configurable: true }, Response: { value: Response, writable: true, configurable: true } }); http.get = getWrappedGet(http.get); http.request = getWrappedGet(http.request); https.get = getWrappedGet(https.get); https.request = getWrappedGet(https.request); //#endregion //#region lib/api/category/one.ts let cachedCategoryList; const getCategoryList = async () => { if (cachedCategoryList) return cachedCategoryList; await ensureAllLoaded(); const list = {}; for (const namespace in namespaces) for (const path in namespaces[namespace].routes) { if (!namespaces[namespace].routes[path].categories?.length) continue; const categories = namespaces[namespace].routes[path].categories; for (const category of categories) { if (!Object.hasOwn(list, category)) list[category] = {}; if (!Object.hasOwn(list[category], namespace)) list[category][namespace] = { ...namespaces[namespace], routes: {} }; list[category][namespace].routes[path] = namespaces[namespace].routes[path]; } } cachedCategoryList = list; return cachedCategoryList; }; const ParamsSchema = z.object({ category: z.string().openapi({ param: { name: "category", in: "path" }, example: "popular" }) }); const route$6 = createRoute({ method: "get", path: "/category/{category}", description: "Namespace list filtered by category", tags: ["Category"], request: { query: z.object({ categories: z.string().transform((val) => val.split(",")).optional(), lang: z.string().optional() }), params: ParamsSchema }, responses: { 200: { description: "Namespaces matching the requested category" } } }); const handler$6 = async (ctx) => { const categoryList = await getCategoryList(); const { categories, lang } = ctx.req.valid("query"); const { category } = ctx.req.valid("param"); let allCategories = [category]; if (categories && categories.length > 0) allCategories = [...allCategories, ...categories]; const commonNamespaces = Object.keys(categoryList[category] || {}).filter((namespace) => allCategories.every((cat) => categoryList[cat]?.[namespace])); let result = Object.fromEntries(commonNamespaces.map((namespace) => [namespace, categoryList[category][namespace]])); if (lang) result = Object.fromEntries(Object.entries(result).filter(([, value]) => value.lang === lang)); return ctx.json(result); }; //#endregion //#region lib/api/follow/config.ts const route$5 = createRoute({ method: "get", path: "/follow/config", description: "Follow configuration for the current instance", tags: ["Follow"], responses: { 200: { description: "Follow configuration for the current instance" } } }); const handler$5 = (ctx) => ctx.json({ ownerUserId: config.follow.ownerUserId, description: config.follow.description, price: config.follow.price, userLimit: config.follow.userLimit, cacheTime: config.cache.routeExpire, gitHash, gitDate: gitDate?.getTime() }); //#endregion //#region lib/api/namespace/all.ts const route$4 = createRoute({ method: "get", path: "/namespace", description: "Information about all namespaces", tags: ["Namespace"], responses: { 200: { description: "Namespace registry data for all namespaces" } } }); const handler$4 = async (ctx) => { await ensureAllLoaded(); return ctx.json(namespaces); }; //#endregion //#region lib/api/namespace/one.ts const pathParam = (name, example) => z.string().openapi({ param: { name, in: "path" }, example }); const nestedExample = Object.keys(namespaces).find((key) => key.includes("/"))?.split("/") ?? ["namespace", "sub"]; const route$3 = createRoute({ method: "get", path: "/namespace/{namespace}", description: "Information about a namespace", tags: ["Namespace"], request: { params: z.object({ namespace: pathParam("namespace", "github") }) }, responses: { 200: { description: "Namespace registry data for a namespace" } } }); const routeNested = createRoute({ method: "get", path: "/namespace/{namespace}/{sub}", description: `Information about a nested namespace (e.g. ${nestedExample.join("/")})`, tags: ["Namespace"], request: { params: z.object({ namespace: pathParam("namespace", nestedExample[0]), sub: pathParam("sub", nestedExample[1]) }) }, responses: { 200: { description: "Namespace registry data for a nested namespace" } } }); const handler$3 = async (ctx) => { await ensureAllLoaded(); const { namespace, sub } = ctx.req.valid("param"); return ctx.json(namespaces[[namespace, sub].filter(Boolean).join("/")]); }; //#endregion //#region lib/api/radar/rules/utils.ts let radar; const getRadarRules = async () => { if (radar) return radar; await ensureAllLoaded(); const rules = {}; for (const namespace in namespaces) for (const path in namespaces[namespace].routes) { const realPath = `/${namespace}${path}`; const data = namespaces[namespace].routes[path]; if (data.radar?.length) for (const radarItem of data.radar) { const parsedDomain = parse(new URL("https://" + radarItem.source[0]).hostname); const subdomain = parsedDomain.subdomain || "."; const domain = parsedDomain.domain; if (domain) { if (!Object.hasOwn(rules, domain)) rules[domain] = { _name: namespaces[namespace].name }; if (!Object.hasOwn(rules[domain], subdomain)) rules[domain][subdomain] = []; rules[domain][subdomain].push({ title: radarItem.title || data.name, docs: `https://docs.rsshub.app/routes/${data.categories?.[0] || "other"}`, source: radarItem.source.map((source) => { const sourceURL = new URL("https://" + source); return sourceURL.pathname + sourceURL.search + sourceURL.hash; }), target: radarItem.target ? `/${namespace}${radarItem.target}` : realPath }); } } } radar = rules; return radar; }; //#endregion //#region lib/api/radar/rules/all.ts const route$2 = createRoute({ method: "get", path: "/radar/rules", description: "All Radar rules grouped by domain", tags: ["Radar"], responses: { 200: { description: "Radar rules grouped by domain" } } }); const handler$2 = async (ctx) => { const rules = await getRadarRules(); return ctx.json(rules); }; //#endregion //#region lib/api/radar/rules/one.ts const route$1 = createRoute({ method: "get", path: "/radar/rules/{domain}", description: "Radar rules for a domain name", tags: ["Radar"], request: { params: z.object({ domain: z.string().openapi({ param: { name: "domain", in: "path" }, example: "github.com" }) }) }, responses: { 200: { description: "Radar rules for a domain name (no subdomains)" } } }); const handler$1 = async (ctx) => { const { domain } = ctx.req.valid("param"); const rules = await getRadarRules(); return ctx.json(rules[domain]); }; //#endregion //#region lib/api/route/status.ts const { h64ToString: h64ToString$1 } = await xxhash(); const QuerySchema = z.object({ requestPath: z.string().openapi({ param: { name: "requestPath", in: "query" }, example: "/github/comments/DIYgod/RSSHub/20768", description: "The route path to check cache status for" }) }); const ResponseSchema = z.object({ cached: z.boolean(), lastBuildDate: z.string().nullable() }); const route = createRoute({ method: "get", path: "/route/status", description: "Check if a route path is cached", tags: ["Route"], request: { query: QuerySchema }, responses: { 200: { content: { "application/json": { schema: ResponseSchema } }, description: "Cache found" }, 404: { content: { "application/json": { schema: ResponseSchema } }, description: "Cache not found" }, 503: { content: { "application/json": { schema: ResponseSchema } }, description: "Cache module unavailable" } } }); const handler = async (ctx) => { if (!cache_default.status.available) return ctx.json({ cached: false, lastBuildDate: null }, 503); const { requestPath } = ctx.req.valid("query"); const key = "rsshub:koa-redis-cache:" + h64ToString$1(requestPath + ":rss"); const cached = await cache_default.globalCache.has(key); if (!cached) return ctx.json({ cached: false, lastBuildDate: null }, 404); let lastBuildDate = null; try { const cachedData = await cache_default.globalCache.get(key); if (cachedData) lastBuildDate = JSON.parse(cachedData).lastBuildDate || null; } catch {} return ctx.json({ cached, lastBuildDate }, 200); }; //#endregion //#region lib/api/index.ts const app$1 = new OpenAPIHono(); app$1.openapi(route$4, handler$4); app$1.openapi(route$3, handler$3); app$1.openapi(routeNested, handler$3); app$1.openapi(route$2, handler$2); app$1.openapi(route$1, handler$1); app$1.openapi(route$6, handler$6); app$1.openapi(route, handler); app$1.openapi(route$5, handler$5); const docs = app$1.getOpenAPI31Document({ openapi: "3.1.0", info: { version: "0.0.1", title: "RSSHub API" } }); for (const path in docs.paths) { docs.paths[`/api${path}`] = docs.paths[path]; delete docs.paths[path]; } app$1.get("/openapi.json", (ctx) => ctx.json(docs)); app$1.get("/reference", Scalar({ content: docs, hiddenClients: { c: true, clojure: true, csharp: true, dart: true, fsharp: true, go: false, http: true, java: true, js: true, kotlin: true, node: ["axios"], objc: true, ocaml: true, php: false, powershell: true, python: false, r: true, ruby: true, rust: true, shell: ["httpie", "wget"], swift: true } })); //#endregion //#region lib/views/error.tsx const Index = ({ requestPath, message, errorRoute, nodeVersion }) => /* @__PURE__ */ jsxs(Layout, { children: [ /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute w-full min-h-screen dark:invert", style: { backgroundImage: `url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAzMiAzMicgd2lkdGg9JzMyJyBoZWlnaHQ9JzMyJyBmaWxsPSdub25lJyBzdHJva2U9J3JnYigxNSAyMyA0MiAvIDAuMDQpJz48cGF0aCBkPSdNMCAuNUgzMS41VjMyJy8+PC9zdmc+')`, maskImage: "linear-gradient(transparent, black, transparent)" } }), /* @__PURE__ */ jsxs("div", { className: "w-full grow shrink-0 py-8 flex items-center justify-center flex-col space-y-4", children: [ /* @__PURE__ */ jsx("img", { className: "grayscale", src: "/logo.png", alt: "RSSHub", width: "100", loading: "lazy" }), /* @__PURE__ */ jsx("h1", { className: "text-4xl font-bold", children: "Looks like something went wrong" }), /* @__PURE__ */ jsxs("div", { className: "text-left w-[800px] space-y-6 !mt-10", children: [ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [ /* @__PURE__ */ jsx("p", { className: "mb-2 font-bold", children: "Helpful Information" }), /* @__PURE__ */ jsxs("p", { className: "message", children: [ "Error Message:", /* @__PURE__ */ jsx("br", {}), /* @__PURE__ */ jsx("code", { className: "mt-2 block max-h-28 overflow-auto bg-zinc-100 dark:bg-zinc-800 align-bottom w-fit details whitespace-pre-line", children: message }) ] }), /* @__PURE__ */ jsxs("p", { className: "message", children: ["Route: ", /* @__PURE__ */ jsx("code", { className: "ml-2 bg-zinc-100 dark:bg-zinc-800", children: errorRoute })] }), /* @__PURE__ */ jsxs("p", { className: "message", children: ["Full Route: ", /* @__PURE__ */ jsx("code", { className: "ml-2 bg-zinc-100 dark:bg-zinc-800", children: requestPath })] }), /* @__PURE__ */ jsxs("p", { className: "message", children: ["Node Version: ", /* @__PURE__ */ jsx("code", { className: "ml-2 bg-zinc-100 dark:bg-zinc-800", children: nodeVersion })] }), /* @__PURE__ */ jsxs("p", { className: "message", children: ["Git Hash: ", /* @__PURE__ */ jsx("code", { className: "ml-2 bg-zinc-100 dark:bg-zinc-800", children: gitHash })] }), /* @__PURE__ */ jsxs("p", { className: "message", children: ["Git Date: ", /* @__PURE__ */ jsx("code", { className: "ml-2 bg-zinc-100 dark:bg-zinc-800", children: gitDate?.toUTCString() })] }) ] }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("p", { className: "mb-2 font-bold", children: "Report" }), /* @__PURE__ */ jsxs("p", { children: [ "After carefully reading the", " ", /* @__PURE__ */ jsx("a", { className: "text-[#F5712C]", href: "https://docs.rsshub.app/", target: "_blank", children: "document" }), ", if you think this is a bug of RSSHub, please", " ", /* @__PURE__ */ jsx("a", { className: "text-[#F5712C]", href: "https://github.com/DIYgod/RSSHub/issues/new?assignees=&labels=RSS+bug&template=bug_report_en.yml", target: "_blank", children: "submit an issue" }), " ", "on GitHub." ] }), /* @__PURE__ */ jsxs("p", { children: [ "在仔细阅读", /* @__PURE__ */ jsx("a", { className: "text-[#F5712C]", href: "https://docs.rsshub.app/zh/", target: "_blank", children: "文档" }), "后,如果你认为这是 RSSHub 的 bug,请在 GitHub", " ", /* @__PURE__ */ jsx("a", { className: "text-[#F5712C]", href: "https://github.com/DIYgod/RSSHub/issues/new?assignees=&labels=RSS+bug&template=bug_report_zh.yml", target: "_blank", children: "提交 issue" }), "。" ] }) ] }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("p", { className: "mb-2 font-bold", children: "Community" }), /* @__PURE__ */ jsxs("p", { children: [ "You can also join our", " ", /* @__PURE__ */ jsx("a", { className: "text-[#F5712C]", target: "_blank", href: "https://t.me/rsshub", children: "Telegram group" }), ", or follow our", " ", /* @__PURE__ */ jsx("a", { className: "text-[#F5712C]", target: "_blank", href: "https://t.me/awesomeRSSHub", children: "Telegram channel" }), " ", "and", " ", /* @__PURE__ */ jsx("a", { target: "_blank", href: "https://x.com/intent/follow?screen_name=_RSSHub", className: "text-[#F5712C]", children: "Twitter" }), " ", "to get community support and news." ] }), /* @__PURE__ */ jsxs("p", { children: [ "你也可以加入我们的", " ", /* @__PURE__ */ jsx("a", { className: "text-[#F5712C]", target: "_blank", href: "https://t.me/rsshub", children: "Telegram 群组" }), ",或关注我们的", " ", /* @__PURE__ */ jsx("a", { className: "text-[#F5712C]", target: "_blank", href: "https://t.me/awesomeRSSHub", children: "Telegram 频道" }), "和", " ", /* @__PURE__ */ jsx("a", { target: "_blank", href: "https://x.com/intent/follow?screen_name=_RSSHub", className: "text-[#F5712C]", children: "Twitter" }), " ", "获取社区支持和新闻。" ] }) ] }) ] }) ] }), /* @__PURE__ */ jsxs("div", { className: "mt-4 pb-8 text-center w-full text-sm font-medium space-y-2", children: [ /* @__PURE__ */ jsxs("p", { className: "space-x-4", children: [ /* @__PURE__ */ jsx("a", { target: "_blank", href: "https://github.com/DIYgod/RSSHub", children: /* @__PURE__ */ jsxs("picture", { children: [/* @__PURE__ */ jsx("source", { srcset: "https://icons.ly/github/_/fff", media: "(prefers-color-scheme: dark)" }), /* @__PURE__ */ jsx("img", { className: "inline", src: "https://icons.ly/github", alt: "github", width: "20", height: "20" })] }) }), /* @__PURE__ */ jsx("a", { target: "_blank", href: "https://t.me/rsshub", children: /* @__PURE__ */ jsx("img", { className: "inline", src: "https://icons.ly/telegram", alt: "telegram group", width: "20", height: "20" }) }), /* @__PURE__ */ jsx("a", { target: "_blank", href: "https://t.me/awesomeRSSHub", children: /* @__PURE__ */ jsx("img", { className: "inline", src: "https://icons.ly/telegram", alt: "telegram channel", width: "20", height: "20" }) }), /* @__PURE__ */ jsx("a", { target: "_blank", href: "https://x.com/intent/follow?screen_name=_RSSHub", className: "text-[#F5712C]", children: /* @__PURE__ */ jsxs("picture", { children: [/* @__PURE__ */ jsx("source", { srcset: "https://icons.ly/x/_/fff", media: "(prefers-color-scheme: dark)" }), /* @__PURE__ */ jsx("img", { className: "inline", src: "https://icons.ly/x", alt: "X", width: "20", height: "20" })] }) }) ] }), /* @__PURE__ */ jsxs("p", { className: "!mt-6", children: [ "Please consider", " ", /* @__PURE__ */ jsx("a", { target: "_blank", href: "https://docs.rsshub.app/sponsor", className: "text-[#F5712C]", children: "sponsoring" }), " ", "to help keep this open source project alive." ] }), /* @__PURE__ */ jsxs("p", { children: [ "Made with ❤️ by", " ", /* @__PURE__ */ jsx("a", { target: "_blank", href: "https://diygod.cc", className: "text-[#F5712C]", children: "DIYgod" }), " ", "and", " ", /* @__PURE__ */ jsx("a", { target: "_blank", href: "https://github.com/DIYgod/RSSHub/graphs/contributors", className: "text-[#F5712C]", children: "Contributors" }), " ", "under AGPL-3.0 License." ] }) ] }) ] }); //#endregion //#region lib/errors/index.tsx const Sentry$1 = config.sentry.dsn ? await import("@sentry/node") : void 0; const errorHandler = (error, ctx) => { const requestPath = ctx.req.path; const matchedRoute = routePath(ctx); const hasMatchedRoute = matchedRoute !== "/*"; const debug = getDebugInfo(); try { if (ctx.res.headers.get("RSSHub-Cache-Status")) debug.hitCache++; } catch {} debug.error++; if (!debug.errorPaths[requestPath]) debug.errorPaths[requestPath] = 0; debug.errorPaths[requestPath]++; if (!debug.errorRoutes[matchedRoute] && hasMatchedRoute) debug.errorRoutes[matchedRoute] = 0; hasMatchedRoute && debug.errorRoutes[matchedRoute]++; setDebugInfo(debug); if (config.honeybadger.apiKey) Honeybadger.notify(error, { context: { name: requestPath.split("/", 2)[1] } }); if (Sentry$1) Sentry$1.withScope((scope) => { scope.setTag("name", requestPath.split("/", 2)[1]); Sentry$1.captureException(error); }); let errorMessage = (process.env.NODE_ENV || process.env.VERCEL_ENV) === "production" || !error.stack ? `${error.name}: ${error.message}` : error.stack; switch (error.name) { case "HTTPError": case "RequestError": case "FetchError": ctx.status(503); break; case "RequestInProgressError": ctx.header("Cache-Control", `public, max-age=${config.requestTimeout / 1e3}`); ctx.status(503); break; case "RejectError": ctx.status(403); break; case "NotFoundError": ctx.status(404); errorMessage += "The route does not exist or has been deleted."; break; default: ctx.status(503); break; } logger.error(`Error in ${requestPath}: ${errorMessage}`); requestMetric.error({ path: matchedRoute, method: ctx.req.method, status: ctx.res.status }); return config.isPackage || ctx.req.query("format") === "json" ? ctx.json({ error: { message: error.message ?? error } }) : ctx.html(/* @__PURE__ */ jsx(Index, { requestPath, message: errorMessage, errorRoute: hasMatchedRoute ? matchedRoute : requestPath, nodeVersion: process.version })); }; const notFoundHandler = (ctx) => errorHandler(new NotFoundError(), ctx); //#endregion //#region lib/middleware/access-control.ts const reject = (requestPath) => { throw new RejectError(`Authentication failed. Access denied.\n${requestPath}`); }; const middleware$10 = async (ctx, next) => { const requestPath = new URL(ctx.req.url).pathname; const accessKey = ctx.req.query("key"); const accessCode = ctx.req.query("code"); if ([ "/", "/robots.txt", "/favicon.ico", "/logo.png" ].includes(requestPath)) await next(); else { if (config.accessKey && !(config.accessKey === accessKey || accessCode === md5(requestPath + config.accessKey))) return reject(requestPath); await next(); } }; //#endregion //#region lib/middleware/anti-hotlink.ts const templateRegex = /\$\{([^{}]+)\}/g; const allowedUrlProperties = /* @__PURE__ */ new Set([ "hash", "host", "hostname", "href", "origin", "password", "pathname", "port", "protocol", "search", "searchParams", "username" ]); const matchPath = (path, paths) => { for (const p of paths) if (path.startsWith(p) && (path.length === p.length || path[p.length] === "/")) return true; return false; }; const filterPath = (path) => { const include = config.hotlink.includePaths; const exclude = config.hotlink.excludePaths; return !(include && !matchPath(path, include)) && !(exclude && matchPath(path, exclude)); }; const interpolate = (str, obj) => str.replaceAll(templateRegex, (_, prop) => { let needEncode = false; if (prop.endsWith("_ue")) { prop = prop.slice(0, -3); needEncode = true; } return needEncode ? encodeURIComponent(obj[prop]) : obj[prop]; }); const parseUrl = (str) => { let url; try { url = new URL(str); } catch { logger.error(`Failed to parse ${str}`); } return url; }; const replaceUrl = (template, url) => { if (!template || !url) return url; const oldUrl = parseUrl(url); if (oldUrl && oldUrl.protocol !== "data:") return interpolate(template, oldUrl); return url; }; const replaceUrls = ($, selector, template, attribute = "src") => { $(selector).each((_, el) => { const oldSrc = $(el).attr(attribute); if (oldSrc) { const url = parseUrl(oldSrc); if (url && url.protocol !== "data:") $(el).attr(attribute, interpolate(template, url)); } }); }; const process$1 = (html, image_hotlink_template, multimedia_hotlink_template) => { const $ = load(html, void 0, false); if (image_hotlink_template) { replaceUrls($, "img, picture > source", image_hotlink_template); replaceUrls($, "video[poster]", image_hotlink_template, "poster"); replaceUrls($, "*[data-rsshub-image=\"href\"]", image_hotlink_template, "href"); } if (multimedia_hotlink_template) { replaceUrls($, "video, video > source, audio, audio > source", multimedia_hotlink_template); if (!image_hotlink_template) replaceUrls($, "video[poster]", multimedia_hotlink_template, "poster"); } return $.html(); }; const validateTemplate = (template) => { if (!template) return; for (const match of template.matchAll(templateRegex)) { const prop = match[1].endsWith("_ue") ? match[1].slice(0, -3) : match[1]; if (!allowedUrlProperties.has(prop)) throw new Error(`Invalid URL property: ${prop}`); } }; const middleware$9 = async (ctx, next) => { await next(); let imageHotlinkTemplate; let multimediaHotlinkTemplate; if (config.feature.allow_user_hotlink_template) { multimediaHotlinkTemplate = ctx.req.query("multimedia_hotlink_template"); imageHotlinkTemplate = ctx.req.query("image_hotlink_template"); } if (config.hotlink.template) { imageHotlinkTemplate = filterPath(ctx.req.path) ? config.hotlink.template : void 0; multimediaHotlinkTemplate = filterPath(ctx.req.path) ? config.hotlink.template : void 0; } if (!imageHotlinkTemplate && !multimediaHotlinkTemplate) return; validateTemplate(imageHotlinkTemplate); validateTemplate(multimediaHotlinkTemplate); const data = ctx.get("data"); if (data) { if (data.image) data.image = replaceUrl(imageHotlinkTemplate, data.image); if (data.description) data.description = process$1(data.description, imageHotlinkTemplate, multimediaHotlinkTemplate); if (data.item) for (const item of data.item) { if (item.description) item.description = process$1(item.description, imageHotlinkTemplate, multimediaHotlinkTemplate); if (item.enclosure_url && item.enclosure_type) { if (item.enclosure_type.startsWith("image/")) item.enclosure_url = replaceUrl(imageHotlinkTemplate, item.enclosure_url); else if (/^(?:video|audio)\//.test(item.enclosure_type)) item.enclosure_url = replaceUrl(multimediaHotlinkTemplate, item.enclosure_url); } if (item.image) item.image = replaceUrl(imageHotlinkTemplate, item.image); if (item.itunes_item_image) item.itunes_item_image = replaceUrl(imageHotlinkTemplate, item.itunes_item_image); } ctx.set("data", data); } }; //#endregion //#region lib/middleware/cache.ts const bypassList = /* @__PURE__ */ new Set([ "/", "/robots.txt", "/logo.png", "/favicon.ico" ]); const { h64ToString } = await xxhash(); const middleware$8 = async (ctx, next) => { if (!cache_default.status.available || bypassList.has(ctx.req.path)) { await next(); return; } const requestPath = ctx.req.path; const format = `:${ctx.req.query("format") || config.format}`; const limit = ctx.req.query("limit") ? `:${ctx.req.query("limit")}` : ""; const key = "rsshub:koa-redis-cache:" + h64ToString(requestPath + format + limit); const controlKey = "rsshub:path-requested:" + h64ToString(requestPath + format + limit); let value = await cache_default.globalCache.get(key); let isRequesting = false; if (!value) isRequesting = !await cache_default.globalCache.claim(controlKey, config.cache.requestTimeout); if (isRequesting) { let retryTimes = process.env.NODE_ENV === "test" ? 1 : 10; let bypass = false; while (retryTimes > 0) { await new Promise((resolve) => setTimeout(resolve, process.env.NODE_ENV === "test" ? 3e3 : 6e3)); if (await cache_default.globalCache.get(controlKey) !== "1") { bypass = true; break; } retryTimes--; } if (!bypass) throw new RequestInProgressError("This path is currently fetching, please come back later!"); value = await cache_default.globalCache.get(key); } if (value) { ctx.status(200); ctx.header("RSSHub-Cache-Status", "HIT"); ctx.set("data", JSON.parse(value)); await next(); return; } if (isRequesting) await cache_default.globalCache.set(controlKey, "1", config.cache.requestTimeout); ctx.set("cacheKey", key); ctx.set("cacheControlKey", controlKey); try { await next(); } catch (error) { await cache_default.globalCache.set(controlKey, "0", config.cache.requestTimeout); throw error; } const data = ctx.get("data"); if (ctx.res.headers.get("Cache-Control") !== "no-cache" && data) { data.lastBuildDate = (/* @__PURE__ */ new Date()).toUTCString(); ctx.set("data", data); const body = JSON.stringify(data); await cache_default.globalCache.set(key, body, config.cache.routeExpire); } await cache_default.globalCache.set(controlKey, "0", config.cache.requestTimeout); }; //#endregion //#region lib/middleware/debug.ts const middleware$7 = async (ctx, next) => { { const debug = getDebugInfo(); if (!debug.paths[ctx.req.path]) debug.paths[ctx.req.path] = 0; debug.paths[ctx.req.path]++; debug.request++; setDebugInfo(debug); } await next(); { const debug = getDebugInfo(); const rPath = routePath(ctx); const hasMatchedRoute = rPath !== "/*"; if (!debug.routes[rPath] && hasMatchedRoute) debug.routes[rPath] = 0; hasMatchedRoute && debug.routes[rPath]++; if (ctx.res.headers.get("RSSHub-Cache-Status")) debug.hitCache++; if (ctx.res.status === 304) debug.etag++; setDebugInfo(debug); } }; //#endregion //#region lib/middleware/header.ts const headers = { "Access-Control-Allow-Methods": "GET", "Content-Type": "application/xml; charset=utf-8", "Cache-Control": `public, max-age=${config.cache.routeExpire}`, "X-Content-Type-Options": "nosniff" }; if (config.nodeName) headers["RSSHub-Node"] = config.nodeName; function etagMatches(etag, ifNoneMatch) { return ifNoneMatch !== null && ifNoneMatch.split(/,\s*/).includes(etag); } const middleware$6 = async (ctx, next) => { for (const key in headers) ctx.header(key, headers[key]); ctx.header("Access-Control-Allow-Origin", config.allowOrigin || new URL(ctx.req.url).host); await next(); const rPath = routePath(ctx); if (rPath !== "/*") ctx.header("X-RSSHub-Route", rPath); const data = ctx.get("data"); if (!data || ctx.res.headers.get("ETag")) return; const { lastBuildDate, ...etagData } = data; const etag = etagCalculate(JSON.stringify(etagData)); ctx.header("ETag", etag); if (etagMatches(etag, ctx.req.header("If-None-Match") ?? null)) { ctx.status(304); ctx.set("no-content", true); } else ctx.header("Last-Modified", lastBuildDate); }; //#endregion //#region lib/middleware/honeybadger.ts if (config.honeybadger.apiKey) { Honeybadger.configure({ apiKey: config.honeybadger.apiKey, enableUncaught: false }); Honeybadger.setContext({ node_name: config.nodeName }); logger.info("Honeybadger inited."); } const middleware$5 = async (ctx, next) => { const time = Date.now(); await next(); if (config.honeybadger.apiKey && Date.now() - time >= config.errorTrackingRouteTimeout) Honeybadger.notify(/* @__PURE__ */ new Error("Route Timeout"), { context: { name: getRouteNameFromPath(ctx.req.path) } }); }; //#endregion //#region lib/middleware/logger.ts const colorStatus = (status) => { return { 7: `\u{1B}[35m${status}\u{1B}[0m`, 5: `\u{1B}[31m${status}\u{1B}[0m`, 4: `\u{1B}[33m${status}\u{1B}[0m`, 3: `\u{1B}[36m${status}\u{1B}[0m`, 2: `\u{1B}[32m${status}\u{1B}[0m`, 1: `\u{1B}[32m${status}\u{1B}[0m`, 0: `\u{1B}[33m${status}\u{1B}[0m` }[Math.trunc(status / 100)]; }; const middleware$4 = async (ctx, next) => { const { method, raw, routePath } = ctx.req; const path = getPath(raw); logger.info(`<-- ${method} ${path}`); const start = Date.now(); await next(); const status = ctx.res.status; logger.info(`--> ${method} ${path} ${colorStatus(status)} ${time(start)}`); requestMetric.success(Date.now() - start, { path: routePath, method, status }); }; //#endregion //#region lib/middleware/parameter.ts const md = markdownit({ html: true }); const resolveRelativeLink = ($, elem, attr, baseUrl) => { const $elem = $(elem); if (baseUrl) try { const oldAttr = $elem.attr(attr); if (oldAttr) $elem.attr(attr, new URL(oldAttr, baseUrl).href); } catch {} }; const getAiCompletion = async (prompt, text) => { return (await rofetch(`${config.openai.endpoint}/chat/completions`, { method: "POST", body: { model: config.openai.model, max_tokens: config.openai.maxTokens, messages: [{ role: "system", content: prompt }, { role: "user", content: text }], temperature: config.openai.temperature }, headers: { Authorization: `Bearer ${config.openai.apiKey}` } })).choices[0].message.content; }; const getAuthorString = (item) => { let author = ""; if (item.author) author = typeof item.author === "string" ? item.author : item.author.map((i) => i.name).join(" "); return author; }; const middleware$3 = async (ctx, next) => { await next(); const data = ctx.get("data"); if (data) { if ((!data.item || data.item.length === 0) && !data.allowEmpty) throw new Error("this route is empty, please check the original site or <a href=\"https://github.com/DIYgod/RSSHub/issues/new/choose\">create an issue</a>"); data.item ||= []; data.title &&= entities.decodeXML(data.title + ""); data.description &&= entities.decodeXML(data.description + ""); if (ctx.req.query("sorted") !== "false") data.item = data.item.toSorted((a, b) => +new Date(b.pubDate || 0) - +new Date(a.pubDate || 0)); const handleItem = (item) => { item.title &&= entities.decodeXML(item.title + ""); item.description ||= item.content?.html; if (item.pubDate) item.pubDate = new Date(item.pubDate).toUTCString(); if (item.link) { let baseUrl = data.link; if (baseUrl && !/^https?:\/\//.test(baseUrl)) baseUrl = baseUrl.startsWith("//") ? "http:" + baseUrl : "http://" + baseUrl; item.link = new URL(item.link, baseUrl).href; } if (item.description) { const $ = load(item.description); let baseUrl = item.link || data.link; if (baseUrl && !/^https?:\/\//.test(baseUrl)) baseUrl = baseUrl.startsWith("//") ? "http:" + baseUrl : "http://" + baseUrl; $("script").remove(); $("img").each((_, ele) => { const $ele = $(ele); if (!$ele.attr("src")) { const lazySrc = $ele.attr("data-src") || $ele.attr("data-original"); if (lazySrc) $ele.attr("src", lazySrc); else for (const key in ele.attribs) { const value = ele.attribs[key].trim(); if ([ ".gif", ".png", ".jpg", ".webp" ].some((suffix) => value.includes(suffix))) { $ele.attr("src", value); break; } } } for (const e of [ "onclick", "onerror", "onload" ]) $ele.removeAttr(e); }); $("a, area").each((_, elem) => { resolveRelativeLink($, elem, "href", baseUrl); }); $("img, video, audio, source, iframe, embed, track").each((_, elem) => { resolveRelativeLink($, elem, "src", baseUrl); }); $("video[poster]").each((_, elem) => { resolveRelativeLink($, elem, "poster", baseUrl); }); $("img, iframe").each((_, elem) => { if (!$(elem).attr("referrerpolicy")) $(elem).attr("referrerpolicy", "no-referrer"); }); item.description = $("body").html() + "" + (config.suffix || ""); if (item._extra?.links && $(".rsshub-quote").length) item._extra?.links?.map((e) => { e.content_html = $.html($(".rsshub-quote")); return e; }); } if (item.category) { Array.isArray(item.category) || (item.category = [item.category]); item.category = item.category.filter((e) => typeof e === "string"); } return item; }; data.item = await Promise.all(data.item.map((itm) => handleItem(itm))); const engine = config.feature.filter_regex_engine; const makeRegex = (str) => { const insensitive = ctx.req.query("filter_case_sensitive") === "false"; switch (engine) { case "regexp": return new RegExp(str, insensitive ? "i" : ""); case "re2": return RE2JS.compile(str, insensitive ? RE2JS.CASE_INSENSITIVE : 0); default: throw new Error(`Invalid Engine Value: ${engine}, please check your config.`); } }; if (ctx.req.query("filter")) { const regex = makeRegex(ctx.req.query("filter")); data.item = data.item.filter((item) => { const title = item.title || ""; const description = item.description || title; const author = getAuthorString(item); const category = item.category || []; return regex instanceof RE2JS ? regex.matcher(title).find() || regex.matcher(description).find() || regex.matcher(author).find() || category.some((c) => regex.matcher(c).find()) : title.match(regex) || description.match(regex) || author.match(regex) || category.some((c) => c.match(regex)); }); } if (!ctx.req.query("filter") && (ctx.req.query("filter_title") || ctx.req.query("filter_description") || ctx.req.query("filter_author") || ctx.req.query("filter_category"))) data.item = data.item.filter((item) => { const title = item.title || ""; const description = item.description || title; const author = getAuthorString(item); const category = item.category || []; let isFilter = true; if (ctx.req.query("filter_title")) { const titleRegex = makeRegex(ctx.req.query("filter_title")); isFilter = titleRegex instanceof RE2JS ? titleRegex.matcher(title).find() : !!titleRegex.test(title); } if (ctx.req.query("filter_description")) { const descriptionRegex = makeRegex(ctx.req.query("filter_description")); isFilter &&= descriptionRegex instanceof RE2JS ? descriptionRegex.matcher(description).find() : !!descriptionRegex.test(description); } if (ctx.req.query("filter_author")) { const authorRegex = makeRegex(ctx.req.query("filter_author")); isFilter &&= authorRegex instanceof RE2JS ? authorRegex.matcher(author).find() : !!authorRegex.test(author); } if (ctx.req.query("filter_category")) { const categoryRegex = makeRegex(ctx.req.query("filter_category")); isFilter &&= category.some((c) => categoryRegex instanceof RE2JS ? categoryRegex.matcher(c).find() : c.match(categoryRegex)); } return isFilter; }); if (ctx.req.query("filterout") || ctx.req.query("filterout_title") || ctx.req.query("filterout_description") || ctx.req.query("filterout_author") || ctx.req.query("filterout_category")) data.item = data.item.filter((item) => { const title = item.title; const description = item.description || title; const author = getAuthorString(item); const category = item.category || []; let isFilter = true; if (ctx.req.query("filterout") || ctx.req.query("filterout_title")) { const titleRegex = makeRegex(ctx.req.query("filterout_title") || ctx.req.query("filterout")); isFilter = titleRegex instanceof RE2JS ? !titleRegex.matcher(title).find() : !titleRegex.test(title); } if (ctx.req.query("filterout") || ctx.req.query("filterout_description")) { const descriptionRegex = makeRegex(ctx.req.query("filterout_description") || ctx.req.query("filterout")); isFilter &&= descriptionRegex instanceof RE2JS ? !descriptionRegex.matcher(description).find() : !descriptionRegex.test(description); } if (ctx.req.query("filterout_author")) { const authorRegex = makeRegex(ctx.req.query("filterout_author")); isFilter &&= authorRegex instanceof RE2JS ? !authorRegex.matcher(author).find() : !authorRegex.test(author); } if (ctx.req.query("filterout_category")) { const categoryRegex = makeRegex(ctx.req.query("filterout_category")); isFilter &&= category.every((c) => !(categoryRegex instanceof RE2JS ? categoryRegex.matcher(c).find() : c.match(categoryRegex))); } return isFilter; }); if (ctx.req.query("filter_time")) { const now = Date.now(); data.item = data.item.filter(({ pubDate }) => { let isFilter = true; try { isFilter = !pubDate || now - new Date(pubDate).getTime() <= Number.parseInt(ctx.req.query("filter_time")) * 1e3; } catch {} return isFilter; }); } if (ctx.req.query("limit")) data.item = data.item.slice(0, Number.parseInt(ctx.req.query("limit"))); if (ctx.req.query("tgiv")) data.item.map((item) => { if (item.link) { item.link = `https://t.me/iv?url=${encodeURIComponent(item.link)}&rhash=${ctx.req.query("tgiv")}`; return item; } return item; }); if (ctx.req.query("mode")?.toLowerCase() === "fulltext") { const tasks = data.item.map(async (item) => { const { link, author, description } = item; const parsed_result = await cache_default.tryGet(`mercury-cache-${link}`, async () => { if (link) try { const { default: Parser } = await import("@jocmp/mercury-parser"); const $ = load(await rofetch(link)); return await Parser.parse(link, { html: $.html() }); } catch {} }); item.author = author || parsed_result?.author; item.description = parsed_result && parsed_result.content.length > 40 ? entities.decodeXML(parsed_result.content) : description; }); await Promise.all(tasks); } if (ctx.req.query("chatgpt") && config.openai.apiKey) data.item = await Promise.all(data.item.map(async (item) => { try { if (config.openai.inputOption === "description" && item.description) { const description = await cache_default.tryGet(`openai:description:${item.link}`, async () => { const description = convert(item.description); const descriptionMd = await getAiCompletion(config.openai.promptDescription, description); return md.render(descriptionMd); }); if (description !== "") item.description = description + "<hr/><br/>" + item.description; } else if (config.openai.inputOption === "title" && item.title) { const title = await cache_default.tryGet(`openai:title:${item.link}`, async () => { const title = convert(item.title); return await getAiCompletion(config.openai.promptTitle, title); }); if (title !== "") item.title = title + ""; } else if (config.openai.inputOption === "both" && item.title && item.description) { const title = await cache_default.tryGet(`openai:title:${item.link}`, async () => { const title = convert(item.title); return await getAiCompletion(config.openai.promptTitle, title); }); if (title !== "") item.title = title + ""; const description = await cache_default.tryGet(`openai:description:${item.link}`, async () => { const description = convert(item.description); const descriptionMd = await getAiCompletion(config.openai.promptDescription, description); return md.render(descriptionMd); }); if (description !== "") item.description = description + "<hr/><br/>" + item.description; } } catch {} return item; })); if (ctx.req.query("scihub")) data.item.map((item) => { item.link = item.doi ? `${config.scihub.host}${item.doi}` : `${config.scihub.host}${item.link}`; return item; }); if (ctx.req.query("opencc")) for (const item of data.item) { item.title = simplecc(item.title ?? item.link, ctx.req.query("opencc")); item.description = simplecc(item.description ?? item.title ?? item.link, ctx.req.query("opencc")); } if (ctx.req.query("brief")) if (/[1-9]\d{2,}/.test(ctx.req.query("brief"))) { const brief = Number.parseInt(ctx.req.query("brief")); for (const item of data.item) { if (!item.description) continue; const text = sanitizeHtml(item.description, { allowedTags: [], allowedAttributes: {} }); item.description = text.length > brief ? `<p>${text.slice(0, brief)}…</p>` : `<p>${text}</p>`; } } else throw new Error("Invalid parameter brief. Please check the doc https://docs.rsshub.app/guide/parameters#shu-chu-jian-xun"); ctx.set("data", data); } }; //#endregion //