From 35f57e0d3e8e653b6ba7fb8da6dc7b0af7a7cbeb Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 28 Mar 2026 00:29:59 -0400 Subject: [PATCH] feat: add GET /api/briefing/news unified news endpoint Co-Authored-By: Claude Sonnet 4.6 --- src/fabledassistant/routes/briefing.py | 55 ++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/fabledassistant/routes/briefing.py b/src/fabledassistant/routes/briefing.py index eba9411..6089c08 100644 --- a/src/fabledassistant/routes/briefing.py +++ b/src/fabledassistant/routes/briefing.py @@ -322,3 +322,58 @@ async def delete_rss_reaction(item_id: int): await session.commit() return jsonify({"ok": True}) + + +@briefing_bp.route("/news", methods=["GET"]) +@_REQUIRE +async def list_news(): + """Return recent RSS articles with optional feed filter and pagination. + + Query params: + days — lookback window (default 2, max 90) + limit — items per page (default 40, max 100) + offset — pagination offset (default 0) + feed_id — optional integer filter by feed + """ + from sqlalchemy import text as _text + + days = min(int(request.args.get("days", 2)), 90) + limit = min(int(request.args.get("limit", 40)), 100) + offset = max(int(request.args.get("offset", 0)), 0) + feed_id = request.args.get("feed_id", type=int) + + async with async_session() as session: + result = await session.execute( + _text(""" + SELECT + i.id, i.title, i.url, i.content, i.published_at, + i.topics, f.title AS feed_title, + r.reaction + FROM rss_items i + JOIN rss_feeds f ON f.id = i.feed_id + LEFT JOIN rss_item_reactions r + ON r.rss_item_id = i.id AND r.user_id = :uid + WHERE f.user_id = :uid + AND (:feed_id IS NULL OR f.id = :feed_id) + AND i.published_at >= NOW() - make_interval(days => :days) + ORDER BY i.published_at DESC NULLS LAST + LIMIT :limit OFFSET :offset + """).bindparams(uid=g.user.id, days=days, limit=limit, + offset=offset, feed_id=feed_id) + ) + rows = result.mappings().all() + + items = [ + { + "id": r["id"], + "title": r["title"], + "url": r["url"], + "snippet": (r["content"] or "")[:300], + "published_at": r["published_at"].isoformat() if r["published_at"] else None, + "topics": r["topics"] or [], + "source": r["feed_title"], + "reaction": r["reaction"], + } + for r in rows + ] + return jsonify({"items": items, "offset": offset, "limit": limit})