diff --git a/src/fabledassistant/routes/briefing.py b/src/fabledassistant/routes/briefing.py index 7aa38a6..b950849 100644 --- a/src/fabledassistant/routes/briefing.py +++ b/src/fabledassistant/routes/briefing.py @@ -235,3 +235,89 @@ async def manual_trigger(): text, metadata = await run_compilation(g.user.id, slot, model) msg = await post_message(conv.id, "assistant", text, metadata=metadata) return jsonify({"conversation_id": conv.id, "message_id": msg.id, "slot": slot}) + + +# ── RSS Reactions ────────────────────────────────────────────────────────────── + +@briefing_bp.route("/rss-reactions", methods=["POST"]) +@_REQUIRE +async def upsert_rss_reaction(): + """Upsert a 👍/👎 reaction on an RSS item. Same reaction toggles off; opposite flips.""" + data = await request.get_json() + rss_item_id = data.get("rss_item_id") + reaction = data.get("reaction") + + if not rss_item_id or reaction not in ("up", "down"): + return jsonify({"error": "rss_item_id and reaction ('up'|'down') required"}), 400 + + from sqlalchemy import text as _text + + async with async_session() as session: + # Ownership check: verify item belongs to a feed owned by this user + result = await session.execute( + _text(""" + SELECT i.id FROM rss_items i + JOIN rss_feeds f ON f.id = i.feed_id + WHERE i.id = :item_id AND f.user_id = :uid + """).bindparams(item_id=rss_item_id, uid=g.user.id) + ) + if result.first() is None: + return jsonify({"error": "Not found"}), 404 + + # Check existing reaction + existing = await session.execute( + _text(""" + SELECT id, reaction FROM rss_item_reactions + WHERE user_id = :uid AND rss_item_id = :item_id + """).bindparams(uid=g.user.id, item_id=rss_item_id) + ) + row = existing.first() + + if row is None: + await session.execute( + _text(""" + INSERT INTO rss_item_reactions (user_id, rss_item_id, reaction) + VALUES (:uid, :item_id, :reaction) + """).bindparams(uid=g.user.id, item_id=rss_item_id, reaction=reaction) + ) + action = "created" + elif row.reaction == reaction: + # Toggle off (same reaction clicked again) + await session.execute( + _text(""" + DELETE FROM rss_item_reactions + WHERE user_id = :uid AND rss_item_id = :item_id + """).bindparams(uid=g.user.id, item_id=rss_item_id) + ) + action = "removed" + else: + # Flip to opposite reaction + await session.execute( + _text(""" + UPDATE rss_item_reactions SET reaction = :reaction + WHERE user_id = :uid AND rss_item_id = :item_id + """).bindparams(reaction=reaction, uid=g.user.id, item_id=rss_item_id) + ) + action = "updated" + + await session.commit() + + return jsonify({"ok": True, "action": action}) + + +@briefing_bp.route("/rss-reactions/", methods=["DELETE"]) +@_REQUIRE +async def delete_rss_reaction(item_id: int): + """Explicitly remove a reaction (useful for MCP/external API callers).""" + from sqlalchemy import text as _text + + async with async_session() as session: + await session.execute( + _text(""" + DELETE FROM rss_item_reactions + WHERE user_id = :uid AND rss_item_id = :item_id + """).bindparams(uid=g.user.id, item_id=item_id) + ) + await session.commit() + + return jsonify({"ok": True})