From 3a7c771ed2974979471884077095c737e4957b7d Mon Sep 17 00:00:00 2001 From: Eric Lay Date: Sun, 4 May 2025 20:04:54 -0500 Subject: [PATCH] Add irc_bot.py --- irc_bot.py | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 irc_bot.py diff --git a/irc_bot.py b/irc_bot.py new file mode 100644 index 0000000..4ae04a3 --- /dev/null +++ b/irc_bot.py @@ -0,0 +1,197 @@ +import socket +import threading +import time +import re +import requests +from auth_db import auth_db +from utils import strip_bbcode, parse_username +from config import ( + SERVER, PORT, BOTNICK, API_ENDPOINT, API_TOKEN, VERIFY_ENDPOINT, + LOBBY_CHANNEL, MAIN_CHANNEL, ADMIN_CHANNEL, STAFF_CHANNELS, + RECONNECT_DELAY, GROUP_NAMES, STAFF_GROUP_IDS, ADMIN_GROUP_IDS +) + +class IRCBot: + def __init__(self): + self.irc = None + self.nickname = BOTNICK + self.running = False + self.lock = threading.Lock() + +# Connect to IRC server, handle nick conflicts, wait for welcome, and join channel + def connect(self): + attempt = 0 + while True: + try: + self.irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + 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 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}") + self.irc.send(f"JOIN {LOBBY_CHANNEL}\r\n".encode("utf-8")) + return + elif " 433 " in line: + attempt += 1 + self.nickname += "_" + self.irc.close() + time.sleep(2) + break + 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 + threading.Thread(target=self.listen_loop, daemon=True).start() + print("[+] IRC listener started") + + 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"): + if line.startswith("PING"): + self.irc.send(f"PONG {line.split()[1]}\r\n".encode("utf-8")) + 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() + + # 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) + if user_data: + site_user, irc_key, _ = user_data + self.api_call(site_user, msg, irc_key) + + # 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) + self.send_notice(nick, f"Verified as {site_user} ({group_name})") + + # Join main lobby + 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: + 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): + try: + self.irc.send(f"NOTICE {nick} :{msg}\r\n".encode("utf-8")) + except Exception as e: + print(f"[-] Notice failed: {e}") + + # Send IRC message to channel + def send_to_channel(self, msg): + clean = strip_bbcode(msg).replace("\n", " ").replace("\r", " ") + try: + self.irc.send(f"PRIVMSG {LOBBY_CHANNEL} :{clean}\r\n".encode("utf-8")) + except Exception as e: + print(f"[-] Failed to send: {e}") + + # Send IRC commands to server + def send_raw_command(self, command): + if self.irc: + try: + self.irc.send(f"{command}\r\n".encode("utf-8")) + 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, token): + try: + requests.post(API_ENDPOINT, headers={ + "Authorization": f"Bearer {API_TOKEN}", + "Content-Type": "application/json" + }, json={"username": user, "text": msg, "token": token}) + 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) + +bot = IRCBot()