Code documentation
This commit is contained in:
parent
3c60008db2
commit
f421e756a8
61
bot.py
61
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()
|
||||
|
|
266
irc.py
266
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<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
20
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()
|
||||
|
|
86
v5.py
86
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 <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)
|
||||
|
|
Loading…
Reference in New Issue