import json import requests as r import logging import time import datetime 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=text").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 pseudo.replace(' ', '_') 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=text").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']