UNPKG

rsshub

Version:
575 lines (562 loc) 19.5 kB
import { t as config } from "./config-C37vj7VH.mjs"; import { t as logger_default } from "./logger-Czu8UMNd.mjs"; import path from "node:path"; import { Hono } from "hono"; import { serveStatic } from "@hono/node-server/serve-static"; import { directoryImport } from "directory-import"; import { routePath } from "hono/route"; import { execSync } from "node:child_process"; import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime"; import { PrometheusExporter, PrometheusSerializer } from "@opentelemetry/exporter-prometheus"; import { resourceFromAttributes } from "@opentelemetry/resources"; import { MeterProvider } from "@opentelemetry/sdk-metrics"; import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; import { trace } from "@opentelemetry/api"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { BasicTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; //#region lib/routes/healthz.ts const handler$3 = (ctx) => { ctx.header("Cache-Control", "no-cache"); return ctx.text("ok"); }; var healthz_default = handler$3; //#endregion //#region lib/utils/debug-info.ts const debug = { hitCache: 0, request: 0, etag: 0, error: 0, routes: {}, paths: {}, errorRoutes: {}, errorPaths: {} }; const getDebugInfo = () => debug; const setDebugInfo = (info) => Object.assign(debug, info); //#endregion //#region lib/utils/git-hash.ts let gitHash = process.env.HEROKU_SLUG_COMMIT?.slice(0, 8) || process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 8); let gitDate; if (!gitHash) try { gitHash = execSync("git rev-parse HEAD").toString().trim().slice(0, 8); gitDate = new Date(execSync("git log -1 --format=%cd").toString().trim()); } catch { gitHash = "unknown"; } //#endregion //#region lib/views/layout.tsx const Layout = (props) => /* @__PURE__ */ jsxs("html", { children: [/* @__PURE__ */ jsxs("head", { children: [ /* @__PURE__ */ jsx("title", { children: "Welcome to RSSHub!" }), /* @__PURE__ */ jsx("script", { src: "https://cdn.tailwindcss.com" }), /* @__PURE__ */ jsx("style", { children: ` details::-webkit-scrollbar { width: 0.25rem; } details::-webkit-scrollbar-thumb { border-radius: 0.125rem; background-color: #e4e4e7; } details::-webkit-scrollbar-thumb:hover { background-color: #a1a1aa; } @font-face { font-family: SN Pro; font-style: normal; font-display: swap; font-weight: 400; src: url(https://cdn.jsdelivr.net/fontsource/fonts/sn-pro@latest/latin-400-normal.woff2) format(woff2); } @font-face { font-family: SN Pro; font-style: normal; font-display: swap; font-weight: 500; src: url(https://cdn.jsdelivr.net/fontsource/fonts/sn-pro@latest/latin-500-normal.woff2) format(woff2); } @font-face { font-family: SN Pro; font-style: normal; font-display: swap; font-weight: 700; src: url(https://cdn.jsdelivr.net/fontsource/fonts/sn-pro@latest/latin-700-normal.woff2) format(woff2); } body { font-family: SN Pro, sans-serif; } ` }) ] }), /* @__PURE__ */ jsx("body", { className: "antialiased min-h-screen text-zinc-700 flex flex-col", children: props.children })] }); //#endregion //#region lib/views/index.tsx const startTime = Date.now(); const Index = ({ debugQuery }) => { const debug$1 = getDebugInfo(); const showDebug = !config.debugInfo || config.debugInfo === "false" ? false : config.debugInfo === "true" || config.debugInfo === debugQuery; const { disallowRobot, nodeName, cache } = config; const duration = Date.now() - startTime; const info = { showDebug, disallowRobot, debug: [ ...nodeName ? [{ name: "Node Name", value: nodeName }] : [], ...gitHash ? [{ name: "Git Hash", value: /* @__PURE__ */ jsx("a", { className: "underline", href: `https://github.com/DIYgod/RSSHub/commit/${gitHash}`, children: gitHash }) }] : [], ...gitDate ? [{ name: "Git Date", value: gitDate.toUTCString() }] : [], { name: "Cache Duration", value: cache.routeExpire + "s" }, { name: "Request Amount", value: debug$1.request }, { name: "Request Frequency", value: (debug$1.request / (duration / 1e3) * 60).toFixed(3) + " times/minute" }, { name: "Cache Hit Ratio", value: debug$1.request ? (debug$1.hitCache / debug$1.request * 100).toFixed(2) + "%" : 0 }, { name: "ETag Matched Ratio", value: debug$1.request ? (debug$1.etag / debug$1.request * 100).toFixed(2) + "%" : 0 }, { name: "Health", value: debug$1.request ? ((1 - debug$1.error / debug$1.request) * 100).toFixed(2) + "%" : 0 }, { name: "Uptime", value: (duration / 36e5).toFixed(2) + " hour(s)" }, { name: "Hot Routes", value: Object.keys(debug$1.routes).toSorted((a, b) => debug$1.routes[b] - debug$1.routes[a]).slice(0, 30).map((route) => /* @__PURE__ */ jsxs(Fragment, { children: [ debug$1.routes[route], " ", route, /* @__PURE__ */ jsx("br", {}) ] })) }, { name: "Hot Paths", value: Object.keys(debug$1.paths).toSorted((a, b) => debug$1.paths[b] - debug$1.paths[a]).slice(0, 30).map((path$1) => /* @__PURE__ */ jsxs(Fragment, { children: [ debug$1.paths[path$1], " ", path$1, /* @__PURE__ */ jsx("br", {}) ] })) }, { name: "Hot Error Routes", value: Object.keys(debug$1.errorRoutes).toSorted((a, b) => debug$1.errorRoutes[b] - debug$1.errorRoutes[a]).slice(0, 30).map((route) => /* @__PURE__ */ jsxs(Fragment, { children: [ debug$1.errorRoutes[route], " ", route, /* @__PURE__ */ jsx("br", {}) ] })) }, { name: "Hot Error Paths", value: Object.keys(debug$1.errorPaths).toSorted((a, b) => debug$1.errorPaths[b] - debug$1.errorPaths[a]).slice(0, 30).map((path$1) => /* @__PURE__ */ jsxs(Fragment, { children: [ debug$1.errorPaths[path$1], " ", path$1, /* @__PURE__ */ jsx("br", {}) ] })) } ] }; return /* @__PURE__ */ jsxs(Layout, { children: [ /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute w-full min-h-screen", 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", { src: "./logo.png", alt: "RSSHub", width: "100", loading: "lazy" }), /* @__PURE__ */ jsxs("h1", { className: "text-4xl font-bold", children: [ "Welcome to ", /* @__PURE__ */ jsx("span", { className: "text-[#F5712C]", children: "RSSHub" }), "!" ] }), /* @__PURE__ */ jsx("p", { className: "text-xl font-medium text-zinc-600", children: "The world's largest RSS Network." }), /* @__PURE__ */ jsx("p", { className: "text-zinc-500", children: "If you see this page, the RSSHub is successfully installed and working." }), /* @__PURE__ */ jsxs("div", { className: "font-bold space-x-4 text-sm", children: [/* @__PURE__ */ jsx("a", { target: "_blank", href: "https://docs.rsshub.app", children: /* @__PURE__ */ jsx("button", { className: "text-white bg-[#F5712C] hover:bg-[#DD4A15] py-2 px-4 rounded-full transition-colors", children: "Home" }) }), /* @__PURE__ */ jsx("a", { target: "_blank", href: "https://github.com/DIYgod/RSSHub", children: /* @__PURE__ */ jsx("button", { className: "bg-zinc-200 hover:bg-zinc-300 py-2 px-4 rounded-full transition-colors", children: "GitHub" }) })] }), info.showDebug ? /* @__PURE__ */ jsxs("details", { className: "text-xs w-96 !mt-8 max-h-[400px] overflow-auto", children: [/* @__PURE__ */ jsx("summary", { className: "text-sm cursor-pointer", children: "Debug Info" }), info.debug.map((item) => /* @__PURE__ */ jsxs("div", { class: "debug-item my-3 pl-8", children: [/* @__PURE__ */ jsxs("span", { class: "debug-key w-32 text-right inline-block mr-2", children: [item.name, ": "] }), /* @__PURE__ */ jsx("span", { class: "debug-value inline-block break-all align-top", children: item.value })] }))] }) : null ] }), /* @__PURE__ */ jsxs("div", { className: "text-center pt-4 pb-8 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__ */ jsx("img", { className: "inline", src: "https://icons.ly/github/_/fff", 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__ */ 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 MIT License." ] }) ] }) ] }); }; var views_default = Index; //#endregion //#region lib/routes/index.tsx const handler$2 = (ctx) => { ctx.header("Cache-Control", "no-cache"); return ctx.html(/* @__PURE__ */ jsx(views_default, { debugQuery: ctx.req.query("debug") })); }; var routes_default = handler$2; //#endregion //#region lib/utils/otel/metric.ts const METRIC_PREFIX = "rsshub"; const exporter$1 = new PrometheusExporter({}); const provider$1 = new MeterProvider({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "rsshub" }), readers: [exporter$1] }); const serializer = new PrometheusSerializer(); const meter = provider$1.getMeter("rsshub"); const requestTotal = meter.createCounter(`${METRIC_PREFIX}_request_total`); const requestErrorTotal = meter.createCounter(`${METRIC_PREFIX}_request_error_total`); const requestDurationSecondsBucket = meter.createHistogram(`${METRIC_PREFIX}_request_duration_seconds_bucket`, { advice: { explicitBucketBoundaries: config.otel.seconds_bucket?.split(",").map(Number) } }); const request_duration_milliseconds_bucket = meter.createHistogram(`${METRIC_PREFIX}_request_duration_milliseconds_bucket`, { advice: { explicitBucketBoundaries: config.otel.milliseconds_bucket?.split(",").map(Number) } }); const requestMetric = { success: (value, attributes) => { requestTotal.add(1, attributes); request_duration_milliseconds_bucket.record(value, { unit: "millisecond", ...attributes }); requestDurationSecondsBucket.record(value / 1e3, { unit: "second", ...attributes }); }, error: (attributes) => { requestErrorTotal.add(1, attributes); } }; const getContext = () => new Promise((resolve, reject) => { exporter$1.collect().then((value) => { resolve(serializer.serialize(value.resourceMetrics)); }).finally(() => { reject(""); }); }); //#endregion //#region lib/utils/otel/trace.ts const exporter = new OTLPTraceExporter({}); const provider = new BasicTracerProvider({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "rsshub" }), spanProcessors: [new BatchSpanProcessor(exporter, { maxQueueSize: 4096, scheduledDelayMillis: 3e4 })] }); trace.setGlobalTracerProvider(provider); const tracer = provider.getTracer("rsshub"); const mainSpan = tracer.startSpan("main"); //#endregion //#region lib/routes/metrics.ts const handler$1 = (ctx) => getContext().then((val) => ctx.text(val)).catch((error) => { ctx.status(500); ctx.json({ error }); }); var metrics_default = handler$1; //#endregion //#region lib/routes/robots.txt.ts const handler = (ctx) => { if (config.disallowRobot) return ctx.text("User-agent: *\nDisallow: /"); else { ctx.status(404); return ctx.text(""); } }; var robots_txt_default = handler; //#endregion //#region lib/registry.ts const __dirname = import.meta.dirname; function isSafeRoutes(routes) { return Object.values(routes).every((route) => !route.features?.nsfw); } function safeNamespaces(namespaces$1) { const safe = {}; for (const [key, value] of Object.entries(namespaces$1)) if (value.routes === null || value.routes === void 0 || isSafeRoutes(value.routes)) safe[key] = value; return safe; } let modules = {}; let namespaces = {}; if (config.isPackage) namespaces = (await import("./routes-DSc26Lhr.mjs")).default; else switch (process.env.NODE_ENV || process.env.VERCEL_ENV) { case "production": namespaces = (await import("./routes-DSc26Lhr.mjs")).default; break; case "test": namespaces = await import("./routes-0gfsvfPA.mjs"); if (namespaces.default) namespaces = namespaces.default; break; default: modules = directoryImport({ targetDirectoryPath: path.join(__dirname, "./routes"), importPattern: /\.ts$/ }); } if (config.feature.disable_nsfw) namespaces = safeNamespaces(namespaces); if (Object.keys(modules).length) for (const module in modules) { const content = modules[module]; const namespace = module.split(/[/\\]/)[1]; if ("namespace" in content) namespaces[namespace] = Object.assign({ routes: {} }, namespaces[namespace], content.namespace); else if ("route" in content) { if (!namespaces[namespace]) namespaces[namespace] = { name: namespace, routes: {}, apiRoutes: {} }; if (Array.isArray(content.route.path)) for (const path$1 of content.route.path) namespaces[namespace].routes[path$1] = { ...content.route, location: module.split(/[/\\]/).slice(2).join("/") }; else namespaces[namespace].routes[content.route.path] = { ...content.route, location: module.split(/[/\\]/).slice(2).join("/") }; } else if ("apiRoute" in content) { if (!namespaces[namespace]) namespaces[namespace] = { name: namespace, routes: {}, apiRoutes: {} }; if (Array.isArray(content.apiRoute.path)) for (const path$1 of content.apiRoute.path) namespaces[namespace].apiRoutes[path$1] = { ...content.apiRoute, location: module.split(/[/\\]/).slice(2).join("/") }; else namespaces[namespace].apiRoutes[content.apiRoute.path] = { ...content.apiRoute, location: module.split(/[/\\]/).slice(2).join("/") }; } } const app = new Hono(); const sortRoutes = (routes) => Object.entries(routes).toSorted(([pathA], [pathB]) => { const segmentsA = pathA.split("/"); const segmentsB = pathB.split("/"); const lenA = segmentsA.length; const lenB = segmentsB.length; const minLen = Math.min(lenA, lenB); for (let i = 0; i < minLen; i++) { const segmentA = segmentsA[i]; const segmentB = segmentsB[i]; if (segmentA.startsWith(":") !== segmentB.startsWith(":")) return segmentA.startsWith(":") ? 1 : -1; } return 0; }); for (const namespace in namespaces) { const subApp = app.basePath(`/${namespace}`); const namespaceData = namespaces[namespace]; if (!namespaceData || !namespaceData.routes) continue; const sortedRoutes = sortRoutes(namespaceData.routes); for (const [path$1, routeData] of sortedRoutes) { const wrappedHandler = async (ctx) => { logger_default.debug(`Matched route: ${routePath(ctx)}`); if (!ctx.get("data")) { if (typeof routeData.handler !== "function") { if (process.env.NODE_ENV === "test") { const { route } = await import(`./routes/${namespace}/${routeData.location}`); routeData.handler = route.handler; } else if (routeData.module) { const { route } = await routeData.module(); routeData.handler = route.handler; } } const response = await routeData.handler(ctx); if (response instanceof Response) return response; ctx.set("data", response); } }; subApp.get(path$1, wrappedHandler); } } for (const namespace in namespaces) { const subApp = app.basePath(`/api/${namespace}`); const namespaceData = namespaces[namespace]; if (!namespaceData || !namespaceData.apiRoutes) continue; const sortedRoutes = Object.entries(namespaceData.apiRoutes); for (const [path$1, routeData] of sortedRoutes) { const wrappedHandler = async (ctx) => { if (!ctx.get("apiData")) { if (typeof routeData.handler !== "function") { if (process.env.NODE_ENV === "test") { const { apiRoute } = await import(`./routes/${namespace}/${routeData.location}`); routeData.handler = apiRoute.handler; } else if (routeData.module) { const { apiRoute } = await routeData.module(); routeData.handler = apiRoute.handler; } } const data = await routeData.handler(ctx); ctx.set("apiData", data); } }; subApp.get(path$1, wrappedHandler); } } app.get("/", routes_default); app.get("/healthz", healthz_default); app.get("/robots.txt", robots_txt_default); if (config.debugInfo) app.get("/metrics", metrics_default); if (!config.isPackage) app.use("/*", serveStatic({ root: path.join(__dirname, "assets"), rewriteRequestPath: (path$1) => path$1 === "/favicon.ico" ? "/favicon.png" : path$1 })); var registry_default = app; //#endregion export { Layout as a, getDebugInfo as c, requestMetric as i, setDebugInfo as l, registry_default as n, gitDate as o, tracer as r, gitHash as s, namespaces as t };