From 5836d05d86c2d783aa23cc806dc2b37e0275d9c8 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:13:27 +0100 Subject: [PATCH] Add Podroll OPML file upload support --- package.json | 4 +- .../patch-endpoint-podroll-opml-upload.mjs | 482 ++++++++++++++++++ 2 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 scripts/patch-endpoint-podroll-opml-upload.mjs diff --git a/package.json b/package.json index 28a68bb2..2639d3b9 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "postinstall": "node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-activitypub-docloader-loglevel.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-posts-ai-fields.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-conversations-mastodon-disconnect.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs", - "serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-activitypub-docloader-loglevel.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-posts-ai-fields.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-conversations-mastodon-disconnect.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", + "postinstall": "node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-activitypub-docloader-loglevel.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-posts-ai-fields.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-conversations-mastodon-disconnect.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs", + "serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-activitypub-docloader-loglevel.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-posts-ai-fields.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-conversations-mastodon-disconnect.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/scripts/patch-endpoint-podroll-opml-upload.mjs b/scripts/patch-endpoint-podroll-opml-upload.mjs new file mode 100644 index 00000000..54cb79ca --- /dev/null +++ b/scripts/patch-endpoint-podroll-opml-upload.mjs @@ -0,0 +1,482 @@ +import { access, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; + +const endpointCandidates = [ + "node_modules/@rmdes/indiekit-endpoint-podroll", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-podroll", +]; + +const dashboardFormOld = + '
'; +const dashboardFormNew = + ''; + +const dashboardOpmlFieldOld = [ + '
', + ' ', + ' {{ __("podroll.opmlUrlHelp") }}', + ' ', + '
', +].join("\n"); + +const dashboardOpmlFieldNew = [ + '
', + ' ', + ' {{ __("podroll.opmlUrlHelp") }}', + ' ', + '
', + '
', + ' ', + ' {{ __("podroll.opmlFileHelp") }}', + ' ', + ' {% if config.hasOpmlUpload %}', + '

{{ __("podroll.opmlFileActive") }}

', + ' {% endif %}', + '
', + '
', + ' ', + '
', +].join("\n"); + +const controllerGetEffectiveOld = [ + '/**', + ' * Get effective URLs: DB-stored settings override env var defaults', + ' * @param {object} db - MongoDB database instance', + ' * @param {object} podrollConfig - Plugin config from env vars', + ' * @returns {Promise} Effective episodesUrl and opmlUrl', + ' */', + 'async function getEffectiveUrls(db, podrollConfig) {', + ' let episodesUrl = podrollConfig?.episodesUrl || "";', + ' let opmlUrl = podrollConfig?.opmlUrl || "";', + '', + ' if (db) {', + ' const settings = await db', + ' .collection("podrollMeta")', + ' .findOne({ key: "settings" });', + ' if (settings) {', + ' if (settings.episodesUrl) episodesUrl = settings.episodesUrl;', + ' if (settings.opmlUrl) opmlUrl = settings.opmlUrl;', + ' }', + ' }', + '', + ' return { episodesUrl, opmlUrl };', + '}', +].join("\n"); + +const controllerGetEffectiveNew = [ + '/**', + ' * Get effective podroll configuration: DB-stored settings override env var defaults', + ' * @param {object} db - MongoDB database instance', + ' * @param {object} podrollConfig - Plugin config from env vars', + ' * @returns {Promise} Effective episodesUrl, opmlUrl and opmlUpload', + ' */', + 'async function getEffectiveUrls(db, podrollConfig) {', + ' let episodesUrl = podrollConfig?.episodesUrl || "";', + ' let opmlUrl = podrollConfig?.opmlUrl || "";', + ' let opmlUpload = podrollConfig?.opmlUpload || "";', + '', + ' if (db) {', + ' const settings = await db', + ' .collection("podrollMeta")', + ' .findOne({ key: "settings" });', + ' if (settings) {', + ' if (settings.episodesUrl) episodesUrl = settings.episodesUrl;', + ' if (settings.opmlUrl) opmlUrl = settings.opmlUrl;', + ' if (settings.opmlUpload) opmlUpload = settings.opmlUpload;', + ' }', + ' }', + '', + ' return { episodesUrl, opmlUrl, opmlUpload };', + '}', +].join("\n"); + +const controllerConfigOld = [ + ' config: {', + ' episodesUrl: urls.episodesUrl,', + ' opmlUrl: urls.opmlUrl,', + ' syncInterval: application.podrollConfig?.syncInterval || 900000,', + ' },', +].join("\n"); + +const controllerConfigNew = [ + ' config: {', + ' episodesUrl: urls.episodesUrl,', + ' opmlUrl: urls.opmlUrl,', + ' hasOpmlUpload: Boolean(urls.opmlUpload),', + ' syncInterval: application.podrollConfig?.syncInterval || 900000,', + ' },', +].join("\n"); + +const controllerSaveOld = [ + ' const { episodesUrl, opmlUrl } = request.body;', + '', + ' await db.collection("podrollMeta").updateOne(', + ' { key: "settings" },', + ' {', + ' $set: {', + ' key: "settings",', + ' episodesUrl: episodesUrl || "",', + ' opmlUrl: opmlUrl || "",', + ' updatedAt: new Date().toISOString(),', + ' },', + ' },', + ' { upsert: true },', + ' );', +].join("\n"); + +const controllerSaveNew = [ + ' const { episodesUrl, opmlUrl, clearOpmlFile } = request.body;', + '', + ' const existingSettings = await db', + ' .collection("podrollMeta")', + ' .findOne({ key: "settings" });', + '', + ' let opmlUpload = existingSettings?.opmlUpload || "";', + '', + ' if (clearOpmlFile === "1") {', + ' opmlUpload = "";', + ' }', + '', + ' const uploadedFileRaw = request.files?.opmlFile;', + ' const uploadedFile = Array.isArray(uploadedFileRaw)', + ' ? uploadedFileRaw[0]', + ' : uploadedFileRaw;', + '', + ' if (uploadedFile) {', + ' const uploadedName = String(uploadedFile.name || "");', + ' const uploadedType = String(uploadedFile.mimetype || "").toLowerCase();', + ' const isXmlName = /\\.(opml|xml)$/i.test(uploadedName);', + ' const isXmlType = uploadedType.includes("xml");', + '', + ' if (!isXmlName && !isXmlType) {', + ' throw new Error(request.__("podroll.opmlFileInvalidType"));', + ' }', + '', + ' if (uploadedFile.size > 1_048_576) {', + ' throw new Error(request.__("podroll.opmlFileTooLarge"));', + ' }', + '', + ' const opmlText = Buffer.from(uploadedFile.data).toString("utf8").trim();', + '', + ' if (!opmlText || !opmlText.toLowerCase().includes("