import queue import redis import requests import socket import ssl import threading import time from auth_db import auth_db from utils import strip_bbcode, parse_username from config import ( SERVER, PORT, BOTNICK, NICKSERV_USER, NICKSERV_PASS, API_ENDPOINT, API_TOKEN, VERIFY_ENDPOINT, LOBBY_CHANNEL, MAIN_CHANNEL, ADMIN_CHANNEL, STAFF_CHANNELS, RECONNECT_DELAY, GROUP_NAMES, STAFF_GROUP_IDS, ADMIN_GROUP_IDS, OPER_USER, OPER_PASS, REDIS_PW ) class IRCBot: def __init__(self): self.irc = None self.nickname = BOTNICK self.running = False self.lock = threading.Lock() self.send_queue = queue.Queue() # Connect to IRC server, handle nick conflicts, wait for welcome, and join channel def connect(self): attempt = 0 while True: try: raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.irc = ssl.wrap_socket(raw_sock) self.irc.connect((SERVER, PORT)) self.irc.send(f"NICK {self.nickname}\r\n".encode("utf-8")) self.irc.send(f"USER {self.nickname} 0 * :{self.nickname}\r\n".encode("utf-8")) print("[+] Sent NICK/USER, waiting...") while True: resp = self.irc.recv(2048) if not resp: raise ConnectionError("No data received.") for line in resp.decode(errors="ignore").split("\r\n"): if not line: continue print(f"[IRC RAW] {line}") if line.startswith("PING"): self.irc.send(f"PONG {line.split()[1]}\r\n".encode("utf-8")) elif " 001 " in line: print(f"[+] Connected as {self.nickname}") # Identify with NickServ self.irc.send(f"PRIVMSG NickServ :IDENTIFY {NICKSERV_USER} {NICKSERV_PASS}\r\n".encode("utf-8")) time.sleep(2) # give time for identify to settle # Send OPER command self.irc.send(f"OPER {OPER_USER} {OPER_PASS}\r\n".encode("utf-8")) print("[IRC CMD] Sent OPER") # Optionally ghost the previous nick if not using SASL if self.nickname != BOTNICK: print("[*] Reclaiming original nickname") self.irc.send(f"PRIVMSG NickServ :GHOST {BOTNICK} {NICKSERV_PASS}\r\n".encode("utf-8")) time.sleep(1) self.irc.send(f"NICK {BOTNICK}\r\n".encode("utf-8")) self.nickname = BOTNICK time.sleep(1) self.irc.send(f"PRIVMSG NickServ :IDENTIFY {NICKSERV_USER} {NICKSERV_PASS}\r\n".encode("utf-8")) time.sleep(1) self.irc.send(f"JOIN {LOBBY_CHANNEL}\r\n".encode("utf-8")) return elif " 433 " in line: # Nickname is already in use print(f"[!] Nick {self.nickname} already in use, trying alternate...") attempt += 1 self.nickname = f"{BOTNICK}_{attempt}" self.irc.send(f"NICK {self.nickname}\r\n".encode("utf-8")) break # Break inner loop to restart connection flow with new nick except Exception as e: print(f"[-] IRC connect error: {e}") time.sleep(RECONNECT_DELAY) def start(self): with self.lock: if self.running: return self.running = True print("[+] Starting IRC listener thread") threading.Thread(target=self.listen_loop, daemon=True).start() print("[+] Starting IRC writer thread") threading.Thread(target=self.writer_loop, daemon=True).start() print("[+] Starting Redis subscriber thread") threading.Thread(target=self.redis_subscribe_loop, daemon=True).start() print("[+] IRC + Redis bridge fully initialized") def listen_loop(self): while True: try: data = self.irc.recv(2048).decode(errors="ignore") if not data: raise ConnectionError("Disconnected.") for line in data.strip().split("\r\n"): print(f"[IRC RAW] {line}") # DEBUG FOR NOW if line.startswith("PING"): self.irc.send(f"PONG {line.split()[1]}\r\n".encode("utf-8")) elif "You are now an IRC Operator" in line or "oper" in line.lower(): print("[+] Oper login succeeded") elif "PRIVMSG" in line: self.handle_message(line) elif " PART " in line or " QUIT " in line: self.handle_join_leave(line) except Exception as e: print(f"[-] IRC error: {e}") self.connect() def redis_subscribe_loop(self): r = redis.Redis(host='localhost', port=6969, password=REDIS_PW, db=0) pubsub = r.pubsub() pubsub.subscribe("chatbox_to_irc") print("[REDIS] Subscribed to chatbox_to_irc") for message in pubsub.listen(): if message["type"] == "message": try: text = message["data"].decode() print(f"[REDIS] Got message: {text}") self.send_to_channel(text) except Exception as e: print(f"[-] Redis IRC dispatch error: {e}") # Handle verification messages from users def handle_message(self, line): user = parse_username(line) if user.lower() == self.nickname.lower(): return # Extract target (channel or bot nick) and message try: prefix, trailing = line.split(" :", 1) _, _, target = prefix.strip().split(" ", 2) msg = trailing.strip() except ValueError: print(f"[-] Malformed PRIVMSG line: {line}") return is_private = target.lower() == self.nickname.lower() if msg.startswith("!verify"): if is_private: self.verify_user(user, msg) else: self.send_notice(user, "Please send !verify as a private message.") return # Relay public message if user is verified if not is_private: user_data = auth_db.get_verified_user(user) print(f"[DEBUG] Lookup for user '{user}' returned: {user_data}") if user_data: site_user, irc_key, _ = user_data print(f"[DEBUG] Sending message from {site_user} with token {irc_key}: {msg}") self.api_call(site_user, msg, irc_key) else: print(f"[DEBUG] No verified user found for: {user}") # Handle verification messages from users def verify_user(self, nick, msg): parts = msg.split(" ") if len(parts) != 3: self.send_notice(nick, "Usage: !verify ") return _, site_user, irc_key = parts try: resp = requests.post( VERIFY_ENDPOINT, headers={ "Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json" }, json={ "username": site_user, "irc_key": irc_key }, timeout=10 ) if resp.ok and resp.json().get("success"): group_id = resp.json().get("group_id", 4) group_name = GROUP_NAMES.get(group_id, "Unknown") auth_db.verify_user(nick, site_user, irc_key, group_id) print(f"[DEBUG] Verification success: {site_user} ({group_name}) - group_id={group_id}") self.send_notice(nick, f"Verified as {site_user} ({group_name})") time.sleep(1) # Add delay before sending SAJOIN # Join main lobby print(f"[DEBUG] Sending SAJOIN for {nick} to {MAIN_CHANNEL}") self.send_raw_command(f"SAJOIN {nick} {MAIN_CHANNEL}") # Join staff chats if in staff group if group_id in STAFF_GROUP_IDS: for channel in STAFF_CHANNELS: print(f"[DEBUG] Sending SAJOIN for {nick} to {channel}") self.send_raw_command(f"SAJOIN {nick} {channel}") # Join admin room if in admin group if group_id in ADMIN_GROUP_IDS: self.send_raw_command(f"SAJOIN {nick} {ADMIN_CHANNEL}") else: self.send_notice(nick, f"Verification failed: {resp.json().get('error', 'Unknown')}") except Exception as e: print(f"[-] Error verifying IRC key: {e}") self.send_notice(nick, "Verification failed (server error)") # Send notice to user def send_notice(self, nick, msg): if not self.irc or not self.is_connected(): print("[-] IRC connection is closed or unavailable.") return try: with self.lock: # Ensure thread-safe socket access print(f"[QUEUE] Queuing message: {msg.strip()}") self.send_queue.put(f"NOTICE {nick} :{msg}\r\n") except Exception as e: print(f"[-] Notice failed: {e}") # Send IRC message to channel def send_to_channel(self, msg): if not self.irc or not self.is_connected(): print("[-] IRC connection is closed or unavailable.") return clean = strip_bbcode(msg).replace("\n", " ").replace("\r", " ") try: with self.lock: # Ensure thread-safe socket access print(f"[QUEUE] Queuing message: {clean.strip()}") self.send_queue.put(f"PRIVMSG {LOBBY_CHANNEL} :{clean}\r\n") except ssl.SSLEOFError as e: print(f"[SSL ERROR] IRC connection closed: {e}") self.irc = None # Mark socket dead except Exception as e: print(f"[-] Failed to send: {e}") # Send IRC commands to server def send_raw_command(self, command): if not self.irc or not self.is_connected(): print("[-] IRC connection is closed or unavailable.") return if self.irc: try: with self.lock: # Ensure thread-safe socket access print(f"[QUEUE] Queuing message: {command.strip()}") self.send_queue.put(f"{command}\r\n") print(f"[IRC CMD] {command}") except Exception as e: print(f"[-] Failed to send IRC command: {e}") # API call ChatBridgeController def api_call(self, user, msg, irc_key): try: payload = { "username": user, "irc_key": irc_key, "text": msg, "source": "IRC" } headers = { "Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json", "Accept": "application/json" } print(f"[DEBUG] Posting to API: {API_ENDPOINT}") print(f"[DEBUG] Headers: {headers}") print(f"[DEBUG] Payload: {payload}") resp = requests.post(API_ENDPOINT, headers=headers, json=payload) print(f"[DEBUG] API response status: {resp.status_code}") print(f"[DEBUG] API response body: {resp.text}") except Exception as e: print(f"[-] API call failed: {e}") def handle_join_leave(self, line): user = parse_username(line) auth_db.remove_verified_user(user) # Helper method for checking connection def is_connected(self): try: if not self.irc: return False self.irc.getpeername() return True except Exception: return False # IRC writer method to work with queue def writer_loop(self): print("[WRITER] Writer loop started") while True: try: msg = self.send_queue.get() print(f"[WRITER] Got message from queue: {msg.strip()}") if not self.irc or not self.is_connected(): print("[-] Writer loop: IRC socket is down, message discarded.") continue with self.lock: print(f"[WRITER] Sending to IRC: {msg.strip()}") self.irc.send(msg.encode("utf-8")) except (ConnectionResetError, BrokenPipeError, ssl.SSLError) as net_err: print(f"[WRITER NET ERROR] Socket error: {net_err}") self.irc = None time.sleep(RECONNECT_DELAY) continue except Exception as e: print(f"[WRITER ERROR] Unexpected error: {e}") time.sleep(1) bot = IRCBot()