import json import requests as r import logging import time import datetime import re from functools import wraps from threading import Thread from irc import IRC from sasl import nick, password from users import USERS class Shoutbox(object): def __init__(self, cookies): self.channels = {'annonces': 0, 'projets': 0, 'hs': 0} self.cookies = cookies self._callbacks = [] self.irc_clients = {} # pseudo: [IRC(), date] self.running = False self._handler = Thread(target=self._handle) 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=irc").text)['messages'] for m in messages: self.channels[channel] = m['id'] def run(self): self.running = True logging.debug("Thread start") self._handler.start() def stop(self): logging.debug("STOP: Stop requests to planet-casio.com") self.running = False logging.debug("STOP: Halt all irc user threads") for client in self.irc_clients: self.irc_clients[client][0].stop() self.irc_clients.pop(client) self._handler.join() logging.debug("STOP: Shoutbox thread closed") 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, user, msg, channel, users): if msg.startswith("ACTION"): msg = msg.replace("ACTION", "/me") # Look for pseudo v43-v5 translation for v43_name, v5_name in users: if v5_name.lower() == user.lower(): r.post("https://www.planet-casio.com/Fr/shoutbox/api/post-as", data={"user": v43_name, "message": msg, "channel": channel}, cookies=self.cookies) return # No translation found r.post("https://www.planet-casio.com/Fr/shoutbox/api/post-as", data={"user": "IRC", "message": f"{user} : {msg}", "channel": channel}, cookies=self.cookies) def normalize(pseudo): if pseudo.lower() in [u[0].lower() for u in USERS]: return [u[1] for u in USERS if u[0].lower() == pseudo.lower()][0] return re.sub(r'[^.A-Za-z0-9_]', '_', pseudo) def _handle(self): while self.running: try: for channel, last_id in self.channels.items(): # Do not spam with logs logging.getLogger().setLevel(logging.INFO) messages = json.loads(r.get(f"https://www.planet-casio.com/Fr/shoutbox/api/read?since={last_id}&channel={channel}&format=irc").text)['messages'] logging.getLogger().setLevel(logging.DEBUG) for m in messages: logging.debug(m) # If message comes from IRC, drop it (no loops allowed) if m['source'] == "IRC": continue # Get channel id, parse SBMessage self.channels[channel] = m['id'] message = SBMessage(m, channel) # If handler needs to be killed if not self.running: logging.debug("going to stop") break # For each callback defined with @decorator for event, callback in self._callbacks: author = Shoutbox.normalize(message.author) # client is not known or is disconnected if author not in self.irc_clients.keys() \ or self.irc_clients[author][0].running == False: self.irc_clients[author] = [ IRC('irc.planet-casio.com', 6697), datetime.datetime.now() ] # Start a thread for new client if self.irc_clients[author][0].start(f"{author}[s]", password, nick): logging.debug(f"{author} has joined IRC") # client is known but AFK else: self.irc_clients[author][1] = datetime.datetime.now() logging.debug(f"{author} has updated IRC") if event(message): logging.info(f"matched {event.__name__}") callback(message) # kill afk clients for k, c in self.irc_clients.items(): if datetime.datetime.now() - c[1] > datetime.timedelta(hours=1): logging.info(f"killing {c[0].nick}") c[0].stop() self.irc_clients.pop(k) except Exception as e: logging.error(f"Faillure in Shoutbox thread {e}") finally: time.sleep(3) class SBMessage(object): def __init__(self, raw, channel): self.author = raw['author'] self.channel = channel self.text = raw['content']