#!/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()