commit 8c46906cc29a7344c800701b04aaaa5b21aed804 Author: Louis Gatin Date: Fri Sep 10 14:23:43 2021 +0200 First commit diff --git a/bridge.py b/bridge.py new file mode 100644 index 0000000..9b6304b --- /dev/null +++ b/bridge.py @@ -0,0 +1,35 @@ +import json +import logging +import requests as r +import time + + +from irc import IRC +from shoutbox import Shoutbox +from cookies import cookies +from sasl import nick, password + + +LOG_FORMAT = "%(asctime)s [%(levelname)s] <%(filename)s> %(funcName)s: %(message)s" +logging.basicConfig(format=LOG_FORMAT, level=logging.INFO) + +channels = ["hs", "projets", "annonces"] + +irc = IRC('irc.planet-casio.com', 6697) +shoutbox = Shoutbox(cookies) + +@irc.on(lambda m: m.to[1:] in channels) +def handle_irc(m): + shoutbox.post(f"{m.author}: {m.text}", m.to[1:]) + +@shoutbox.on(lambda m: m.channel in channels and m.author != "IRC") +def handle_shoutbox(m): + irc.send(f"#{m.channel}", f"{m.author}: {m.text}") + + +irc.start("Shoutbox", password, nick) +for c in channels: + irc.join(f"#{c}") + +shoutbox.run() +irc.run() diff --git a/irc.py b/irc.py new file mode 100644 index 0000000..974b552 --- /dev/null +++ b/irc.py @@ -0,0 +1,133 @@ +# Manage the IRC layer of GLaDOS + +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 """ + # 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, sasl_nick=None): + """ Start the IRC layer. Manage authentication as well """ + sasl_nick = sasl_nick or nick + 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 {sasl_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(f"received {message}") + if message is not None: + for event, callback in self._callbacks: + if event(message): + logging.info(f"matched {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 receive(self): + """ Receive a private message """ + while True: + message = self._recv() + if " PRIVMSG " in message: + msg = IRCMessage(message) + if msg: + return msg + + def join(self, channel): + """ Join a channel """ + self._send(f"JOIN {channel}") + logging.info(f"joined {channel}") + + def on(self, event): + """ Adds a callback to the IRC handler + Event is a function taking in parameter a IRCMessage 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)) + logging.info(f"added callback {func.__name__}") + 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) + logging.debug(f"received {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 IRCMessage(object): + r = re.compile("^:(?P[\w.~|]+)(?:!(?P\S+))? PRIVMSG (?P\S+) :(?P.+)") + + def __init__(self, raw): + match = re.search(IRCMessage.r, 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}") + else: + self.author = "" + self.to = "" + self.text = "" + logging.warning(f"failed to parse {raw} into valid message") + + def __str__(self): + return f"{self.author} to {self.to}: {self.text}" diff --git a/shoutbox.py b/shoutbox.py new file mode 100644 index 0000000..cab7e23 --- /dev/null +++ b/shoutbox.py @@ -0,0 +1,59 @@ +import json +import requests as r +import logging +import time +from functools import wraps +from queue import Queue +from threading import Thread + +class Shoutbox(object): + def __init__(self, cookies): + self.channels = {'annonces': 0, 'projets': 0, 'hs': 0} + self.cookies = cookies + self._callbacks = [] + + for channel, last_id in self.channels.items(): + messages = json.loads(r.get(f"https://www.planet-casio.com/Fr/shoutbox/api/read?since={last_id}&channel={channel}&format=text").text)['messages'] + for m in messages: + self.channels[channel] = m['id'] + + def run(self): + def handler(): + while True: + for channel, last_id in self.channels.items(): + messages = json.loads(r.get(f"https://www.planet-casio.com/Fr/shoutbox/api/read?since={last_id}&channel={channel}&format=text").text)['messages'] + for m in messages: + logging.debug(m) + self.channels[channel] = m['id'] + message = SBMessage(m, channel) + for event, callback in self._callbacks: + if event(message): + logging.info(f"matched {event.__name__}") + callback(message) + time.sleep(1) + Thread(target=handler).start() + + def on(self, event): + """ Adds a callback to the IRC handler + Event is a function taking in parameter a SBMessage 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)) + logging.info(f"added callback {func.__name__}") + return wrapper + + return callback + + def post(self, msg, channel): + r.post("https://www.planet-casio.com/Fr/shoutbox/api/post", + data={"message": msg, "channel": channel}, cookies=self.cookies) + +class SBMessage(object): + def __init__(self, raw, channel): + self.author = raw['author'] + self.channel = channel + self.text = raw['content']