Files
derp/scripts/irc-test
user 2d00360bc3 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 <noreply@anthropic.com>
2026-02-19 11:26:12 +01:00

143 lines
4.9 KiB
Python
Executable File

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