2026-02-21 02:37:37 +00:00
|
|
|
"""Bridge monop binary to IRC via subprocess pipes."""
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
sys.stdout.reconfigure(line_buffering=True)
|
|
|
|
|
|
|
|
|
|
import socket
|
|
|
|
|
import subprocess
|
|
|
|
|
import threading
|
|
|
|
|
import time
|
|
|
|
|
import sys
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
HOST = "127.0.0.1"
|
|
|
|
|
PORT = 6667
|
|
|
|
|
NICK = "monop"
|
|
|
|
|
CHANNEL = "#monop"
|
|
|
|
|
PREFIX = "."
|
|
|
|
|
MONOP_BIN = "/tmp/monop-irc/monop/monop"
|
|
|
|
|
CARDS_FILE = "/tmp/monop-irc/monop/cards.pck"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class IRCBridge:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
|
self.proc = None
|
|
|
|
|
self.buffer = ""
|
|
|
|
|
|
|
|
|
|
def irc_send(self, line):
|
|
|
|
|
self.sock.sendall((line + "\r\n").encode())
|
|
|
|
|
|
|
|
|
|
def irc_say(self, msg):
|
|
|
|
|
self.irc_send(f"PRIVMSG {CHANNEL} :{msg}")
|
|
|
|
|
|
|
|
|
|
def connect_irc(self):
|
|
|
|
|
self.sock.connect((HOST, PORT))
|
|
|
|
|
self.irc_send(f"NICK {NICK}")
|
|
|
|
|
self.irc_send(f"USER {NICK} 0 * :{NICK}")
|
|
|
|
|
# Wait for registration
|
|
|
|
|
while True:
|
|
|
|
|
data = self.sock.recv(4096)
|
|
|
|
|
if not data:
|
|
|
|
|
raise ConnectionError("IRC connection closed during registration")
|
|
|
|
|
self.buffer += data.decode("utf-8", errors="replace")
|
|
|
|
|
for line in self.buffer.split("\r\n"):
|
|
|
|
|
if line.startswith("PING"):
|
|
|
|
|
self.irc_send("PONG" + line[4:])
|
|
|
|
|
if "ERROR" in line:
|
|
|
|
|
print(f"[bridge] Server error: {line}")
|
|
|
|
|
if " 001 " in self.buffer:
|
|
|
|
|
break
|
|
|
|
|
self.irc_send(f"JOIN {CHANNEL}")
|
|
|
|
|
time.sleep(0.5)
|
|
|
|
|
self.buffer = ""
|
|
|
|
|
self.irc_say("Shall We Play A Game?")
|
|
|
|
|
self.irc_say(f'Prefix your commands with a dot "."')
|
|
|
|
|
print(f"[bridge] Connected to IRC as {NICK} in {CHANNEL}")
|
|
|
|
|
|
|
|
|
|
def start_monop(self):
|
|
|
|
|
env = os.environ.copy()
|
|
|
|
|
self.proc = subprocess.Popen(
|
|
|
|
|
[MONOP_BIN],
|
|
|
|
|
stdin=subprocess.PIPE,
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.STDOUT,
|
|
|
|
|
env=env,
|
|
|
|
|
cwd=os.path.dirname(MONOP_BIN),
|
|
|
|
|
)
|
|
|
|
|
# Read monop stdout -> IRC
|
|
|
|
|
t = threading.Thread(target=self._monop_reader, daemon=True)
|
|
|
|
|
t.start()
|
|
|
|
|
print("[bridge] monop process started")
|
|
|
|
|
|
|
|
|
|
def _monop_reader(self):
|
|
|
|
|
"""Read lines from monop stdout and send to IRC."""
|
|
|
|
|
for raw in self.proc.stdout:
|
|
|
|
|
line = raw.decode("utf-8", errors="replace").rstrip("\n\r")
|
|
|
|
|
if line:
|
|
|
|
|
self.irc_say(line)
|
|
|
|
|
print(f"[monop -> irc] {line}")
|
|
|
|
|
|
|
|
|
|
def _feed_monop(self, nick, text):
|
|
|
|
|
"""Feed input to monop stdin as 'nick text'."""
|
|
|
|
|
feed = f"{nick} {text}\n"
|
|
|
|
|
print(f"[irc -> monop] {feed.rstrip()}")
|
|
|
|
|
try:
|
|
|
|
|
self.proc.stdin.write(feed.encode())
|
|
|
|
|
self.proc.stdin.flush()
|
|
|
|
|
except BrokenPipeError:
|
|
|
|
|
print("[bridge] monop process died, restarting...")
|
|
|
|
|
self.start_monop()
|
|
|
|
|
|
2026-02-21 11:33:28 +00:00
|
|
|
def _wait_for_players(self):
|
|
|
|
|
"""Wait until at least one other user joins the channel before starting."""
|
|
|
|
|
print(f"[bridge] Waiting for players to join {CHANNEL}...")
|
|
|
|
|
while True:
|
|
|
|
|
data = self.sock.recv(4096)
|
|
|
|
|
if not data:
|
|
|
|
|
raise ConnectionError("IRC connection lost while waiting")
|
|
|
|
|
self.buffer += data.decode("utf-8", errors="replace")
|
|
|
|
|
while "\r\n" in self.buffer:
|
|
|
|
|
line, self.buffer = self.buffer.split("\r\n", 1)
|
|
|
|
|
if line.startswith("PING"):
|
|
|
|
|
self.irc_send("PONG" + line[4:])
|
|
|
|
|
# Detect JOIN from someone other than us
|
|
|
|
|
m = re.match(r":(\S+?)!\S+ JOIN", line)
|
|
|
|
|
if m and m.group(1) != NICK:
|
|
|
|
|
print(f"[bridge] {m.group(1)} joined — starting game")
|
|
|
|
|
return
|
|
|
|
|
|
2026-02-21 11:54:53 +00:00
|
|
|
def _process_buffer(self):
|
|
|
|
|
"""Process any complete lines in the buffer."""
|
|
|
|
|
while "\r\n" in self.buffer:
|
|
|
|
|
line, self.buffer = self.buffer.split("\r\n", 1)
|
|
|
|
|
if line.startswith("PING"):
|
|
|
|
|
self.irc_send("PONG" + line[4:])
|
|
|
|
|
continue
|
|
|
|
|
m = re.match(r":(\S+?)!(\S+?) PRIVMSG (\S+) :(.+)", line)
|
|
|
|
|
if m:
|
|
|
|
|
sender_nick = m.group(1)
|
|
|
|
|
target = m.group(3)
|
|
|
|
|
msg = m.group(4)
|
|
|
|
|
if target == CHANNEL and sender_nick != NICK:
|
|
|
|
|
if msg.startswith(PREFIX):
|
|
|
|
|
cmd = msg[len(PREFIX):]
|
|
|
|
|
self._feed_monop(sender_nick, cmd)
|
|
|
|
|
|
2026-02-21 02:37:37 +00:00
|
|
|
def run(self):
|
|
|
|
|
self.connect_irc()
|
2026-02-21 11:33:28 +00:00
|
|
|
self._wait_for_players()
|
2026-02-21 02:37:37 +00:00
|
|
|
self.start_monop()
|
|
|
|
|
|
2026-02-21 11:54:53 +00:00
|
|
|
# Process any lines buffered during _wait_for_players
|
|
|
|
|
self._process_buffer()
|
|
|
|
|
|
2026-02-21 02:37:37 +00:00
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
data = self.sock.recv(4096)
|
|
|
|
|
if not data:
|
|
|
|
|
break
|
|
|
|
|
self.buffer += data.decode("utf-8", errors="replace")
|
2026-02-21 11:54:53 +00:00
|
|
|
self._process_buffer()
|
2026-02-21 02:37:37 +00:00
|
|
|
except Exception as e:
|
|
|
|
|
print(f"[bridge] error: {e}")
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
bridge = IRCBridge()
|
|
|
|
|
bridge.run()
|