@ -1 +1,2 @@ | |||
__pycache__/* | |||
secrets.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") |
@ -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<author>[\w.~]+)(?:!(?P<host>\S+))? PRIVMSG (?P<to>\S+) :(?P<text>.+)") | |||
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() |
@ -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 <login>:<mot de passe> | |||
> 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 |
@ -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 |