diff --git a/.gitignore b/.gitignore index 96403d3..d68cb17 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ __pycache__/* +secrets.py diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..b5a4380 --- /dev/null +++ b/bot.py @@ -0,0 +1,35 @@ +import socket +from threading import Thread + +from irc import IRC +from secrets import USER, PASSWORD + + +class Bot(object): + def __init__(self, irc, v5, channels): + self.irc = IRC(*irc) + self.sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) + self.channels = channels + self._v5_handler = Thread(target=self._handle_v5) + + + def start(self): + # Start IRC + self.irc.start(USER, PASSWORD) + + for c in self.channels: + self.irc.join(c) + + # Start v5 handler + self._v5_handler.start() + + # Run IRC + self.irc.run() + + + def _handle_v5(self): + while True: + data, addr = self.v5_sock.recvfrom(4096) + data = data.decode() + print(f"v5: Received <{data}>") + self.irc.msg(data, "#glados") diff --git a/irc.py b/irc.py new file mode 100644 index 0000000..762f59d --- /dev/null +++ b/irc.py @@ -0,0 +1,134 @@ +# Manage the IRC layer of GLaDOS + +import 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() + for event, callback in self._callbacks: + if event(message): + 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() + print(f"receive: {message}") + if " PRIVMSG " in message: + return Message(message) + + def join(self, channel): + """ Join a channel """ + self._send(f"JOIN {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)) + 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) + print(f"_handle: <{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) + self.author = match.group("author") + self.to = match.group("to") + self.text = match.group("text") + print(self) + + def __str__(self): + return f"<{self.author}> à <{self.to}> : {self.text}" + + + +if __name__ == "__main__": + bot = IRC("irc.planet-casio.com", 6697) + + @bot.on(lambda m: "Hello" in m.text) + def hello(msg): + bot.send(msg.to, f"Nice to see you {msg.author}!") + + bot.start("glados", "abcdef123456") + bot.join("#glados") + + bot.run() diff --git a/main.py b/main.py index 6f5ca47..003866e 100755 --- a/main.py +++ b/main.py @@ -3,56 +3,20 @@ import sys import time from threading import Thread -# from queue import Queue - -from utils import create_socket, IRC - -server_name = 'localhost' -server_port = 5500 - -NICK = "GLaDOS" -PASSWORD = "abcdef123456" - -class Bot(object): - def __init__(self, irc, v5): - self.irc = IRC(irc) - self.v5_sock = create_socket("v5", v5) - self.channels = ["#general", "#glados"] - self.connected = False # Dirty lock - - def run(self): - # Start a thread for raw IRC messages - Thread(target=self.irc.raw_handle).start() - - # Authenticate to server - self.irc.connect(NICK, PASSWORD) - for c in self.channels: - self.irc.join(c) - - # Finally, handle v5 messages - while True: - data, addr = self.v5_sock.recvfrom(4096) - data = data.decode() - print(f"v5: Received <{data}>") - self.irc.msg(data, "#glados") - - def handle_irc(self): - self.connected = True - print("irc connected") - - while True: - msg = self.irc.get_msg() - print(f"IRC: {data}") - - if "Hello" in msg: - self.irc.msg("Hello :)", "#glados") +from bot import Bot +from secrets import USER, PASSWORD glados = Bot( ('irc.planet-casio.com', 6697), - ('127.0.0.1', 5555) + ('127.0.0.1', 5555), + ["#general", "#glados"] ) -glados.run() +@glados.irc.on(lambda m: "Hello" in m.text) +def say_hello(msg): + glados.irc.send(msg.to, f"Nice to see you, {msg.author}") -#abcdef123456 + +glados.start() +glados.run() diff --git a/sample.txt b/sample.txt deleted file mode 100644 index 0b1180c..0000000 --- a/sample.txt +++ /dev/null @@ -1,42 +0,0 @@ -< :irc.planet-casio.com NOTICE * :*** Looking up your hostname... -< :irc.planet-casio.com NOTICE * :*** Found your hostname (cached) -> USER GLaDOS * * :GLaDOS -> NICK GLaDOS -< PING :5B61F700 -> PONG :5B61F700 - -< :irc.planet-casio.com NOTICE GLaDOS :Serveur réservé aux membres de Planète Casio. -< :irc.planet-casio.com NOTICE GLaDOS :Ce serveur nécessite de s'authentifier. Les identifiants sont ceux de votre compte Planète Casio (https://www.planet-casio.com). -< :irc.planet-casio.com NOTICE GLaDOS :Pour vous connecter, configurez le SASL dans votre client ou entrez la commande suivante : /AUTH : - -> AUTH GLaDOS:password -< :irc.planet-casio.com 900 GLaDOS GLaDOS!GLaDOS@87-231-180-235.rev.numericable.fr GLaDOS :You are now logged in as GLaDOS. -< :irc.planet-casio.com 001 GLaDOS :Welcome to the Planete Casio IRC Network GLaDOS!GLaDOS@87-231-180-235.rev.numericable.fr -< :irc.planet-casio.com 002 GLaDOS :Your host is irc.planet-casio.com, running version UnrealIRCd-5.0.7 -< :irc.planet-casio.com 003 GLaDOS :This server was created Sun Oct 11 2020 at 12:46:49 MSK -< :irc.planet-casio.com 004 GLaDOS irc.planet-casio.com UnrealIRCd-5.0.7 iowrsxzdHtIDZRqpWGTSB lvhopsmntikraqbeIHzMQNRTOVKDdGLPZSCcf -< :irc.planet-casio.com 005 GLaDOS AWAYLEN=307 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=beI,kLf,lH,psmntirzMQNRTOVKDdGPZSCc CHANNELLEN=32 CHANTYPES=# CLIENTTAGDENY=*,-draft/typing,-typing DEAF=d ELIST=MNUCT EXCEPTS EXTBAN=~,ptmTSOcarnqjf :are supported by this server -< :irc.planet-casio.com 005 GLaDOS HCN INVEX KICKLEN=307 KNOCK MAP MAXCHANNELS=100 MAXLIST=b:60,e:60,I:60 MAXNICKLEN=30 MINNICKLEN=0 MODES=12 NAMESX NETWORK=Planete-Casio :are supported by this server -< :irc.planet-casio.com 005 GLaDOS NICKLEN=30 PREFIX=(qaohv)~&@%+ QUITLEN=307 SAFELIST SILENCE=15 STATUSMSG=~&@%+ TARGMAX=DCCALLOW:,ISON:,JOIN:,KICK:4,KILL:,LIST:,NAMES:1,NOTICE:1,PART:,PRIVMSG:4,SAJOIN:,SAPART:,TAGMSG:1,USERHOST:,USERIP:,WATCH:,WHOIS:1,WHOWAS:1 TOPICLEN=360 UHNAMES USERIP WALLCHOPS WATCH=128 :are supported by this server -< :irc.planet-casio.com 005 GLaDOS WATCHOPTS=A WHOX :are supported by this server -< :irc.planet-casio.com 396 GLaDOS Clk-3B0FEC88.rev.numericable.fr :is now your displayed host -< :irc.planet-casio.com NOTICE GLaDOS :*** You are connected to irc.planet-casio.com with TLSv1.3-TLS_CHACHA20_POLY1305_SHA256 -< :irc.planet-casio.com 251 GLaDOS :There are 1 users and 10 invisible on 2 servers -< :irc.planet-casio.com 252 GLaDOS 7 :operator(s) online -< :irc.planet-casio.com 254 GLaDOS 1 :channels formed -< :irc.planet-casio.com 255 GLaDOS :I have 4 clients and 1 servers -< :irc.planet-casio.com 265 GLaDOS 4 8 :Current local users 4, max 8 -< :irc.planet-casio.com 266 GLaDOS 11 13 :Current global users 11, max 13 -< :irc.planet-casio.com 422 GLaDOS :MOTD File is missing -< :GLaDOS MODE GLaDOS :+iwxz -< :GLaDOS!GLaDOS@Clk-3B0FEC88.rev.numericable.fr JOIN :#general -< :irc.planet-casio.com 353 GLaDOS = #general :GLaDOS Darks Breizh eragon -< :irc.planet-casio.com 366 GLaDOS #general :End of /NAMES list. -< :NickServ MODE GLaDOS :+r - -> Hello -< :irc.planet-casio.com 421 GLaDOS Hello :Unknown command - -> join #general -> PRIVMSG #general :Hello -< :Darks!eldeberen@Clk-3B0FEC88.rev.numericable.fr PRIVMSG #general :Nice diff --git a/utils.py b/utils.py deleted file mode 100644 index a641abd..0000000 --- a/utils.py +++ /dev/null @@ -1,69 +0,0 @@ -import socket, ssl -from queue import Queue - -def create_socket(type, target): - if type == "irc": - s = socket.create_connection(('irc.planet-casio.com', 6697)) - context = ssl.create_default_context() - s = context.wrap_socket(s, server_hostname='irc.planet-casio.com') - return s - elif type == "v5": - s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) - s.bind(target) - return s - -class IRC(object): - def __init__(self, target): - self.socket = create_socket("irc", target) - self.inbox = Queue() - - - # Public API - - def raw_handle(self): - """ Handle new messages and ping responses. Runs in its own thread""" - data = self.socket.recv(4096).decode() - for m in data.split('\r\n'): - if m.startswith("PING"): - self._send(m.replace("PING", "PONG")) - else: - self.inbox.put(m) - - def connect(self, nick, password): - """ Connect and authenticate to IRC server """ - self._send(f"NICK {nick}") - self._send(f"USER {nick} * * :{nick}") - - self._waitfor(lambda m: "NOTICE" in m and "/AUTH" in m) - self._send(f"auth {nick}:{password}") - self._waitfor(lambda m: f":{nick} MODE {nick} :+iwxz" in m) - - def join(self, channel): - """ Join a channel """ - self._send("JOIN :{channel}") - - def msg(self, msg, channel): - self._send(f"PRIVMSG {channel} :{msg}") - - def get_msg(self): - """ Get the next message on the inbox """ - return _waitfor(lambda m: "PRIVMSG" in m) - - # Private API - - def _send(self, raw): - """ Send raw data """ - self.socket.send(f"{raw}\r\n".encode()) - - def _recv(self): - """ Pop a new item from INBOX """ - print("Waiting for new item") - return self.inbox.get() - - def _waitfor(self, condition): - """ Wait for a message that matches the condition """ - msg = self._recv() - while not condition(msg): - print(f"Wait for: {msg}") - msg = self._recv() - return msg