From cbc367b397a8b51e1248896ef8f231634d4aefeb Mon Sep 17 00:00:00 2001 From: Dan Date: Sun, 14 Jun 2026 22:48:52 +0800 Subject: [PATCH] Initial commit --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++ components.jsx | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++ proxy.js | 59 ++++++++++++++++++++++++++++++++++++++++ widget.js | 9 +++++++ 4 files changed, 204 insertions(+) create mode 100644 README.md create mode 100644 components.jsx create mode 100644 proxy.js create mode 100644 widget.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..47ec76c --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# FreshRSS List Widget for Homepage + +A custom widget for [gethomepage.dev](https://gethomepage.dev) that displays the latest RSS feed items from your [FreshRSS](https://freshrss.org/) subscriptions. + +## Why? + +The built-in FreshRSS widget for Homepage only shows the **unread count**. This widget goes a step further by listing the actual articles from your reading list (or a specific category) directly on your dashboard. + +## Features + +- Lists recent RSS items with title, date, author, and source feed. +- Supports filtering by FreshRSS category/label. +- Configurable number of items to display. +- Links directly to the article. + +## Prerequisites + +- A running FreshRSS instance with the **Google Reader API** enabled. + - In FreshRSS: **Settings → Profile → API management** → Enable API and set your API password. + - Ensure the Google Reader API endpoint (`api/greader.php`) is accessible. + +## Installation + +1. Copy the `freshrsslist` folder into your Homepage widgets directory (e.g., `src/widgets/` or your custom widgets path). +2. If Homepage requires widget registration, import and add it to your widget registry. + +## Configuration + +Add the widget to your `services.yaml` (or `widgets.yaml`): + +```yaml +- FreshRSS: + href: https://your-freshrss-instance.com + widget: + type: freshrsslist + url: https://your-freshrss-instance.com + username: your_username + password: your_api_password # Not your regular login password + category: Tech # Optional: filter by FreshRSS label + limit: 5 # Optional: number of items (default: 5) +``` + +## Options +| Field | Required | Description | +|------------|----------|------------------------------------------------------------------------| +| `url` | Yes | Base URL of your FreshRSS instance. | +| `username` | Yes | Your FreshRSS username. | +| `password` | Yes | Your FreshRSS **API password** (set in Profile → API management). | +| `category` | No | A FreshRSS category/label to filter items. Omit to show reading list. | +| `limit` | No | Number of items to display. Default: `5`. | + +## How It Works + +The widget authenticates with FreshRSS via the Google Reader API (`ClientLogin`) and fetches the stream contents. It then maps the API response to a clean list of articles rendered inside a Homepage service container. + +## File Structure + +- `widget.js` — Defines the widget endpoint and proxy handler. +- `proxy.js` — Handles FreshRSS authentication and API requests. +- `components.jsx` — The React component that renders the RSS list. + +## License +MIT diff --git a/components.jsx b/components.jsx new file mode 100644 index 0000000..5c220a3 --- /dev/null +++ b/components.jsx @@ -0,0 +1,73 @@ +import { useTranslation } from "next-i18next"; +import Container from "components/services/widget/container"; +import useWidgetAPI from "utils/proxy/use-widget-api"; +export default function Component({ service }) { + const { widget } = service; + const { data, error } = useWidgetAPI(widget, "items"); + if (error) { + return ; + } + if (!data) { + return ( + + {" "} +
+ {" "} +
{" "} + {[1, 2, 3, 4].map((i) => ( +
+ {" "} +
{" "} +
{" "} +
+ ))}{" "} +
{" "} + + ); + } + const limit = parseInt(widget.limit, 10) || 5; + const items = data?.items?.slice(0, limit) || []; + const fmtDate = (unix) => { + if (!unix) return ""; + return new Date(unix * 1000).toLocaleDateString(undefined, { + month: "short", + day: "numeric", + }); + }; + return ( + + {" "} +
+ {" "} +
+ {" "} + {widget.category || "FreshRSS"}{" "} +
{" "} +
    + {" "} + {items.map((item) => ( +
  • + {" "} + + {" "} + {item.title}{" "} + {" "} +
    + {" "} + {fmtDate(item.created_on_time)}{" "} + {item.author && · {item.author}}{" "} + {item.feed_name && · {item.feed_name}}{" "} +
    {" "} +
  • + ))}{" "} +
{" "} +
{" "} +
+ ); +} diff --git a/proxy.js b/proxy.js new file mode 100644 index 0000000..d6e67c1 --- /dev/null +++ b/proxy.js @@ -0,0 +1,59 @@ +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; +const logger = createLogger("freshrsslist"); +export default async function freshrsslistProxyHandler(req, res) { + try { + const { group, service } = req.query; + const serviceWidget = await getServiceWidget(group, service); + if (!serviceWidget) { + return res.status(400).json({ error: "Widget not found" }); + } + const { url, username, password, category, limit } = serviceWidget; + const itemLimit = parseInt(limit, 10) || 5; + if (!url || !username || !password) { + return res.status(400).json({ error: "Missing required fields" }); + } + const baseUrl = url.replace(/\/+$/, ""); + // 1) ClientLogin to get Auth token + const loginUrl = `${baseUrl}/api/greader.php/accounts/ClientLogin`; + const loginRes = await fetch(loginUrl, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: `Email=${encodeURIComponent(username)}&Passwd=${encodeURIComponent(password)}`, + }); + const loginText = await loginRes.text(); + const authMatch = loginText.match(/Auth=([^\n]+)/); + if (!authMatch) { + logger.error(`FreshRSS ClientLogin failed: ${loginText}`); + return res.status(401).json({ error: "FreshRSS auth failed" }); + } + const authToken = authMatch[1]; + // 2) Fetch reading list with GoogleLogin auth header + const streamPath = category + ? `user/-/label/${encodeURIComponent(category)}` + : "user/-/state/com.google/reading-list"; + const targetUrl = `${baseUrl}/api/greader.php/reader/api/0/stream/contents/${streamPath}?n=${itemLimit}&output=json&r=n`; + const response = await fetch(targetUrl, { + headers: { Authorization: `GoogleLogin auth=${authToken}` }, + }); + if (!response.ok) { + logger.error(`FreshRSS returned ${response.status}`); + return res + .status(response.status) + .json({ error: `HTTP ${response.status}` }); + } + const json = await response.json(); + const items = (json.items || []).map((item) => ({ + id: item.id || item.timestampUsec, + title: item.title || "Untitled", + url: item.alternate?.[0]?.href || item.canonical?.[0]?.href || "", + author: item.author || "", + created_on_time: item.published || Math.floor(Date.now() / 1000), + feed_name: item.origin?.title || "", + })); + return res.status(200).json({ items }); + } catch (err) { + logger.error(err); + return res.status(500).json({ error: err.message }); + } +} diff --git a/widget.js b/widget.js new file mode 100644 index 0000000..353d757 --- /dev/null +++ b/widget.js @@ -0,0 +1,9 @@ +import freshrsslistProxyHandler from "./proxy"; +const widget = { + api: "{url}/{endpoint}", + proxyHandler: freshrsslistProxyHandler, + mappings: { + items: { endpoint: "api/greader.php/reader/api/0/stream/contents" }, + }, +}; +export default widget;