updating the scripts

This commit is contained in:
2026-02-11 13:31:50 +00:00
parent 3a845bf532
commit 36211d28d8
18 changed files with 700 additions and 148 deletions

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
import requests
from datetime import datetime
docstring = """
This script sends a reminder message via a Discord webhook every month to import the latest bank statements into actual.
"""
DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1465643876953100433/P5AIBRJobwkryQesUXBsJx6W91VseUbcPZbiOvGTie5FvUdtiOtP2XPx_jlcaXdr56X1"
if __name__ == "__main__":
actual_link = "https://actual.zainezq.com/"
halifax_link = "https://www.halifax-online.co.uk/personal/logon/login.jsp"
message = f"Reminder to import your latest bank statements into Actual!\n\n"
message += f"🔗 [Actual App]({actual_link})\n"
message += f"🔗 [Halifax Online Banking]({halifax_link})\n"
payload = {
"content": message
}
r = requests.post(DISCORD_WEBHOOK_URL, json=payload, timeout=10)
r.raise_for_status()

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
import requests
from datetime import datetime
docstring = """
This script sends a reminder message via a Discord webhook every month to import the latest bank statements into actual.
"""
DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1461100056580067684/BAxF4ndrugLJX8L6WyUtbzOtqoTR0GyxMWhg6jzkdCy1CC8yLfDb7e5erz_FoMUHmviw"
if __name__ == "__main__":
actual_link = "https://actual.zainezq.com/"
halifax_link = "https://www.halifax-online.co.uk/personal/logon/login.jsp"
message = f"Reminder to import your latest bank statements into Actual!\n\n"
message += f"🔗 [Actual App]({actual_link})\n"
message += f"🔗 [Halifax Online Banking]({halifax_link})\n"
payload = {
"content": message
}
r = requests.post(DISCORD_WEBHOOK_URL, json=payload, timeout=10)
r.raise_for_status()

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env python3
import requests
from datetime import datetime
NIKAH_DAY = datetime(2026, 8, 2)
WALIMAH_DAY = datetime(2026, 8, 9)
docstring = """
This script sends daily countdown reminders via a Discord webhook about
the number of days remaining until two significant events: Nikah Day and
Walimah Day. It calculates the days left for each event and formats a
message to be sent to a specified Discord channel.
"""
DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1461100056580067684/BAxF4ndrugLJX8L6WyUtbzOtqoTR0GyxMWhg6jzkdCy1CC8yLfDb7e5erz_FoMUHmviw"
if __name__ == "__main__":
today = datetime.now()
days_to_nikah = (NIKAH_DAY - today).days
days_to_walimah = (WALIMAH_DAY - today).days
days_to_nikah += 1
days_to_walimah += 1
message = f"📅 Countdown Update:\n\n"
message += f"💍 Nikah Day: {days_to_nikah} days remaining!\n"
message += f"🎉 Walimah Day: {days_to_walimah} days remaining!\n"
payload = {
"content": message
}
r = requests.post(DISCORD_WEBHOOK_URL, json=payload, timeout=10)
r.raise_for_status()

View File

@@ -0,0 +1,303 @@
#!/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]
async def entry_autocomplete(
interaction: discord.Interaction,
current: str,
):
if len(current) < 3:
return []
matches = []
offset = 0
limit = 100
while len(matches) < 25:
r = requests.get(
f"{MINIFLUX_URL}/v1/entries",
headers=HEADERS,
params={
"status": "unread",
"limit": limit,
"offset": offset,
},
timeout=10
)
r.raise_for_status()
entries = r.json()["entries"]
if not entries:
break # no more unread entries
for e in entries:
if current.lower() in e["title"].lower():
matches.append(
app_commands.Choice(
name=e["title"][:100],
value=str(e["id"])
)
)
if len(matches) >= 25:
break
offset += limit
return matches
def get_entry(entry_id: int):
r = requests.get(
f"{MINIFLUX_URL}/v1/entries/{entry_id}",
headers=HEADERS,
timeout=10
)
r.raise_for_status()
return r.json()
async def send_entry_embed(interaction, entry):
current_state[interaction.channel_id] = {
"entry": entry,
"feed_id": entry["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)
@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="rss-find",
description="Search unread RSS articles by title"
)
@app_commands.describe(title="Start typing the article title")
@app_commands.autocomplete(title=entry_autocomplete)
async def rss_find(interaction: discord.Interaction, title: str):
await interaction.response.defer()
entry_id = int(title)
entry = get_entry(entry_id)
await send_entry_embed(interaction, entry)
@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)

View File

@@ -0,0 +1,216 @@
#!/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)

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
import json
import requests
from pathlib import Path
from datetime import datetime
docstring = """
This script reads log files generated by various Org automation scripts,
parses their contents to determine success or failure statuses, and sends
a summary report as an embed message to a specified Discord webhook URL.
It runs on a cron schedule daily at 6 AM.
"""
# -----------------------------
# CONFIG
# -----------------------------
LOG_DIR = Path("/home/zaine/logs")
CALIBRE_LOG = LOG_DIR / "org-books-calibre.log"
ROAM_LOG = LOG_DIR / "org-roam.log"
WEB_LOG = LOG_DIR / "org-web.log"
DISCORD_WEBHOOK_URL = "https://discordapp.com/api/webhooks/1460004143870513464/kRSTsNMXU8AL3Pnj-t1EUKxrLb6cBKROdMVyowX02P46CPmayiB-RHi4UH5g6tCZQf5l"
# -----------------------------
# HELPERS
# -----------------------------
def send_discord_embed(embed: dict):
payload = {
"embeds": [embed]
}
r = requests.post(DISCORD_WEBHOOK_URL, json=payload, timeout=10)
r.raise_for_status()
def parse_calibre_log(path: Path):
successes = 0
failures = 0
last_ts = None
if not path.exists():
return successes, failures, last_ts
for line in path.read_text().splitlines():
try:
entry = json.loads(line)
status = entry.get("status")
ts = entry.get("timestamp")
if status == "success":
successes += 1
else:
failures += 1
if ts:
last_ts = ts
except json.JSONDecodeError:
failures += 1
return successes, failures, last_ts
def parse_roam_log(path: Path):
if not path.exists():
return False
return "✅ Full rebuild complete" in path.read_text()
def parse_web_log(path: Path):
if not path.exists():
return False
text = path.read_text()
return (
"Successfully installed" in text
and "JSON saved to" in text
)
def format_ts(ts):
if not ts:
return "Unknown"
try:
return datetime.fromisoformat(ts).strftime("%Y-%m-%d %H:%M")
except ValueError:
return ts
def delete_and_recreate_log_files():
log_files = [
CALIBRE_LOG,
ROAM_LOG,
WEB_LOG
]
for log_file in log_files:
if log_file.exists():
log_file.unlink()
log_file.touch()
# -----------------------------
# MAIN
# -----------------------------
def main():
cal_ok, cal_fail, cal_last = parse_calibre_log(CALIBRE_LOG)
roam_ok = parse_roam_log(ROAM_LOG)
web_ok = parse_web_log(WEB_LOG)
all_ok = (cal_fail == 0) and roam_ok and web_ok
embed = {
"title": "Org Automation Status",
"color": 0x2ECC71 if all_ok else 0xE74C3C,
"fields": [
{
"name": "Calibre → Org",
"value": (
f"✅ Successes: **{cal_ok}**\n"
f"❌ Failures: **{cal_fail}**\n"
f"⏱ Last run: `{format_ts(cal_last)}`"
),
"inline": False
},
{
"name": "Org-Roam",
"value": "✅ Successful rebuild" if roam_ok else "❌ No success marker found",
"inline": True
},
{
"name": "Org-Web",
"value": "✅ Build + index complete" if web_ok else "❌ Build or index incomplete",
"inline": True
}
],
"footer": {
"text": "Generated automatically from ~/logs"
},
"timestamp": datetime.utcnow().isoformat()
}
send_discord_embed(embed)
delete_and_recreate_log_files()
if __name__ == "__main__":
main()