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:
@@ -43,3 +43,21 @@ conf.registerGlobalValue(
|
|||||||
"""IANA timezone for displaying dates in IRC (e.g. America/New_York, America/Chicago).""",
|
"""IANA timezone for displaying dates in IRC (e.g. America/New_York, America/Chicago).""",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
conf.registerGlobalValue(
|
||||||
|
NtrPlaylist,
|
||||||
|
"wsUrl",
|
||||||
|
registry.String(
|
||||||
|
"ws://127.0.0.1:8000/ws/announce",
|
||||||
|
"""WebSocket URL for receiving announce commands from the dashboard.""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
conf.registerGlobalValue(
|
||||||
|
NtrPlaylist,
|
||||||
|
"announceChannel",
|
||||||
|
registry.String(
|
||||||
|
"#sewerchat",
|
||||||
|
"""IRC channel to send announce messages to.""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
import re
|
import re
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -121,6 +122,71 @@ _NUMBER_RE = re.compile(r"^!(\d+)$")
|
|||||||
class NtrPlaylist(callbacks.Plugin):
|
class NtrPlaylist(callbacks.Plugin):
|
||||||
"""Query the NtR SoundCloud Fetcher API from IRC."""
|
"""Query the NtR SoundCloud Fetcher API from IRC."""
|
||||||
|
|
||||||
|
def __init__(self, irc):
|
||||||
|
super().__init__(irc)
|
||||||
|
self._irc = irc
|
||||||
|
self._ws_stop = threading.Event()
|
||||||
|
self._ws_thread = threading.Thread(target=self._ws_listener, daemon=True)
|
||||||
|
self._ws_thread.start()
|
||||||
|
|
||||||
|
def die(self):
|
||||||
|
self._ws_stop.set()
|
||||||
|
super().die()
|
||||||
|
|
||||||
|
def _ws_listener(self):
|
||||||
|
import websocket
|
||||||
|
from supybot import ircmsgs
|
||||||
|
|
||||||
|
backoff = 5
|
||||||
|
max_backoff = 60
|
||||||
|
|
||||||
|
while not self._ws_stop.is_set():
|
||||||
|
ws_url = self.registryValue("wsUrl")
|
||||||
|
token = self.registryValue("adminToken")
|
||||||
|
channel = self.registryValue("announceChannel")
|
||||||
|
|
||||||
|
if not ws_url or not token:
|
||||||
|
LOGGER.warning("wsUrl or adminToken not configured, WS listener sleeping")
|
||||||
|
self._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 self._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:
|
||||||
|
msg = ircmsgs.privmsg(channel, data["message"])
|
||||||
|
self._irc.queueMsg(msg)
|
||||||
|
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 self._ws_stop.is_set():
|
||||||
|
LOGGER.info("Reconnecting in %ds", backoff)
|
||||||
|
self._ws_stop.wait(backoff)
|
||||||
|
backoff = min(backoff * 2, max_backoff)
|
||||||
|
|
||||||
def _is_admin(self, nick: str) -> bool:
|
def _is_admin(self, nick: str) -> bool:
|
||||||
admin_nicks = self.registryValue("adminNicks")
|
admin_nicks = self.registryValue("adminNicks")
|
||||||
if not admin_nicks:
|
if not admin_nicks:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -21,10 +22,79 @@ class NtrPlaylistSection(types.StaticSection):
|
|||||||
admin_token = types.ValidatedAttribute("admin_token", default="")
|
admin_token = types.ValidatedAttribute("admin_token", default="")
|
||||||
admin_nicks = types.ListAttribute("admin_nicks")
|
admin_nicks = types.ListAttribute("admin_nicks")
|
||||||
display_timezone = types.ValidatedAttribute("display_timezone", default="America/New_York")
|
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):
|
def setup(bot):
|
||||||
|
global _ws_stop, _ws_thread
|
||||||
bot.settings.define_section("ntr_playlist", NtrPlaylistSection)
|
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):
|
def configure(config):
|
||||||
|
|||||||
Reference in New Issue
Block a user