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>
143 lines
4.9 KiB
Python
Executable File
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()
|