Starting an API
This commit is contained in:
parent
20df81a28c
commit
efe477612b
|
@ -0,0 +1,19 @@
|
|||
from irc_bot_api import commands
|
||||
|
||||
class GladosV4(commands.CommandsPack):
|
||||
@commands.command(name="bye", event=lambda m: "au revoir glados" in m.text.lower())
|
||||
def greetings_bye(self, msg):
|
||||
"""Répond "au revoir"."""
|
||||
self.bot.irc.send(msg.to, f"Au revoir {msg.author}")
|
||||
|
||||
@commands.command(name="hello", event=lambda m: "bonjour glados" in m.text.lower())
|
||||
def greetings_hello(self, msg):
|
||||
"""Dit bonjour lorsque l'on salue le GLaDOS."""
|
||||
self.bot.irc.send(msg.to, f"Bonjour {msg.author}")
|
||||
|
||||
|
||||
class GladosV5(commands.CommandsPack):
|
||||
@commands.command(name="test")
|
||||
def test(self, msg):
|
||||
"""Test de module de fonction."""
|
||||
self.bot.irc.send(msg.to, "Ceci est un test")
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,138 @@
|
|||
from functools import wraps
|
||||
import logging
|
||||
from irc_bot_api.secrets import USER, PASSWORD
|
||||
from irc_bot_api.irc import IRC
|
||||
from irc_bot_api.v5 import V5
|
||||
|
||||
|
||||
PREFIX = ""
|
||||
|
||||
def command(name, event=None):
|
||||
"""Decorate a function and return a Command instance."""
|
||||
def decorator(func):
|
||||
desc = name
|
||||
if func.__doc__:
|
||||
desc = func.__doc__
|
||||
return Command(
|
||||
name=name,
|
||||
desc=desc,
|
||||
func=func,
|
||||
event=event
|
||||
)
|
||||
return decorator
|
||||
|
||||
|
||||
class Command:
|
||||
def __init__(self, name, desc, func, event=None):
|
||||
self.name = name
|
||||
self.desc = desc
|
||||
self.func = func
|
||||
if not event:
|
||||
self.event = lambda m: m.text.startswith(PREFIX + name)
|
||||
else:
|
||||
self.event = event
|
||||
|
||||
self.cmnd_pack = None
|
||||
|
||||
def __call__(self, msg):
|
||||
return self.func(self.cmnd_pack, msg)
|
||||
|
||||
|
||||
class CommandsPack:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
|
||||
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, prefix: str=""):
|
||||
"""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.
|
||||
prefix : str, optionnal
|
||||
The prefix on which the bot will react.
|
||||
"""
|
||||
global PREFIX
|
||||
PREFIX = prefix
|
||||
|
||||
self.irc = IRC(*irc_params)
|
||||
self.v5 = V5(v5_params, self.irc)
|
||||
self.channels = channels
|
||||
self.help = []
|
||||
|
||||
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 channel in self.channels:
|
||||
self.irc.join(channel)
|
||||
|
||||
# Start V5 hadndler
|
||||
self.v5.start()
|
||||
|
||||
# Run IRC
|
||||
self.irc.run()
|
||||
|
||||
# add auto-generated help function
|
||||
self.add_help()
|
||||
|
||||
|
||||
def add_help(self):
|
||||
def callback(message):
|
||||
return help_cmnd(self, message)
|
||||
|
||||
help_callback = Command(
|
||||
"aide",
|
||||
"Affiche la liste des commandes disponibles.",
|
||||
callback
|
||||
)
|
||||
self.irc_callbacks.append(help_callback)
|
||||
|
||||
def add_commands_pack(self, commands_pack):
|
||||
"""Add a package of commands to the bot.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
commands_pack : CommandsPack
|
||||
A commands pack which contains command's instances.
|
||||
"""
|
||||
cmnd_pack = commands_pack(self)
|
||||
|
||||
for cmnd_name in dir(commands_pack):
|
||||
if not cmnd_name.startswith("__") and not cmnd_name.endswith("__"):
|
||||
cmnd = getattr(commands_pack, cmnd_name)
|
||||
cmnd.cmnd_pack = cmnd_pack
|
||||
|
||||
self.help.append(cmnd)
|
||||
self.irc.callbacks.append(cmnd)
|
||||
|
||||
|
||||
def help_cmnd(bot, msg):
|
||||
"""Documentation des fonctions disponibles."""
|
||||
bot.irc.send(msg.to, f"Aide des commandes")
|
||||
for cmnd in bot.help:
|
||||
bot.irc.send(msg.to, f" – {cmnd.name} : {cmnd.desc}")
|
|
@ -0,0 +1,234 @@
|
|||
"""
|
||||
irc (GLaDOS)
|
||||
============
|
||||
|
||||
Description
|
||||
-----------
|
||||
Manage the IRC layer of GLaDOS.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
from functools import wraps
|
||||
from queue import Queue
|
||||
from threading import Thread
|
||||
|
||||
|
||||
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.
|
||||
callbacks : list, public
|
||||
List of the registred callbacks.
|
||||
|
||||
socket : ssl.SSLSocket, private
|
||||
The IRC's socket.
|
||||
inbox : Queue, private
|
||||
Queue of the incomming messages.
|
||||
handler : Thread, private
|
||||
|
||||
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.callbacks = []
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
# Public methods
|
||||
def start(self, nick: str, password: str):
|
||||
"""Start the IRC layer. Manage authentication as well.
|
||||
|
||||
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.connected = True
|
||||
|
||||
def run(self):
|
||||
"""Handle new messages."""
|
||||
while True:
|
||||
message = self.receive()
|
||||
logging.info("received %s", message)
|
||||
if message is not None:
|
||||
for callback in self.callbacks:
|
||||
if callback.event(message):
|
||||
logging.info("matched %s", callback.name)
|
||||
callback(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.
|
||||
|
||||
Returns
|
||||
-------
|
||||
msg : Message
|
||||
The incoming processed private message.
|
||||
"""
|
||||
while True:
|
||||
message = self.__inbox.get()
|
||||
if " PRIVMSG " in message:
|
||||
msg = Message(message)
|
||||
if msg:
|
||||
return msg
|
||||
|
||||
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)
|
||||
|
||||
# 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 msg in data.split('\r\n'):
|
||||
# Manage ping
|
||||
if msg.startswith("PING"):
|
||||
self.__send(msg.replace("PING", "PONG"))
|
||||
|
||||
# Or add a new message to inbox
|
||||
elif len(msg):
|
||||
self.__inbox.put(msg)
|
||||
logging.debug("received %s", msg)
|
||||
|
||||
def __send(self, raw: str):
|
||||
"""Wrap and encode raw message to send.
|
||||
|
||||
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.
|
||||
|
||||
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.__inbox.get()
|
||||
while not condition(msg):
|
||||
msg = self.__inbox.get()
|
||||
return msg
|
||||
|
||||
|
||||
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: 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("sucessfully parsed %s into %s", raw, self.__str__())
|
||||
else:
|
||||
self.author = ""
|
||||
self.to = ""
|
||||
self.text = ""
|
||||
logging.warning("failed to parse %s into valid message", raw)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.author} to {self.to}: {self.text}"
|
|
@ -0,0 +1,92 @@
|
|||
"""
|
||||
v5 (GLaDOS)
|
||||
===========
|
||||
|
||||
Description
|
||||
-----------
|
||||
Manage the V5 layer of GLaDOS.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
from threading import Thread
|
||||
from functools import wraps
|
||||
|
||||
|
||||
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_params)
|
||||
self.__handler = Thread(target=self.__handle)
|
||||
self.__callbacks = []
|
||||
|
||||
def start(self):
|
||||
"""Start v5 handler."""
|
||||
self.__handler.start()
|
||||
logging.info("started")
|
||||
|
||||
def on(self, event):
|
||||
"""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))
|
||||
return wrapper
|
||||
|
||||
return callback
|
||||
|
||||
def __handle(self):
|
||||
"""Handle the incoming messages and callbacks."""
|
||||
while True:
|
||||
data, addr = self.__sock.recvfrom(4096)
|
||||
data = data.decode()
|
||||
logging.debug("received %s", data)
|
||||
channels, message = data.split(":", 1)
|
||||
channels = channels.split(" ")
|
||||
|
||||
for event, callback in self.__callbacks:
|
||||
if event(channels, message):
|
||||
logging.info("passed %s", event.__name__)
|
||||
callback(channels, message)
|
20
main.py
20
main.py
|
@ -10,30 +10,22 @@ Create a bot's instance and manages it.
|
|||
|
||||
import logging
|
||||
import re
|
||||
from bot import Bot
|
||||
from irc_bot_api import commands
|
||||
from glados_cmnds import GladosV4, GladosV5
|
||||
|
||||
|
||||
LOG_FORMAT = "%(asctime)s [%(levelname)s] <%(filename)s> %(funcName)s: %(message)s"
|
||||
logging.basicConfig(format=LOG_FORMAT, level=logging.DEBUG)
|
||||
|
||||
|
||||
glados = Bot(
|
||||
glados = commands.Bot(
|
||||
('irc.planet-casio.com', 6697),
|
||||
('127.0.0.1', 5555),
|
||||
["#general", "#glados"]
|
||||
["#general", "#glados"],
|
||||
)
|
||||
|
||||
glados.add_commands_pack(GladosV4)
|
||||
glados.add_commands_pack(GladosV5)
|
||||
|
||||
@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):
|
||||
"""Make an announce."""
|
||||
for channel in channels:
|
||||
glados.irc.send(channel, message)
|
||||
|
||||
glados.start()
|
||||
|
|
Loading…
Reference in New Issue