Add WebSocket announce listener to both IRC bot plugins

- Limnoria: add wsUrl, announceChannel config; __init__, die, _ws_listener
- Sopel: add ws_url, announce_channel config; setup/shutdown, _ws_listener
- Feature parity: subscribe to WS, receive announce msgs, send to IRC channel
- Deferred websocket-client import to avoid load failure if not installed

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-12 07:22:49 -04:00
parent a7849e6cd9
commit e31a9503db
3 changed files with 154 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import json
import logging
import threading
import urllib.error
import urllib.request
from datetime import datetime
@@ -21,10 +22,79 @@ class NtrPlaylistSection(types.StaticSection):
admin_token = types.ValidatedAttribute("admin_token", default="")
admin_nicks = types.ListAttribute("admin_nicks")
display_timezone = types.ValidatedAttribute("display_timezone", default="America/New_York")
ws_url = types.ValidatedAttribute("ws_url", default="ws://127.0.0.1:8000/ws/announce")
announce_channel = types.ValidatedAttribute("announce_channel", default="#sewerchat")
_ws_stop = None
_ws_thread = None
def setup(bot):
global _ws_stop, _ws_thread
bot.settings.define_section("ntr_playlist", NtrPlaylistSection)
_ws_stop = threading.Event()
_ws_thread = threading.Thread(target=_ws_listener, args=(bot,), daemon=True)
_ws_thread.start()
def shutdown(bot):
global _ws_stop
if _ws_stop:
_ws_stop.set()
def _ws_listener(bot):
import websocket
backoff = 5
max_backoff = 60
while not _ws_stop.is_set():
ws_url = bot.settings.ntr_playlist.ws_url
token = bot.settings.ntr_playlist.admin_token
channel = bot.settings.ntr_playlist.announce_channel
if not ws_url or not token:
LOGGER.warning("ws_url or admin_token not configured, WS listener sleeping")
_ws_stop.wait(30)
continue
ws = None
try:
ws = websocket.WebSocket()
ws.connect(ws_url, timeout=10)
ws.send(json.dumps({"type": "subscribe", "token": token}))
LOGGER.info("Connected to announce WebSocket at %s", ws_url)
backoff = 5
while not _ws_stop.is_set():
ws.settimeout(5)
try:
raw = ws.recv()
if not raw:
break
data = json.loads(raw)
if data.get("type") == "announce" and "message" in data:
bot.say(data["message"], channel)
LOGGER.info("Announced to %s: %s", channel, data["message"])
except websocket.WebSocketTimeoutException:
continue
except websocket.WebSocketConnectionClosedException:
break
except Exception:
LOGGER.exception("WS listener error")
finally:
if ws:
try:
ws.close()
except Exception:
pass
if not _ws_stop.is_set():
LOGGER.info("Reconnecting in %ds", backoff)
_ws_stop.wait(backoff)
backoff = min(backoff * 2, max_backoff)
def configure(config):