From 2d00360bc3f5a76de87e4ab92a564c857cc4a2de Mon Sep 17 00:00:00 2001 From: user Date: Thu, 19 Feb 2026 11:26:12 +0100 Subject: [PATCH] feat: add minimal IRC test client Connect to IRC, join a channel, send commands, and print bot responses. Waits for the bot's WHO cycle before sending. Co-Authored-By: Claude Opus 4.6 --- scripts/irc-test | 142 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100755 scripts/irc-test diff --git a/scripts/irc-test b/scripts/irc-test new file mode 100755 index 0000000..f9307fc --- /dev/null +++ b/scripts/irc-test @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Minimal IRC test client -- connect, send commands, print responses.""" + +from __future__ import annotations + +import argparse +import selectors +import socket +import ssl +import sys +import time + + +def connect(host: str, port: int, password: str, nick: str, tls: bool) -> socket.socket: + """Connect to IRC server, register, return socket.""" + raw = socket.create_connection((host, port), timeout=10) + if tls: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + sock = ctx.wrap_socket(raw, server_hostname=host) + else: + sock = raw + sock.setblocking(False) + + def send(line: str) -> None: + sock.sendall(f"{line}\r\n".encode()) + + # Register + if password: + send(f"PASS {password}") + send(f"NICK {nick}") + send(f"USER {nick} 0 * :{nick}") + return sock + + +def run(sock: socket.socket, channel: str, nick: str, commands: list[str], + delay: float, wait: float) -> None: + """Join channel, send commands, print PRIVMSG responses.""" + sel = selectors.DefaultSelector() + sel.register(sock, selectors.EVENT_READ) + buf = "" + joined = False + ready = False # True after bot's WHO completes + cmd_idx = 0 + last_send = 0.0 + deadline = 0.0 + + def send(line: str) -> None: + sock.sendall(f"{line}\r\n".encode()) + + # Join channel + time.sleep(1.5) + send(f"JOIN {channel}") + + start = time.monotonic() + timeout = 60.0 # hard cap + + while time.monotonic() - start < timeout: + events = sel.select(timeout=0.5) + for key, _ in events: + data = key.fileobj.recv(4096).decode("utf-8", errors="replace") + if not data: + print("-- connection closed --", file=sys.stderr) + return + buf += data + while "\r\n" in buf: + line, buf = buf.split("\r\n", 1) + # Handle PING + if line.startswith("PING"): + send(f"PONG {line[5:]}") + continue + # Detect our join complete (End of NAMES) + if " 366 " in line: + joined = True + print(f"-- joined {channel} --", file=sys.stderr) + # Detect bot's WHO complete (End of WHO list) + # 315 = RPL_ENDOFWHO -- bot sends WHO on join + if " 315 " in line and joined and not ready: + ready = True + print("-- bot WHO complete, sending commands --", + file=sys.stderr) + # Print PRIVMSG from bot (not from us) + if "PRIVMSG" in line and f":{nick}!" not in line: + parts = line.split(" ", 3) + if len(parts) >= 4: + sender = parts[0].split("!")[0].lstrip(":") + msg = parts[3].lstrip(":") + print(f"\033[2m{sender}>\033[0m {msg}") + + # Send commands after bot finishes WHO + if ready and cmd_idx < len(commands): + now = time.monotonic() + if now - last_send >= delay: + cmd = commands[cmd_idx] + send(f"PRIVMSG {channel} :{cmd}") + print(f"\033[33m --> {cmd}\033[0m", file=sys.stderr) + last_send = now + cmd_idx += 1 + if cmd_idx >= len(commands): + deadline = now + wait + + # Fallback: if no WHO seen after 5s, start sending anyway + if joined and not ready and (time.monotonic() - start) > 8: + ready = True + print("-- WHO timeout, sending anyway --", file=sys.stderr) + + # Exit after wait period following last command + if deadline and time.monotonic() > deadline: + break + + send("QUIT :done") + sel.unregister(sock) + sock.close() + + +def main() -> None: + ap = argparse.ArgumentParser(description="IRC test client") + ap.add_argument("commands", nargs="*", default=["!twitch list", "!yt list"], + help="Commands to send (default: !twitch list, !yt list)") + ap.add_argument("-H", "--host", default="mymx.me") + ap.add_argument("-p", "--port", type=int, default=6697) + ap.add_argument("-P", "--password", default="irc$1234=") + ap.add_argument("-n", "--nick", default="tester") + ap.add_argument("-c", "--channel", default="#derp") + ap.add_argument("--no-tls", action="store_true") + ap.add_argument("-d", "--delay", type=float, default=2.0, + help="Seconds between commands (default: 2)") + ap.add_argument("-w", "--wait", type=float, default=8.0, + help="Seconds to wait after last command (default: 8)") + args = ap.parse_args() + + sock = connect(args.host, args.port, args.password, args.nick, + not args.no_tls) + try: + run(sock, args.channel, args.nick, args.commands, args.delay, args.wait) + except KeyboardInterrupt: + sock.close() + + +if __name__ == "__main__": + main()