# Manage the IRC layer of GLaDOS import logging, re, socket, ssl from functools import wraps from queue import Queue from threading import Thread class IRC(object): def __init__(self, host, port): """ Initialize an IRC wrapper """ # Public attributes self.connected = False # Simple lock # Private attributes self._socket = ssl.create_default_context().wrap_socket( socket.create_connection((host, port)), server_hostname=host) self._inbox = Queue() self._handler = Thread(target=self._handle) self._callbacks = [] # Public methods def start(self, nick, password): """ Start the IRC layer. Manage authentication as well """ self._handler.start() self._send(f"USER {nick} * * :{nick}") self._send(f"NICK {nick}") self._waitfor(lambda m: "NOTICE" in m and "/AUTH" in m) self._send(f"AUTH {nick}:{password}") self._waitfor(lambda m: "You are now logged in" in m) self.connected = True def run(self): """ Handle new messages """ while True: message = self.receive() logging.info(f"received {message}") if message is not None: for event, callback in self._callbacks: if event(message): logging.info(f"matched {event.__name__}") callback(message) def send(self, target, message): """ Send a message to the specified target (channel or user) """ self._send(f"PRIVMSG {target} :{message}") def receive(self): """ Receive a private message """ while True: message = self._recv() if " PRIVMSG " in message: msg = Message(message) if msg: return msg def join(self, channel): """ Join a channel """ self._send(f"JOIN {channel}") logging.info(f"joined {channel}") def on(self, event): """ Adds a callback to the IRC handler Event is a function taking in parameter a Message and returning True if the callback should be executed on the message """ def callback(func): @wraps(func) def wrapper(message): func(message) self._callbacks.append((event, wrapper)) logging.info(f"added callback {func.__name__}") return wrapper return callback # Private methods def _handle(self): """ Handle raw messages from irc and manage ping """ while True: # Get incoming messages data = self._socket.recv(4096).decode() # Split multiple lines for m in data.split('\r\n'): # Manage ping if m.startswith("PING"): self._send(m.replace("PING", "PONG")) # Or add a new message to inbox elif len(m): self._inbox.put(m) logging.debug(f"received {m}") def _send(self, raw): """ Wrap and encode raw message to send """ self._socket.send(f"{raw}\r\n".encode()) def _recv(self): m = self._inbox.get() return m def _waitfor(self, condition): """ Wait for a raw message that matches the condition """ msg = self._recv() while not condition(msg): msg = self._recv() return msg class Message(object): r = re.compile("^:(?P[\w.~|]+)(?:!(?P\S+))? PRIVMSG (?P\S+) :(?P.+)") def __init__(self, raw): match = re.search(Message.r, raw) if match: self.author = match.group("author") self.to = match.group("to") self.text = match.group("text") logging.debug(f"sucessfully parsed {raw} into {self}") else: self.author = "" self.to = "" self.text = "" logging.warning(f"failed to parse {raw} into valid message") def __str__(self): return f"{self.author} to {self.to}: {self.text}"