Rewrite Miniflux plugin to use requests, add filter and bookmark tools
Drop the miniflux pip client in favour of requests (already in the container). Add update_feed_filters (keeplist/blocklist regex), toggle_bookmark, get_entry (full content), and category filtering. Remove the pip install step from Ansible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,3 +14,27 @@ def register(ctx):
|
||||
schema=schemas.GET_UNREAD_ENTRIES,
|
||||
handler=tools.get_unread_entries,
|
||||
)
|
||||
ctx.register_tool(
|
||||
name="get_entry",
|
||||
toolset="miniflux",
|
||||
schema=schemas.GET_ENTRY,
|
||||
handler=tools.get_entry,
|
||||
)
|
||||
ctx.register_tool(
|
||||
name="toggle_bookmark",
|
||||
toolset="miniflux",
|
||||
schema=schemas.TOGGLE_BOOKMARK,
|
||||
handler=tools.toggle_bookmark,
|
||||
)
|
||||
ctx.register_tool(
|
||||
name="update_feed_filters",
|
||||
toolset="miniflux",
|
||||
schema=schemas.UPDATE_FEED_FILTERS,
|
||||
handler=tools.update_feed_filters,
|
||||
)
|
||||
ctx.register_tool(
|
||||
name="mark_as_read",
|
||||
toolset="miniflux",
|
||||
schema=schemas.MARK_AS_READ,
|
||||
handler=tools.mark_as_read,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
name: miniflux
|
||||
version: 1.0.0
|
||||
description: Read feeds and entries from the local Miniflux RSS reader
|
||||
version: 2.0.0
|
||||
description: Read and manage feeds and entries from the local Miniflux RSS reader
|
||||
provides_tools:
|
||||
- list_feeds
|
||||
- get_unread_entries
|
||||
- get_entry
|
||||
- toggle_bookmark
|
||||
- update_feed_filters
|
||||
- mark_as_read
|
||||
|
||||
@@ -24,6 +24,10 @@ GET_UNREAD_ENTRIES = {
|
||||
"type": "integer",
|
||||
"description": "Filter to a specific feed. Omit for all feeds.",
|
||||
},
|
||||
"category_id": {
|
||||
"type": "integer",
|
||||
"description": "Filter to a specific category. Omit for all categories.",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of entries to return. Defaults to 20.",
|
||||
@@ -32,3 +36,81 @@ GET_UNREAD_ENTRIES = {
|
||||
"required": [],
|
||||
},
|
||||
}
|
||||
|
||||
GET_ENTRY = {
|
||||
"name": "get_entry",
|
||||
"description": (
|
||||
"Get a single entry from Miniflux by ID, including its full content. "
|
||||
"Use this to read an article's text."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entry_id": {
|
||||
"type": "integer",
|
||||
"description": "The entry ID to retrieve.",
|
||||
},
|
||||
},
|
||||
"required": ["entry_id"],
|
||||
},
|
||||
}
|
||||
|
||||
TOGGLE_BOOKMARK = {
|
||||
"name": "toggle_bookmark",
|
||||
"description": "Toggle the bookmark/star status of a Miniflux entry.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entry_id": {
|
||||
"type": "integer",
|
||||
"description": "The entry ID to bookmark or unbookmark.",
|
||||
},
|
||||
},
|
||||
"required": ["entry_id"],
|
||||
},
|
||||
}
|
||||
|
||||
UPDATE_FEED_FILTERS = {
|
||||
"name": "update_feed_filters",
|
||||
"description": (
|
||||
"Update the keep or block filter rules on a Miniflux feed. "
|
||||
"Rules are case-insensitive regexes matched against entry titles and URLs. "
|
||||
"keeplist_rules: only entries matching are kept. "
|
||||
"blocklist_rules: entries matching are excluded. "
|
||||
"Pass an empty string to clear a rule."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"feed_id": {
|
||||
"type": "integer",
|
||||
"description": "The feed ID to update.",
|
||||
},
|
||||
"keeplist_rules": {
|
||||
"type": "string",
|
||||
"description": "Regex pattern. Only matching entries are kept. Omit to leave unchanged.",
|
||||
},
|
||||
"blocklist_rules": {
|
||||
"type": "string",
|
||||
"description": "Regex pattern. Matching entries are excluded. Omit to leave unchanged.",
|
||||
},
|
||||
},
|
||||
"required": ["feed_id"],
|
||||
},
|
||||
}
|
||||
|
||||
MARK_AS_READ = {
|
||||
"name": "mark_as_read",
|
||||
"description": "Mark one or more Miniflux entries as read.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entry_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
"description": "List of entry IDs to mark as read.",
|
||||
},
|
||||
},
|
||||
"required": ["entry_ids"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
_PLUGIN_DIR = Path(__file__).parent
|
||||
with open(_PLUGIN_DIR / "config.json") as _f:
|
||||
_CONFIG = json.loads(_f.read())
|
||||
|
||||
MINIFLUX_BASE = _CONFIG.get("base_url", "http://miniflux:8080")
|
||||
MINIFLUX_KEY = _CONFIG.get("api_key", "")
|
||||
_BASE = _CONFIG.get("base_url", "http://miniflux:8080").rstrip("/")
|
||||
_HEADERS = {"X-Auth-Token": _CONFIG.get("api_key", "")}
|
||||
|
||||
|
||||
def _request(path):
|
||||
"""Make an authenticated GET request to the Miniflux API."""
|
||||
url = f"{MINIFLUX_BASE}/v1{path}"
|
||||
req = urllib.request.Request(url, headers={"X-Auth-Token": MINIFLUX_KEY})
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
return json.loads(resp.read())
|
||||
def _get(path, **params):
|
||||
resp = requests.get(f"{_BASE}/v1{path}", headers=_HEADERS, params=params, timeout=10)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def _put(path, body):
|
||||
resp = requests.put(f"{_BASE}/v1{path}", headers=_HEADERS, json=body, timeout=10)
|
||||
resp.raise_for_status()
|
||||
return resp
|
||||
|
||||
|
||||
def list_feeds(args: dict, **kwargs) -> str:
|
||||
try:
|
||||
feeds = _request("/feeds")
|
||||
counters = _request("/feeds/counters")
|
||||
unreads = counters.get("reads", {}) # keyed by feed id
|
||||
unread_map = counters.get("unreads", {})
|
||||
feeds = _get("/feeds")
|
||||
counters = _get("/feeds/counters")
|
||||
unreads = counters.get("unreads", {})
|
||||
|
||||
result = []
|
||||
for f in feeds:
|
||||
fid = str(f["id"])
|
||||
result.append({
|
||||
"id": f["id"],
|
||||
"title": f["title"],
|
||||
"site_url": f.get("site_url", ""),
|
||||
"unread": unread_map.get(fid, 0),
|
||||
"category": f.get("category", {}).get("title", ""),
|
||||
"unread": unreads.get(str(f["id"]), 0),
|
||||
})
|
||||
result.sort(key=lambda x: x["unread"], reverse=True)
|
||||
return json.dumps({"feeds": result, "total": len(result)})
|
||||
@@ -43,15 +46,20 @@ def list_feeds(args: dict, **kwargs) -> str:
|
||||
|
||||
def get_unread_entries(args: dict, **kwargs) -> str:
|
||||
try:
|
||||
limit = args.get("limit", 20)
|
||||
feed_id = args.get("feed_id")
|
||||
|
||||
if feed_id:
|
||||
path = f"/feeds/{feed_id}/entries?status=unread&limit={limit}&direction=desc&order=published_at"
|
||||
params = {
|
||||
"status": "unread",
|
||||
"limit": args.get("limit", 20),
|
||||
"direction": "desc",
|
||||
"order": "published_at",
|
||||
}
|
||||
if args.get("feed_id"):
|
||||
path = f"/feeds/{args['feed_id']}/entries"
|
||||
elif args.get("category_id"):
|
||||
path = f"/categories/{args['category_id']}/entries"
|
||||
else:
|
||||
path = f"/entries?status=unread&limit={limit}&direction=desc&order=published_at"
|
||||
path = "/entries"
|
||||
|
||||
data = _request(path)
|
||||
data = _get(path, **params)
|
||||
entries = []
|
||||
for e in data.get("entries", []):
|
||||
entries.append({
|
||||
@@ -59,6 +67,7 @@ def get_unread_entries(args: dict, **kwargs) -> str:
|
||||
"title": e["title"],
|
||||
"url": e.get("url", ""),
|
||||
"feed": e.get("feed", {}).get("title", ""),
|
||||
"category": e.get("feed", {}).get("category", {}).get("title", ""),
|
||||
"author": e.get("author", ""),
|
||||
"published_at": e.get("published_at", ""),
|
||||
"reading_time": e.get("reading_time", 0),
|
||||
@@ -69,3 +78,67 @@ def get_unread_entries(args: dict, **kwargs) -> str:
|
||||
})
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
|
||||
def get_entry(args: dict, **kwargs) -> str:
|
||||
try:
|
||||
entry = _get(f"/entries/{args['entry_id']}")
|
||||
return json.dumps({
|
||||
"id": entry["id"],
|
||||
"title": entry["title"],
|
||||
"url": entry.get("url", ""),
|
||||
"author": entry.get("author", ""),
|
||||
"feed": entry.get("feed", {}).get("title", ""),
|
||||
"category": entry.get("feed", {}).get("category", {}).get("title", ""),
|
||||
"published_at": entry.get("published_at", ""),
|
||||
"reading_time": entry.get("reading_time", 0),
|
||||
"content": entry.get("content", ""),
|
||||
})
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
|
||||
def toggle_bookmark(args: dict, **kwargs) -> str:
|
||||
try:
|
||||
_put(f"/entries/{args['entry_id']}/bookmark", {})
|
||||
return json.dumps({"ok": True, "entry_id": args["entry_id"]})
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
|
||||
def update_feed_filters(args: dict, **kwargs) -> str:
|
||||
try:
|
||||
feed_id = args["feed_id"]
|
||||
body = {}
|
||||
if "keeplist_rules" in args:
|
||||
body["keeplist_rules"] = args["keeplist_rules"]
|
||||
if "blocklist_rules" in args:
|
||||
body["blocklist_rules"] = args["blocklist_rules"]
|
||||
if not body:
|
||||
return json.dumps({"error": "Provide keeplist_rules and/or blocklist_rules"})
|
||||
resp = requests.put(
|
||||
f"{_BASE}/v1/feeds/{feed_id}",
|
||||
headers=_HEADERS, json=body, timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
feed = resp.json()
|
||||
return json.dumps({
|
||||
"ok": True,
|
||||
"feed_id": feed["id"],
|
||||
"title": feed["title"],
|
||||
"keeplist_rules": feed.get("keeplist_rules", ""),
|
||||
"blocklist_rules": feed.get("blocklist_rules", ""),
|
||||
})
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
|
||||
def mark_as_read(args: dict, **kwargs) -> str:
|
||||
try:
|
||||
entry_ids = args.get("entry_ids", [])
|
||||
if not entry_ids:
|
||||
return json.dumps({"error": "No entry_ids provided"})
|
||||
_put("/entries", {"entry_ids": entry_ids, "status": "read"})
|
||||
return json.dumps({"ok": True, "marked_read": entry_ids})
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
Reference in New Issue
Block a user