From f421e756a896251a0cd5d46518881a37ad21357c Mon Sep 17 00:00:00 2001 From: Shadow15510 Date: Fri, 9 Jun 2023 14:07:04 +0200 Subject: [PATCH] Code documentation --- bot.py | 61 +++++++++++-- irc.py | 266 ++++++++++++++++++++++++++++++++++++++++++-------------- main.py | 20 ++++- v5.py | 86 +++++++++++++----- 4 files changed, 336 insertions(+), 97 deletions(-) diff --git a/bot.py b/bot.py index f1cfbd0..28173f7 100644 --- a/bot.py +++ b/bot.py @@ -1,24 +1,67 @@ -import socket -from threading import Thread +""" +Bot (GLaDOS) +============ + +Description +----------- +Allow to make and run a Bot instance which will communicate to the V5 server and IRC one. +""" + +import json +#import socket +#from threading import Thread from irc import IRC from v5 import V5 -from secrets import USER, PASSWORD -class Bot(object): - def __init__(self, irc, v5, channels): - self.irc = IRC(*irc) +with open("secrets.json", "r", encoding="utf-8") as file: + secrets = json.load(file) + USER = secrets["user"] + PASSWORD = secrets["password"] + + +class Bot: + """Run the connexion between IRC's server and V5 one. + + Attributes + ---------- + irc : IRC, public + IRC wrapper which handle communication with IRC server. + v5 : V5, public + V5 wrapper which handle communication with V5 server. + channels : list, public + The channels the bot will listen. + + Methods + ------- + start : NoneType, public + Runs the bot and connects it to IRC and V5 servers. + """ + def __init__(self, irc_params: tuple, v5_params: tuple, channels: list): + """Initialize the Bot instance. + + Parameters + ---------- + irc_params : tuple + Contains the IRC server informations (host, port) + v5_params : tuple + Contains the V5 server informations (host, port) + channels : list + Contains the names of the channels on which the bot will connect. + """ + self.irc = IRC(*irc_params) self.channels = channels - self.v5 = V5(v5, self.irc) + self.v5 = V5(v5_params, self.irc) def start(self): + """Starts the bot and connect it to the given IRC and V5 servers.""" # Start IRC self.irc.start(USER, PASSWORD) # Join channels - for c in self.channels: - self.irc.join(c) + for channel in self.channels: + self.irc.join(channel) # Start v5 handler self.v5.start() diff --git a/irc.py b/irc.py index f969d56..76dc851 100644 --- a/irc.py +++ b/irc.py @@ -1,132 +1,268 @@ -# Manage the IRC layer of GLaDOS +""" +irc (GLaDOS) +============ + +Description +----------- +Manage the IRC layer of GLaDOS. +""" + +import logging +import re +import socket +import ssl -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 """ + +class IRC: + """Manage connexion to an IRC server, authentication and callbacks. + + Attributes + ---------- + connected : bool, public + If the bot is connected to an IRC server or not. + + socket : ssl.SSLSocket, private + The IRC's socket. + inbox : Queue, private + Queue of the incomming messages. + handler : Thread, private + callbacks : list, private + List of the registred callbacks. + + Methods + ------- + start : NoneType, public + Starts the IRC layer and manage authentication. + run : NoneType, public + Mainloop, allows to handle public messages. + send : NoneType, public + Sends a message to a given channel. + receive : Message, public + Same as ``run`` for private messages. + join : NoneType, public + Allows to join a given channel. + on : function, public + Add a callback on a given message. + + handle : NoneType, private + Handles the ping and store incoming messages into the inbox attribute. + send : NoneType, private + Send message to a target. + recv : str, private + Get the oldest incoming message and returns it. + waitfor : str, private + Wait for a raw message that matches the given condition. + """ + def __init__(self, host: str, port: int): + """Initialize an IRC wrapper. + + Parameters + ---------- + host : str + The adress of the IRC server. + port : int + The port of the IRC server. + """ + # Public attributes - self.connected = False # Simple lock + 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 = [] + 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: str, password: str): + """Start the IRC layer. Manage authentication as well. - def start(self, nick, password): - """ Start the IRC layer. Manage authentication as well """ - self._handler.start() + Parameters + ---------- + nick : str + The username for login and nickname once connected. + password : str + The password for authentification. + """ + 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.__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 """ + """Handle new messages.""" while True: message = self.receive() - logging.info(f"received {message}") + logging.info("received %s", message) if message is not None: - for event, callback in self._callbacks: + for event, callback in self.__callbacks: if event(message): - logging.info(f"matched {event.__name__}") + logging.info("matched %s", 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 send(self, target: str, message: str): + """Send a message to the specified target (channel or user). + + Parameters + ---------- + target : str + The target of the message. It can be a channel or user (private message). + message : str + The content of the message to send. + """ + self.__send(f"PRIVMSG {target} :{message}") def receive(self): - """ Receive a private message """ + """Receive a private message. + + Returns + ------- + msg : Message + The incoming processed private message. + """ while True: - message = self._recv() + 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 join(self, channel: str): + """Join a channel. + + Parameters + ---------- + channel : str + The name of the channel to join. + """ + self.__send(f"JOIN {channel}") + logging.info("joined %s", 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 """ - + """Adds a callback to the IRC handler. + + Parameters + ---------- + event : function + ``event`` must taking in parameter a Message and returning ``True`` if the callback + should be executed on the message. + + Returns + ------- + callback : function + The callback function. + """ def callback(func): @wraps(func) def wrapper(message): func(message) - self._callbacks.append((event, wrapper)) - logging.info(f"added callback {func.__name__}") + self.__callbacks.append((event, wrapper)) + logging.info("added callback %s", func.__name__) return wrapper return callback # Private methods - - def _handle(self): - """ Handle raw messages from irc and manage ping """ + def __handle(self): + """Handle raw messages from irc and manage ping.""" while True: # Get incoming messages - data = self._socket.recv(4096).decode() + data = self.__socket.recv(4096).decode() # Split multiple lines - for m in data.split('\r\n'): + for msg in data.split('\r\n'): # Manage ping - if m.startswith("PING"): - self._send(m.replace("PING", "PONG")) + if msg.startswith("PING"): + self.__send(msg.replace("PING", "PONG")) # Or add a new message to inbox - elif len(m): - self._inbox.put(m) - logging.debug(f"received {m}") + elif len(msg): + self.__inbox.put(msg) + logging.debug("received %s", msg) - def _send(self, raw): - """ Wrap and encode raw message to send """ - self._socket.send(f"{raw}\r\n".encode()) + def __send(self, raw: str): + """Wrap and encode raw message to send. - def _recv(self): - m = self._inbox.get() - return m + Parameters + ---------- + raw : str + The raw message to send. + """ + self.__socket.send(f"{raw}\r\n".encode()) - def _waitfor(self, condition): - """ Wait for a raw message that matches the condition """ - msg = self._recv() + def __recv(self): + """Get the older received message and returns it. + + Returns + ------- + m : str + The raw content of the message to return. + """ + msg = self.__inbox.get() + return msg + + def __waitfor(self, condition): + """Wait for a raw message that matches the condition. + + Parameters + ---------- + condition : function + ``condition`` is a function that must taking a raw message in parameter and returns a + boolean. + + Returns + ------- + msg : str + The last message received that doesn't match the condition. + """ + msg = self.__recv() while not condition(msg): - msg = self._recv() + msg = self.__recv() return msg -class Message(object): - r = re.compile("^:(?P[\w.~|]+)(?:!(?P\S+))? PRIVMSG (?P\S+) :(?P.+)") +class Message: + """Parse the raw message in three fields : author, the channel, and text. + + Attributes + ---------- + pattern : re.Pattern, public + The message parsing pattern. + author : str, public + The message's author. + to : str, public + The message's origin (channel or DM). + text : str, public + The message's content. + """ + pattern = re.compile( + r"^:(?P[\w.~|]+)(?:!(?P\S+))? PRIVMSG (?P\S+) :(?P.+)" + ) - def __init__(self, raw): - match = re.search(Message.r, raw) + def __init__(self, raw: str): + match = re.search(Message.pattern, 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}") + logging.debug("sucessfully parsed %s into %s", raw, self.__str__()) else: self.author = "" self.to = "" self.text = "" - logging.warning(f"failed to parse {raw} into valid message") + logging.warning("failed to parse %s into valid message", raw) def __str__(self): return f"{self.author} to {self.to}: {self.text}" diff --git a/main.py b/main.py index 511cb11..bed2a2d 100755 --- a/main.py +++ b/main.py @@ -1,25 +1,39 @@ #!/usr/bin/env python3 +""" +main (GLaDOS) +============= -import logging, re +Description +----------- +Create a bot's instance and manages it. +""" + +import logging +import re from bot import Bot LOG_FORMAT = "%(asctime)s [%(levelname)s] <%(filename)s> %(funcName)s: %(message)s" logging.basicConfig(format=LOG_FORMAT, level=logging.DEBUG) + glados = Bot( ('irc.planet-casio.com', 6697), ('127.0.0.1', 5555), ["#general", "#glados"] ) + @glados.irc.on(lambda m: re.match("bonjour glados", m.text, re.IGNORECASE)) def say_hello(msg): + """Make GLaDOS responds to greetings.""" glados.irc.send(msg.to, f"Heureuse de vous revoir, {msg.author}") + @glados.v5.on(lambda c, m: True) def announce(channels, message): - for c in channels: - glados.irc.send(c, message) + """Make an announce.""" + for channel in channels: + glados.irc.send(channel, message) glados.start() diff --git a/v5.py b/v5.py index 943f6df..6ecb651 100644 --- a/v5.py +++ b/v5.py @@ -1,46 +1,92 @@ -import logging, socket +""" +v5 (GLaDOS) +=========== + +Description +----------- +Manage the V5 layer of GLaDOS. +""" + +import logging +import socket from threading import Thread from functools import wraps -class V5(object): - def __init__(self, v5, irc): - """ Initialize v5 handle - :irc : an initialized IRC object """ + +class V5: + """Manage connexion beetween the bot and the V5 server, and manage callbacks. + + Attributes + ---------- + irc : irc.IRC, public + An IRC instance. + + sock : ssl.SSLSocket, private + The V5 socket. + handler : Thread, private + callbacks : list, private + List of the registred callbacks. + + Methods + ------- + start : NoneType, public + Start v5 handler. + on : function, public + Add a callback to the v5 handler. + + handle : NoneType, private + Handle the incoming messages and callbacks. + """ + + def __init__(self, v5_params: tuple, irc): + """Initialize V5 handle. + + Parameters + ---------- + v5 : tuple + The information on V5 server (host, port). + irc : irc.IRC + An initialized IRC instance. + """ self.irc = irc - self._sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) - self._sock.bind(v5) - self._handler = Thread(target=self._handle) - self._callbacks = [] + self.__sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) + self.__sock.bind(v5_params) + self.__handler = Thread(target=self.__handle) + self.__callbacks = [] def start(self): - # Start v5 handler - self._handler.start() + """Start v5 handler.""" + self.__handler.start() logging.info("started") def on(self, event): - """ Adds a callback to the v5 handler - Event is a function taking in parameter a list of channels and a - string, and return True if the callback should be executed """ + """Adds a callback to the v5 handler. + Parameters + ---------- + event : function + ``event`` is a function taking in parameter a list of channels and a string, and return + ``True`` if the callback should be executed. + """ def callback(func): @wraps(func) def wrapper(channels, message): func(channels, message) - self._callbacks.append((event, wrapper)) + self.__callbacks.append((event, wrapper)) return wrapper return callback - def _handle(self): + def __handle(self): while True: - data, addr = self._sock.recvfrom(4096) + data, addr = self.__sock.recvfrom(4096) data = data.decode() - logging.debug(f"received {data}") + logging.debug("received %s", data) channels, message = data.split(":", 1) channels = channels.split(" ") - for event, callback in self._callbacks: + for event, callback in self.__callbacks: if event(channels, message): - logging.info(f"passed {event.__name__}") + logging.info("passed %s", event.__name__) callback(channels, message)