#!/usr/bin/env python3 import os, random, requests, discord from discord import app_commands from discord.ext import commands from datetime import datetime from pathlib import Path docstring = """ This script connects to a Miniflux RSS feed reader instance and a Discord bot. It allows users to fetch random unread articles from specified RSS feeds via Discord commands, mark them as read in Miniflux, and append their details to an Org-mode file for personal knowledge management. These are the available commands: - /rss [feed]: Fetch a random unread article from the specified feed. - /rss-any: Fetch a random unread article from any feed. - /next: Fetch another article from the same feed. - /done: Mark the current article as read and save it to the Org file. """ # ----------------------------- # CONFIG # ----------------------------- MINIFLUX_URL = "https://miniflux.zainezq.com" MINIFLUX_TOKEN = "Q_tDLF7Ht-aPjub2Xsbn6JCKM1WbNUEDfjHPYAIzT0I=" DISCORD_TOKEN = "MTQ1OTYwNzg2Mjg1Nzk1NzY0OQ.GBTRdl.iqfwD3kMN277z7rhm6lG5EgwSyqyPoCF1t8NVI" ORG_RSS_FILE = "/home/zaine/master-folder/org_files/org_roam/20260111204228-rss_articles.org" GUILD_ID = 1459545736961327116 HEADERS = { "X-Auth-Token": MINIFLUX_TOKEN } intents = discord.Intents.default() bot = commands.Bot(command_prefix="!", intents=intents) tree = bot.tree current_state = {} def append_to_org(entry): org_path = Path(ORG_RSS_FILE) org_path.parent.mkdir(parents=True, exist_ok=True) title = entry["title"].replace("\n", " ").strip() url = entry["url"] feed = entry["feed"]["title"] entry_id = entry["id"] date_read = datetime.now().strftime("%Y-%m-%d %H:%M") org_block = f"""* [[{url}][{title}]] :PROPERTIES: :FEED: {feed} :MINIFLUX_ID: {entry_id} :READ_AT: {date_read} :END: """ with org_path.open("a", encoding="utf-8") as f: f.write(org_block) def get_feeds(): r = requests.get(f"{MINIFLUX_URL}/v1/feeds", headers=HEADERS, timeout=10) r.raise_for_status() return r.json() def get_unread_entries(feed_id=None, limit=100): params = {"status": "unread", "limit": limit} if feed_id: params["feed_id"] = feed_id r = requests.get( f"{MINIFLUX_URL}/v1/entries", headers=HEADERS, params=params, timeout=10 ) r.raise_for_status() return r.json()["entries"] def mark_as_read(entry_id): r = requests.put( f"{MINIFLUX_URL}/v1/entries", headers=HEADERS, json={"entry_ids": [entry_id], "status": "read"}, timeout=10 ) r.raise_for_status() async def send_random_entry( interaction: discord.Interaction, feed_id: int, ): entries = get_unread_entries(feed_id=feed_id) if not entries: await interaction.followup.send( "🎉 No unread articles in this feed!" ) return entry = random.choice(entries) current_state[interaction.channel_id] = { "entry": entry, "feed_id": feed_id } embed = discord.Embed( title=entry["title"], url=entry["url"], description=(entry.get("summary") or "")[:500], color=0x3498DB ) embed.set_footer( text=f"{entry['feed']['title']} • /next or /done" ) await interaction.followup.send(embed=embed) async def feed_autocomplete( interaction: discord.Interaction, current: str, ): feeds = get_feeds() return [ app_commands.Choice(name=f["title"], value=str(f["id"])) for f in feeds if current.lower() in f["title"].lower() ][:25] @tree.command(name="rss", description="Get a random unread RSS article") @app_commands.describe(feed="Which feed to read from") @app_commands.autocomplete(feed=feed_autocomplete) async def rss(interaction: discord.Interaction, feed: str): await interaction.response.defer() await send_random_entry(interaction, feed_id=int(feed)) @tree.command(name="rss-any", description="Get a random unread RSS article from any feed") async def rss_any(interaction: discord.Interaction): await interaction.response.defer() await send_random_entry(interaction, feed_id=None) @tree.command(name="next", description="Get another article from the same feed") async def next_article(interaction: discord.Interaction): state = current_state.get(interaction.channel_id) if not state: await interaction.response.send_message( "⚠️ No active feed. Use /rss first.", ephemeral=True ) return await interaction.response.defer() await send_random_entry( interaction, feed_id=state.get("feed_id") ) @tree.command(name="done", description="Mark the current article as read and save to Org") async def done(interaction: discord.Interaction): state = current_state.get(interaction.channel_id) if not state: await interaction.response.send_message( "⚠️ No article to mark as read.", ephemeral=True ) return if "entry" not in state: await interaction.response.send_message( "⚠️ This article was queued before the last update. Please use /rss again.", ephemeral=True ) del current_state[interaction.channel_id] return entry = state["entry"] mark_as_read(entry["id"]) append_to_org(entry) del current_state[interaction.channel_id] await interaction.response.send_message( "✅ Article marked as read and added to Org" ) @bot.event async def on_ready(): guild = discord.Object(id=GUILD_ID) tree.copy_global_to(guild=guild) await tree.sync(guild=guild) print(f"✅ Logged in as {bot.user} (guild-only sync)") if __name__ == "__main__": if not all([MINIFLUX_URL, MINIFLUX_TOKEN, DISCORD_TOKEN]): raise RuntimeError("Missing environment variables") bot.run(DISCORD_TOKEN)