${feedName}
376 |Feed ID: ${feedId}
378 |Build Time: ${lastBuildDate}
379 |Feed Type: ${feedType}
380 | 389 |├── .github ├── FUNDING.yml └── workflows │ ├── docker-image.yml │ └── docker-test.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bun.lockb ├── bunenv.txt ├── dockerfile ├── index.ts ├── models ├── apiconfig.model.ts ├── csstarget.model.ts └── imapconfig.model.ts ├── node └── imap-watch.utility.ts ├── package.json ├── public ├── index.html ├── logo.ico ├── logo.png └── logo.svg ├── tsconfig.json ├── utilities ├── data-handler.utility.ts ├── imap.utility.ts ├── rss-builder.utility.ts ├── security.utility.ts └── suggestion-engine.utility.ts └── workers ├── feed-updater.worker.ts └── imap-feed.worker.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [TBosak] 4 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker Images 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build-and-push: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | packages: write 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v2 20 | 21 | # Login to Docker Hub 22 | - name: Log in to Docker Hub 23 | uses: docker/login-action@v2 24 | with: 25 | registry: docker.io 26 | username: ${{ secrets.DOCKERHUB_USERNAME }} 27 | password: ${{ secrets.DOCKERHUB_TOKEN }} 28 | 29 | # Login to GitHub Container Registry 30 | - name: Log in to GitHub Container Registry 31 | uses: docker/login-action@v2 32 | with: 33 | registry: ghcr.io 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GHCR_TOKEN }} 36 | 37 | - name: Extract version number 38 | id: vars 39 | run: | 40 | VERSION=${{ github.run_number }} 41 | echo "VERSION=$VERSION" >> $GITHUB_ENV 42 | 43 | - name: Build and push Docker images to Docker Hub and GHCR 44 | uses: docker/build-push-action@v4 45 | with: 46 | context: . 47 | push: true 48 | tags: | 49 | docker.io/tbosk/mkfd:latest 50 | docker.io/tbosk/mkfd:${{ env.VERSION }} 51 | ghcr.io/tbosak/mkfd:latest 52 | ghcr.io/tbosak/mkfd:${{ env.VERSION }} 53 | -------------------------------------------------------------------------------- /.github/workflows/docker-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Test Docker Images 2 | 3 | on: 4 | push: 5 | branches: [test] 6 | 7 | jobs: 8 | build-and-push: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | packages: write 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v2 20 | 21 | # Login to Docker Hub 22 | - name: Log in to Docker Hub 23 | uses: docker/login-action@v2 24 | with: 25 | registry: docker.io 26 | username: ${{ secrets.DOCKERHUB_USERNAME }} 27 | password: ${{ secrets.DOCKERHUB_TOKEN }} 28 | 29 | # Login to GitHub Container Registry 30 | - name: Log in to GitHub Container Registry 31 | uses: docker/login-action@v2 32 | with: 33 | registry: ghcr.io 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GHCR_TOKEN }} 36 | 37 | - name: Extract version number 38 | id: vars 39 | run: | 40 | VERSION=${{ github.run_number }} 41 | echo "VERSION=$VERSION" >> $GITHUB_ENV 42 | 43 | - name: Build and push Docker images to Docker Hub and GHCR 44 | uses: docker/build-push-action@v4 45 | with: 46 | context: . 47 | push: true 48 | tags: | 49 | docker.io/tbosk/mkfd:test-latest 50 | docker.io/tbosk/mkfd:test-${{ env.VERSION }} 51 | ghcr.io/tbosak/mkfd:test-latest 52 | ghcr.io/tbosak/mkfd:test-${{ env.VERSION }} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | public/feeds/ 171 | configs/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## 🤝 Contributing to Mkfd 2 | 3 | Thanks for considering a contribution to **Mkfd**! This project turns webpages, email folders, or REST APIs into structured RSS feeds using Bun and Hono. Below are ways you can get involved and help improve the project. 4 | 5 | --- 6 | 7 | ### 🧑💻 Code Contributions 8 | 9 | - **Fix Bugs** 10 | - Help identify and patch issues in feed parsing, scheduling, or UI logic. 11 | - **Complete To-Do Items** 12 | - Check off tasks listed in [`README.md`](https://github.com/TBosak/mkfd#-to-do). 13 | - **Feature Development** 14 | - Propose and implement new features. 15 | - **Improve the Web UI** 16 | - Enhance the `index.html` GUI or improve its responsiveness and user experience. 17 | 18 | ### 📄 Documentation 19 | 20 | - **Improve Existing Docs** 21 | - Expand `README.md` with usage examples and troubleshooting tips. 22 | - **Add New Guides** 23 | - Create tutorials for building feeds from various sources. 24 | - Document how CSS selectors and API mappings work. 25 | 26 | ### 🧪 Testing 27 | 28 | - **Write Tests** 29 | - Add unit/integration tests for utilities and adapters. 30 | - **Report Bugs** 31 | - Open detailed GitHub issues with steps to reproduce problems. 32 | - **Cross-Browser Testing** 33 | - Ensure GUI functionality across different browsers. 34 | 35 | ### 🛠️ DevOps 36 | 37 | - **Improve Docker Setup** 38 | - Optimize the Dockerfile or suggest multi-arch improvements. 39 | - **Create Helm Charts** 40 | - Add Kubernetes deployment files for streamlined hosting. 41 | - Work on TrueNAS app catalog integration. 42 | - **Add CI for Testing** 43 | - Propose GitHub Actions for test and lint automation. 44 | 45 | ### 🌍 Community & Support 46 | 47 | - **Help in Issues or Discussions** 48 | - Answer questions and guide new users. 49 | - **Feature Requests** 50 | - Suggest new functionalities via GitHub issues. 51 | - **Translations** 52 | - Contribute internationalization for UI or docs. 53 | 54 | ### 💡 Ideas & Feedback 55 | 56 | - **Use the App** 57 | - Try building your own feeds and tell us what works or what doesn't. 58 | - **Feedback Matters** 59 | - Open issues for ideas, bugs, or UI/UX improvements. 60 | 61 | --- 62 | 63 | ## 🛠️ Getting Started 64 | 65 | 1. Install dependencies: 66 | ```bash 67 | bun install 68 | ``` 69 | 2. Run the app locally: 70 | ```bash 71 | bun run index.ts --passkey=your_passkey --cookieSecret=your_cookie_secret --encryptionKey=your_encryption_key_here 72 | ``` 73 | 3. Access it via: 74 | ``` 75 | http://localhost:5000/ 76 | ``` 77 | 78 | --- 79 | 80 | ## 🚀 How to Contribute 81 | 82 | 1. **Fork** the repository on GitHub. 83 | 2. **Clone** your fork locally: 84 | ```bash 85 | git clone https://github.com/your-username/mkfd.git 86 | cd mkfd 87 | ``` 88 | 3. **Create a new branch** for your feature or fix: 89 | ```bash 90 | git checkout -b your-feature-name 91 | ``` 92 | 4. **Make your changes**, test them locally, and commit: 93 | ```bash 94 | git commit -am "Add your message here" 95 | ``` 96 | 5. **Push** the branch to your fork: 97 | ```bash 98 | git push origin your-feature-name 99 | ``` 100 | 6. **Open a Pull Request** against the `main` branch on the original repository. 101 | 102 | We’ll review your PR and work with you to get it merged. Welcome aboard ✨ 103 | 104 | Thanks again for helping improve Mkfd 💜 105 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Tim Barani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
6 |
7 |
8 |
Incorrect passkey. Try again.
' 101 | ); 102 | } 103 | } 104 | 105 | if (c.req.path === "/passkey") { 106 | return await next(); 107 | } 108 | 109 | return c.redirect("/passkey"); 110 | }; 111 | 112 | app.use( 113 | "*", 114 | sessionMiddleware({ 115 | store, 116 | encryptionKey: cookieSecret, 117 | expireAfterSeconds: 60 * 60 * 24, 118 | cookieOptions: { 119 | path: "/", 120 | httpOnly: true, 121 | secure: SSL, 122 | sameSite: "lax", 123 | }, 124 | }) 125 | ); 126 | app.use("/*", except("/public/feeds/*", middleware)); 127 | app.use("/public/*", serveStatic({ root: "./" })); 128 | app.use("/configs/*", serveStatic({ root: "./" })); 129 | app.get("/", (ctx) => ctx.html(file("./public/index.html").text())); 130 | app.post("/", async (ctx) => { 131 | const feedId = uuidv4(); 132 | const contentType = ctx.req.header("Content-Type") || ""; 133 | 134 | let body: RecordYour RSS feed is being generated and will update every ${feedConfig.refreshTime} minutes.
234 |Access it at: public/feeds/${feedId}.xml
235 | `); 236 | }); 237 | 238 | app.post("/preview", async (ctx) => { 239 | try { 240 | const jsonData = await ctx.req.json(); 241 | 242 | const extract = (key: string, fallback: any = undefined) => 243 | jsonData[key] ?? fallback; 244 | 245 | const feedType = extract("feedType", "webScraping"); 246 | const cookieNames = extract("cookieNames[]") || []; 247 | const cookieValues = extract("cookieValues[]") || []; 248 | 249 | const cookieString = cookieNames 250 | .map((rawName: string, i: number) => { 251 | const rawValue = cookieValues[i] ?? ""; 252 | const name = rawName.trim(); 253 | const value = rawValue.trim(); 254 | return `${name}=${value}`; 255 | }) 256 | .join("; "); 257 | 258 | const apiConfig: ApiConfig = { 259 | title: extract("feedName", "RSS Feed"), 260 | baseUrl: extract("feedUrl"), 261 | method: extract("apiMethod", "GET"), 262 | route: extract("apiRoute"), 263 | params: JSON.parse(extract("apiParams", "{}")), 264 | headers: JSON.parse(extract("apiHeaders", "{}")), 265 | cookieString: cookieString, 266 | body: JSON.parse(extract("apiBody", "{}")), 267 | advanced: ["on", true, "true"].includes(extract("advanced")), 268 | }; 269 | 270 | const feedConfig = { 271 | feedId: "preview", 272 | feedName: apiConfig.title, 273 | feedType, 274 | config: apiConfig, 275 | article: 276 | feedType === "webScraping" 277 | ? { 278 | iterator: new CSSTarget(extract("itemSelector")), 279 | title: buildCSSTarget("title", jsonData), 280 | description: buildCSSTarget("description", jsonData), 281 | link: buildCSSTarget("link", jsonData), 282 | author: buildCSSTarget("author", jsonData), 283 | date: buildCSSTarget("date", jsonData), 284 | enclosure: buildCSSTarget("enclosure", jsonData), 285 | } 286 | : {}, 287 | apiMapping: 288 | feedType === "api" 289 | ? { 290 | items: extract("apiItemsPath"), 291 | title: extract("apiTitleField"), 292 | description: extract("apiDescriptionField"), 293 | link: extract("apiLinkField"), 294 | date: extract("apiDateField"), 295 | } 296 | : {}, 297 | refreshTime: parseInt(extract("refreshTime", "5")), 298 | reverse: ["on", true, "true"].includes(extract("reverse")), 299 | strict: ["on", true, "true"].includes(extract("strict")), 300 | }; 301 | 302 | const response = await generatePreview(feedConfig); 303 | 304 | return ctx.text(response, 200, { 305 | "Content-Type": "application/rss+xml", 306 | "Cache-Control": "no-cache, no-store, must-revalidate", 307 | }); 308 | } catch (error) { 309 | console.error("Error generating preview:", error); 310 | return ctx.text("Invalid request.", 400); 311 | } 312 | }); 313 | 314 | app.get("/feeds", async (ctx) => { 315 | const files = await readdir(configsDir); 316 | const yamlFiles = files.filter((file) => file.endsWith(".yaml")); 317 | const configs = []; 318 | 319 | // Read feed configurations 320 | for (const file of yamlFiles) { 321 | const filePath = join(configsDir, file); 322 | const yamlContent = await readFile(filePath, "utf8"); 323 | const feedConfig = yaml.load(yamlContent); 324 | configs.push(feedConfig); 325 | } 326 | 327 | // Start building the HTML response 328 | let response = ` 329 | 330 | 331 | 332 | 333 |Feed ID: ${feedId}
378 |Build Time: ${lastBuildDate}
379 |Feed Type: ${feedType}
380 | 389 |")) { 454 | modified = modified.replace("", SG_SCRIPT + "\n"); 455 | } else { 456 | modified += SG_SCRIPT; 457 | } 458 | return modified; 459 | } 460 | 461 | app.get("/proxy", async (ctx) => { 462 | // 1) Read the remote URL from query params 463 | const targetUrl = ctx.req.query("url"); 464 | if (!targetUrl) { 465 | return ctx.text('Missing "url" parameter', 400); 466 | } 467 | 468 | try { 469 | const response = await axios.get(targetUrl); 470 | let html = response.data; 471 | 472 | html = injectSelectorGadget(html); 473 | 474 | return ctx.html(html); 475 | } catch (error) { 476 | console.error("Error fetching remote URL:", error); 477 | return ctx.text("Could not fetch the target URL", 500); 478 | } 479 | }); 480 | 481 | // Passkey entry routes 482 | app.get("/passkey", (c) => { 483 | return c.html(` 484 | 485 | 486 |
487 |
488 | 489 | 490 |
500 |