Code documentation

This commit is contained in:
Shadow15510 2023-06-09 14:07:04 +02:00
parent 3c60008db2
commit f421e756a8
4 changed files with 336 additions and 97 deletions

61
bot.py
View File

@ -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()

266
irc.py
View File

@ -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<author>[\w.~|]+)(?:!(?P<host>\S+))? PRIVMSG (?P<to>\S+) :(?P<text>.+)")
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<author>[\w.~|]+)(?:!(?P<host>\S+))? PRIVMSG (?P<to>\S+) :(?P<text>.+)"
)
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}"

20
main.py
View File

@ -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()

86
v5.py
View File

@ -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 <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)