From 0aefb97844c495c0235b8e2c3c88eda9609fc8ee Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sat, 20 Jun 2026 16:08:04 +0300 Subject: fix: bit of vibed update --- alfred/feed.py | 26 ++++- alfred/helpstrings.py | 8 +- alfred/twt.py | 255 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 283 insertions(+), 6 deletions(-) diff --git a/alfred/feed.py b/alfred/feed.py index b4c1c28..4839181 100644 --- a/alfred/feed.py +++ b/alfred/feed.py @@ -12,6 +12,7 @@ Commands (owner only): import subprocess import os import sys +from datetime import date as _date from sopel import plugin @@ -21,8 +22,9 @@ if _alfred_dir not in _sys.path: _sys.path.insert(0, _alfred_dir) import helpstrings as h -FEEDS_FILE = '/home/ahmed/feeds.txt' -FEED_SCRIPT = '/home/ahmed/scripts/feed-gen.py' +FEEDS_FILE = '/home/ahmed/feeds.txt' +READ_POINT_FILE = '/home/ahmed/feed-read.txt' +FEED_SCRIPT = '/home/ahmed/scripts/feed-gen.py' def _is_owner(bot, trigger): @@ -128,4 +130,24 @@ def cmd_feed(bot, trigger): _refresh(bot) return + if sub == 'read': + if len(args) >= 2: + date_str = args[1].strip() + try: + from datetime import datetime + datetime.strptime(date_str, '%Y-%m-%d') + except ValueError: + bot.say('usage: !feed read [yyyy-mm-dd]') + return + else: + date_str = _date.today().strftime('%Y-%m-%d') + try: + with open(READ_POINT_FILE, 'w') as f: + f.write(date_str + '\n') + bot.say(f'read point set to {date_str}') + _refresh(bot) + except Exception as e: + bot.say(f'error: {e}') + return + bot.say(h.FEED_TOPIC) diff --git a/alfred/helpstrings.py b/alfred/helpstrings.py index 30fc996..f4e094f 100644 --- a/alfred/helpstrings.py +++ b/alfred/helpstrings.py @@ -77,21 +77,25 @@ GIT = { "remove": "git remove: remove a repo, prompts for confirmation. usage: !git remove repo", } -TWF_TOPIC = "twt: last [n] count delete yes no . type !help twt for details" +TWF_TOPIC = "twt: last [n] count delete yes no add users rm . type !help twt for details" TWF = { "last": "twt last: show last n twts, most recent first (default 5). usage: !twt last or !twt last n", "count": "twt count: total number of twts. usage: !twt count", "delete": "twt delete: delete the most recent twt, asks for confirmation. usage: !twt delete", "yes": "twt yes or no: confirm or cancel a pending delete. usage: !twt yes or !twt no", "no": "twt yes or no: confirm or cancel a pending delete. usage: !twt yes or !twt no", + "add": "twt add: provision a wk.fo/twt user and add their token. usage: !twt add ", + "users": "twt users: list all wk.fo/twt users. usage: !twt users", + "rm": "twt rm: remove a wk.fo/twt user token (feed files kept). usage: !twt rm ", } -FEED_TOPIC = "feed: refresh add list rm. type !help feed for details" +FEED_TOPIC = "feed: refresh add list rm read. type !help feed for details" FEED = { "refresh": "feed refresh: regenerate feed.gumx.cc from all followed feeds. usage: !feed refresh", "add": "feed add: follow a feed. usage: !feed add [nick]", "list": "feed list: show all followed feeds. usage: !feed list", "rm": "feed rm: remove a feed by nick or url. usage: !feed rm ", + "read": "feed read: set read point and regenerate. items after the date are new, before are old. usage: !feed read or !feed read yyyy-mm-dd", } TOPIC_MAP = { diff --git a/alfred/twt.py b/alfred/twt.py index 0953d3c..97bf5df 100644 --- a/alfred/twt.py +++ b/alfred/twt.py @@ -16,8 +16,10 @@ Config ([twt] section in sopel config): url - url value written in file header """ +import glob import os -from datetime import datetime +from datetime import datetime, timezone +from pathlib import Path from zoneinfo import ZoneInfo from sopel import plugin @@ -31,7 +33,209 @@ import helpstrings as h _pending_delete = {} -_SUBCOMMANDS = {"last", "count", "delete", "yes", "no"} +_SUBCOMMANDS = {"last", "count", "delete", "yes", "no", "add", "users", "rm"} + +WKFO_TOKENS = '/etc/nginx/twt-tokens.map' +WKFO_TWR = '/var/www/wk.fo/twt' + + +def _load_twt_tokens(): + tokens = {} + try: + with open(WKFO_TOKENS) as f: + for line in f: + line = line.strip() + if not line or line.startswith('#') or not line.startswith('"'): + continue + end = line.index('"', 1) + tok = line[1:end] + rest = line[end+1:].strip().rstrip(';').split('#')[0].strip() + if rest: + tokens[tok] = rest + except FileNotFoundError: + pass + return tokens + + +def _save_twt_tokens(tokens): + with open(WKFO_TOKENS, 'w') as f: + for tok, user in tokens.items(): + f.write(f'"{tok}" {user};\n') + + +def _esc(s): + return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') + + +def _load_twtxt(path): + twts = [] + try: + with open(path) as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + parts = line.split('\t', 1) + if len(parts) == 2: + try: + dt = datetime.fromisoformat(parts[0].replace('Z', '+00:00')) + except ValueError: + dt = datetime.min.replace(tzinfo=timezone.utc) + twts.append((dt, parts[1])) + except FileNotFoundError: + pass + return twts + + +_WKFO_HEAD = """\ + + + + + +{title} + + + +""" + +_WKFO_FOOTER = """\ + + +""" + + +def _build_user_page(username): + twts = _load_twtxt(f'{WKFO_TWR}/~{username}/twtxt.txt') + items = '\n'.join( + f'
  • [{dt.strftime("%Y-%m-%d")}] {_esc(text)}
  • ' + for dt, text in reversed(sorted(twts)) + ) or '
  • (no twts yet)
  • ' + + return ( + _WKFO_HEAD.format(title=f'wk.fo / twt / ~{_esc(username)}') + '\n' + '
    \n' + f'\n' + '
    \n' + '
    \n' + f'

    ~{_esc(username)}

    \n' + f'

    twtxt feed

    \n' + '
    \n' + '

    \n' + '

    \n' + '

    \n' + '

    \n' + '
    \n' + '\n' + '
    \n' + '
      \n' + + items + '\n' + '
    \n' + '
    \n' + + _WKFO_FOOTER + ) + + +def _build_index_page(): + users = sorted( + os.path.basename(d)[1:] + for d in glob.glob(f'{WKFO_TWR}/~*') + if os.path.isdir(d) + ) + + all_twts = [] + for u in users: + for dt, text in _load_twtxt(f'{WKFO_TWR}/~{u}/twtxt.txt'): + all_twts.append((dt, u, text)) + all_twts.sort(reverse=True) + + feed_items = '\n'.join( + f'
  • [{dt.strftime("%Y-%m-%d")}] ' + f'~{_esc(u)}: {_esc(text)}
  • ' + for dt, u, text in all_twts[:50] + ) or '
  • (nothing yet)
  • ' + + user_items = '\n'.join( + f'
  • ~{_esc(u)} ' + f'(twtxt)
  • ' + for u in users + ) or '
  • (no users yet)
  • ' + + return ( + _WKFO_HEAD.format(title='wk.fo / twt') + '\n' + '
    \n' + '\n' + '
    \n' + '
    \n' + '

    twt

    \n' + '

    Hosted twtxt. Short messages from friends.

    \n' + '

    feed

    \n' + '
      \n' + feed_items + '\n
    \n' + '

    users

    \n' + '
      \n' + user_items + '\n
    \n' + '
    \n' + + _WKFO_FOOTER + ) + + +def _wkfo_regen(bot, username=None): + try: + root = Path(WKFO_TWR) + root.mkdir(parents=True, exist_ok=True) + if username: + Path(f'{WKFO_TWR}/~{username}/index.html').write_text( + _build_user_page(username), encoding='utf-8') + Path(f'{WKFO_TWR}/index.html').write_text( + _build_index_page(), encoding='utf-8') + except Exception as e: + bot.say(f'regen error: {e}') class TwtSection(StaticSection): @@ -168,6 +372,53 @@ def cmd_twt(bot, trigger): bot.say("cancelled.") return + # --- wk.fo/twt user management --- + if sub == "users": + tokens = _load_twt_tokens() + if not tokens: + bot.say("no wk.fo/twt users") + return + users = sorted(set(tokens.values())) + bot.say(", ".join(users)) + return + + if sub == "add": + if len(args) < 3: + bot.say("usage: !twt add ") + return + username = args[1] + token = args[2] + tokens = _load_twt_tokens() + if token in tokens: + bot.say(f"token already exists for {tokens[token]}") + return + user_dir = Path(f'{WKFO_TWR}/~{username}') + user_dir.mkdir(parents=True, exist_ok=True) + twtxt = user_dir / 'twtxt.txt' + if not twtxt.exists(): + twtxt.write_text(f'# nick = {username}\n# url = https://wk.fo/twt/~{username}/twtxt.txt\n') + tokens[token] = username + _save_twt_tokens(tokens) + _wkfo_regen(bot, username) + bot.say(f"added {username}") + return + + if sub == "rm": + if len(args) < 2: + bot.say("usage: !twt rm ") + return + target = args[1] + tokens = _load_twt_tokens() + before = len(tokens) + tokens = {t: u for t, u in tokens.items() if u != target} + if len(tokens) == before: + bot.say(f"not found: {target}") + return + _save_twt_tokens(tokens) + _wkfo_regen(bot) + bot.say(f"removed {target} (feed files kept)") + return + # --- post (anything that's not a subcommand) --- ts = _append(bot, raw) bot.say(f"twted [{ts[:10]}]: {raw}") -- cgit v1.2.3