UNPKG

rsshub

Version:
1,249 lines (1,228 loc) 45.8 kB
import "./esm-shims-CzJ_djXG.mjs"; import { t as config } from "./config-C37vj7VH.mjs"; import "./dist-BInvbO1W.mjs"; import { t as logger_default$1 } from "./logger-Czu8UMNd.mjs"; import { t as ofetch_default } from "./ofetch-BIyrKU3Y.mjs"; import "./parse-date-BrP7mxXf.mjs"; import { a as Layout, c as getDebugInfo, i as requestMetric, l as setDebugInfo, n as registry_default, o as gitDate, r as tracer, s as gitHash, t as namespaces } from "./registry-DfukR-yn.mjs"; import { t as not_found_default } from "./not-found-Z_3JX2qs.mjs"; import { t as reject_default } from "./reject-Cf3yDwA8.mjs"; import { t as md5 } from "./md5-C8GRvctM.mjs"; import { t as request_in_progress_default } from "./request-in-progress-Bdms9gGw.mjs"; import { t as cache_default$1 } from "./cache-Bo__VnGm.mjs"; import { n as getPath, o as time, r as getRouteNameFromPath } from "./helpers-DxBp0Pty.mjs"; import { n as convertDateToISO8601, t as collapseWhitespace } from "./common-utils-vrWQFAEk.mjs"; import { a as atom_default, i as json_default, n as rss3_default, r as rss_default } from "./render-BQo6B4tL.mjs"; 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 * as Sentry from "@sentry/node"; import { load } from "cheerio"; import xxhash from "xxhash-wasm"; import etagCalculate from "etag"; import Parser from "@postlight/parser"; 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/api/category/one.ts const categoryList = {}; for (const namespace in namespaces) for (const path in namespaces[namespace].routes) if (namespaces[namespace].routes[path].categories?.length) for (const category of namespaces[namespace].routes[path].categories) { if (!categoryList[category]) categoryList[category] = {}; if (!categoryList[category][namespace]) categoryList[category][namespace] = { ...namespaces[namespace], routes: {} }; categoryList[category][namespace].routes[path] = namespaces[namespace].routes[path]; } const ParamsSchema = z.object({ category: z.string().openapi({ param: { name: "category", in: "path" }, example: "popular" }) }); const route = createRoute({ method: "get", path: "/category/{category}", tags: ["Category"], request: { query: z.object({ categories: z.string().transform((val) => val.split(",")).optional(), lang: z.string().optional() }), params: ParamsSchema }, responses: { 200: { description: "Namespace list by categories and language" } } }); const handler = (ctx) => { 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$1 = createRoute({ method: "get", path: "/follow/config", tags: ["Follow"], responses: { 200: { description: "Follow config" } } }); const handler$1 = (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$2 = createRoute({ method: "get", path: "/namespace", tags: ["Namespace"], responses: { 200: { description: "Information about all namespaces" } } }); const handler$2 = (ctx) => ctx.json(namespaces); //#endregion //#region lib/api/namespace/one.ts const route$3 = createRoute({ method: "get", path: "/namespace/{namespace}", tags: ["Namespace"], request: { params: z.object({ namespace: z.string().openapi({ param: { name: "namespace", in: "path" }, example: "github" }) }) }, responses: { 200: { description: "Information about a namespace" } } }); const handler$3 = (ctx) => { const { namespace } = ctx.req.valid("param"); return ctx.json(namespaces[namespace]); }; //#endregion //#region lib/api/radar/rules/all.ts const radar$1 = {}; 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 (!radar$1[domain]) radar$1[domain] = { _name: namespaces[namespace].name }; if (!radar$1[domain][subdomain]) radar$1[domain][subdomain] = []; radar$1[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 }); } } } const route$4 = createRoute({ method: "get", path: "/radar/rules", tags: ["Radar"], responses: { 200: { description: "All Radar rules" } } }); const handler$4 = (ctx) => ctx.json(radar$1); //#endregion //#region lib/api/radar/rules/one.ts const radar = {}; 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 (!radar[domain]) radar[domain] = { _name: namespaces[namespace].name }; if (!radar[domain][subdomain]) radar[domain][subdomain] = []; radar[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 }); } } } const route$5 = createRoute({ method: "get", path: "/radar/rules/{domain}", 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 (does not support subdomains)" } } }); const handler$5 = (ctx) => { const { domain } = ctx.req.valid("param"); return ctx.json(radar[domain]); }; //#endregion //#region lib/api/index.ts const app$1 = new OpenAPIHono(); app$1.openapi(route$2, handler$2); app$1.openapi(route$3, handler$3); app$1.openapi(route$4, handler$4); app$1.openapi(route$5, handler$5); app$1.openapi(route, handler); app$1.openapi(route$1, handler$1); 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 })); var api_default = app$1; //#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", 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 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", children: errorRoute })] }), /* @__PURE__ */ jsxs("p", { className: "message", children: ["Full Route: ", /* @__PURE__ */ jsx("code", { className: "ml-2 bg-zinc-100", children: requestPath })] }), /* @__PURE__ */ jsxs("p", { className: "message", children: ["Node Version: ", /* @__PURE__ */ jsx("code", { className: "ml-2 bg-zinc-100", children: nodeVersion })] }), /* @__PURE__ */ jsxs("p", { className: "message", children: ["Git Hash: ", /* @__PURE__ */ jsx("code", { className: "ml-2 bg-zinc-100", children: gitHash })] }), /* @__PURE__ */ jsxs("p", { className: "message", children: ["Git Date: ", /* @__PURE__ */ jsx("code", { className: "ml-2 bg-zinc-100", 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__ */ 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 error_default = Index; //#endregion //#region lib/errors/index.tsx 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.sentry.dsn) Sentry.withScope((scope) => { scope.setTag("name", requestPath.split("/")[1]); Sentry.captureException(error); }); let errorMessage = (process.env.NODE_ENV || process.env.VERCEL_ENV) === "production" ? error.message : error.stack || error.message; switch (error.constructor.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; } const message = `${error.name}: ${errorMessage}`; logger_default$1.error(`Error in ${requestPath}: ${message}`); 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(error_default, { requestPath, message, errorRoute: hasMatchedRoute ? matchedRoute : requestPath, nodeVersion: process.version })); }; const notFoundHandler = (ctx) => errorHandler(new not_found_default(), ctx); //#endregion //#region lib/middleware/access-control.ts const reject = (requestPath) => { throw new reject_default(`Authentication failed. Access denied.\n${requestPath}`); }; const middleware$9 = async (ctx, next) => { const requestPath = new URL(ctx.req.url).pathname; const accessKey = ctx.req.query("key"); const accessCode = ctx.req.query("code"); if (requestPath === "/" || requestPath === "/robots.txt" || requestPath === "/favicon.ico" || requestPath === "/logo.png") await next(); else { if (config.accessKey && !(config.accessKey === accessKey || accessCode === md5(requestPath + config.accessKey))) return reject(requestPath); await next(); } }; var access_control_default = middleware$9; //#endregion //#region lib/middleware/anti-hotlink.ts const templateRegex = /\${([^{}]+)}/g; const allowedUrlProperties = 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_default$1.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(function() { const oldSrc = $(this).attr(attribute); if (oldSrc) { const url = parseUrl(oldSrc); if (url && url.protocol !== "data:") $(this).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$8 = 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); } }; var anti_hotlink_default = middleware$8; //#endregion //#region lib/middleware/cache.ts const bypassList = new Set([ "/", "/robots.txt", "/logo.png", "/favicon.ico" ]); const middleware$7 = async (ctx, next) => { if (!cache_default$1.status.available || bypassList.has(ctx.req.path)) { await next(); return; } const requestPath = ctx.req.path; const format = `:${ctx.req.query("format") || "rss"}`; const limit = ctx.req.query("limit") ? `:${ctx.req.query("limit")}` : ""; const { h64ToString } = await xxhash(); const key = "rsshub:koa-redis-cache:" + h64ToString(requestPath + format + limit); const controlKey = "rsshub:path-requested:" + h64ToString(requestPath + format + limit); if (await cache_default$1.globalCache.get(controlKey) === "1") { 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$1.globalCache.get(controlKey) !== "1") { bypass = true; break; } retryTimes--; } if (!bypass) throw new request_in_progress_default("This path is currently fetching, please come back later!"); } const value = await cache_default$1.globalCache.get(key); if (value) { ctx.status(200); ctx.header("RSSHub-Cache-Status", "HIT"); ctx.set("data", JSON.parse(value)); await next(); return; } await cache_default$1.globalCache.set(controlKey, "1", config.cache.requestTimeout); ctx.set("cacheKey", key); ctx.set("cacheControlKey", controlKey); try { await next(); } catch (error) { await cache_default$1.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$1.globalCache.set(key, body, config.cache.routeExpire); } await cache_default$1.globalCache.set(controlKey, "0", config.cache.requestTimeout); }; var cache_default = middleware$7; //#endregion //#region lib/middleware/debug.ts const middleware$6 = 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); } }; var debug_default = middleware$6; //#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$5 = 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 = data.lastBuildDate; delete data.lastBuildDate; const etag = etagCalculate(JSON.stringify(data)); 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); }; var header_default = middleware$5; //#endregion //#region lib/middleware/logger.ts var LogPrefix = /* @__PURE__ */ function(LogPrefix$1) { LogPrefix$1["Outgoing"] = "-->"; LogPrefix$1["Incoming"] = "<--"; LogPrefix$1["Error"] = "xxx"; return LogPrefix$1; }(LogPrefix || {}); const colorStatus = (status) => { return { 7: `\u001B[35m${status}\u001B[0m`, 5: `\u001B[31m${status}\u001B[0m`, 4: `\u001B[33m${status}\u001B[0m`, 3: `\u001B[36m${status}\u001B[0m`, 2: `\u001B[32m${status}\u001B[0m`, 1: `\u001B[32m${status}\u001B[0m`, 0: `\u001B[33m${status}\u001B[0m` }[Math.trunc(status / 100)]; }; const middleware$4 = async (ctx, next) => { const { method, raw, routePath: routePath$1 } = ctx.req; const path = getPath(raw); logger_default$1.info(`${LogPrefix.Incoming} ${method} ${path}`); const start = Date.now(); await next(); const status = ctx.res.status; logger_default$1.info(`${LogPrefix.Outgoing} ${method} ${path} ${colorStatus(status)} ${time(start)}`); requestMetric.success(Date.now() - start, { path: routePath$1, method, status }); }; var logger_default = middleware$4; //#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 ofetch_default(`${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.item || []; data.title && (data.title = entities.decodeXML(data.title + "")); data.description && (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 && (item.title = entities.decodeXML(item.title + "")); if (item.pubDate) item.pubDate = new Date(item.pubDate).toUTCString(); if (item.link) { let baseUrl = data.link; if (baseUrl && !/^https?:\/\//.test(baseUrl)) baseUrl = /^\/\//.test(baseUrl) ? "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 = /^\/\//.test(baseUrl) ? "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 = 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 = 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 = 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 = 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 = 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 = isFilter && !category.some((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; } else 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$1.tryGet(`mercury-cache-${link}`, async () => { if (link) try { const $ = load(await ofetch_default(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$1.tryGet(`openai:description:${item.link}`, async () => { const description$1 = convert(item.description); const descriptionMd = await getAiCompletion(config.openai.promptDescription, description$1); 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$1.tryGet(`openai:title:${item.link}`, async () => { const title$1 = convert(item.title); return await getAiCompletion(config.openai.promptTitle, title$1); }); if (title !== "") item.title = title + ""; } else if (config.openai.inputOption === "both" && item.title && item.description) { const title = await cache_default$1.tryGet(`openai:title:${item.link}`, async () => { const title$1 = convert(item.title); return await getAiCompletion(config.openai.promptTitle, title$1); }); if (title !== "") item.title = title + ""; const description = await cache_default$1.tryGet(`openai:description:${item.link}`, async () => { const description$1 = convert(item.description); const descriptionMd = await getAiCompletion(config.openai.promptDescription, description$1); 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) { let text; if (item.description) { 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); } }; var parameter_default = middleware$3; //#endregion //#region lib/middleware/sentry.ts if (config.sentry.dsn) { Sentry.init({ dsn: config.sentry.dsn }); Sentry.getCurrentScope().setTag("node_name", config.nodeName); logger_default$1.info("Sentry inited."); } const middleware$2 = async (ctx, next) => { const time$1 = Date.now(); await next(); if (config.sentry.dsn && Date.now() - time$1 >= config.sentry.routeTimeout) Sentry.withScope((scope) => { scope.setTag("name", getRouteNameFromPath(ctx.req.path)); Sentry.captureException(/* @__PURE__ */ new Error("Route Timeout")); }); }; var sentry_default = middleware$2; //#endregion //#region lib/middleware/template.tsx const middleware$1 = async (ctx, next) => { const ttl = cache_default$1.status.available && Math.trunc(config.cache.routeExpire / 60) || 1; await next(); const apiData = ctx.get("apiData"); if (apiData) return ctx.json(apiData); const data = ctx.get("data"); const outputType = ctx.req.query("format") || "rss"; if (config.debugInfo) { if (outputType === "debug.json") return ctx.json(ctx.get("json") || { message: "plugin does not set debug json" }); if (/(\d+)\.debug\.html$/.test(outputType)) { const index = Number.parseInt(outputType.match(/(\d+)\.debug\.html$/)?.[1] || "0"); return ctx.html(data?.item?.[index]?.description || `data.item[${index}].description not found`); } } if (data) { data.title = collapseWhitespace(data.title) || ""; data.description && (data.description = collapseWhitespace(data.description) || ""); data.author && (data.author = collapseWhitespace(data.author) || ""); if (data.item) for (const item of data.item) { if (item.title) { item.title = collapseWhitespace(item.title) || ""; for (let length = 0, i = 0; i < item.title.length; i++) { length += Buffer.from(item.title[i]).length === 1 ? 1 : 2; if (length > config.titleLengthLimit) { item.title = `${item.title.slice(0, i)}...`; break; } } } if (item.description) item.description = item.description.replaceAll(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F\u200B\uFFFF]/g, ""); if (typeof item.author === "string") item.author = collapseWhitespace(item.author) || ""; else if (typeof item.author === "object" && item.author !== null) { for (const a of item.author) a.name = collapseWhitespace(a.name) || ""; if (outputType !== "json") item.author = item.author.map((a) => a.name).join(", "); } if (item.itunes_duration && (typeof item.itunes_duration === "string" && !item.itunes_duration.includes(":") || typeof item.itunes_duration === "number" && !Number.isNaN(item.itunes_duration))) { item.itunes_duration = +item.itunes_duration; item.itunes_duration = Math.floor(item.itunes_duration / 3600) + ":" + (Math.floor(item.itunes_duration % 3600 / 60) / 100).toFixed(2).slice(-2) + ":" + (item.itunes_duration % 3600 % 60 / 100).toFixed(2).slice(-2); } if (outputType !== "rss") { try { item.pubDate && (item.pubDate = convertDateToISO8601(item.pubDate) || ""); } catch { item.pubDate = ""; } try { item.updated && (item.updated = convertDateToISO8601(item.updated) || ""); } catch { item.updated = ""; } } } } const currentDate = /* @__PURE__ */ new Date(); const result = { lastBuildDate: currentDate.toUTCString(), updated: currentDate.toISOString(), ttl, atomlink: ctx.req.url, ...data }; if (config.isPackage) return ctx.json(result); if (ctx.get("redirect")) return ctx.redirect(ctx.get("redirect"), 301); else if (ctx.get("no-content")) return ctx.body(null); else switch (outputType) { case "ums": case "rss3": return ctx.json(rss3_default(result)); case "json": ctx.header("Content-Type", "application/feed+json; charset=UTF-8"); return ctx.body(json_default(result)); case "atom": return ctx.render(/* @__PURE__ */ jsx(atom_default, { data: result })); default: return ctx.render(/* @__PURE__ */ jsx(rss_default, { data: result })); } }; var template_default = middleware$1; //#endregion //#region lib/middleware/trace.ts const middleware = async (ctx, next) => { if (config.debugInfo) { const { method, raw } = ctx.req; const path = getPath(raw); const span = tracer.startSpan(`${method} ${path}`, { kind: 1, attributes: {} }); span.addEvent("invoking handleRequest"); await next(); span.end(); } else await next(); }; var trace_default = middleware; //#endregion //#region lib/app-bootstrap.tsx process.on("uncaughtException", (e) => { logger_default$1.error("uncaughtException: " + e); }); const app = new Hono(); app.use(trimTrailingSlash()); app.use(compress()); app.use(jsxRenderer(({ children }) => /* @__PURE__ */ jsx(Fragment, { children }), { docType: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>", stream: {} })); app.use(logger_default); app.use(trace_default); app.use(sentry_default); app.use(access_control_default); app.use(debug_default); app.use(template_default); app.use(header_default); app.use(anti_hotlink_default); app.use(parameter_default); app.use(cache_default); app.route("/", registry_default); app.route("/api", api_default); app.notFound(notFoundHandler); app.onError(errorHandler); var app_bootstrap_default = app; //#endregion export { app_bootstrap_default as default };