From 94107fed312d013495c5287bca3041dc1aa08314 Mon Sep 17 00:00:00 2001 From: Luther Blissett Date: Sat, 24 Sep 2022 22:22:41 +0200 Subject: [PATCH] meetbot: Better debugging, and persistent channel data (including topics). --- .gitignore | 1 + bin/ircd/script/meetbot.py | 166 +++++++++++++++++++++++++------------ 2 files changed, 112 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index c7ecf9d11..8f983f260 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyc +*.pickle *.sage.py *.zk.bin *_circuit_layout.png diff --git a/bin/ircd/script/meetbot.py b/bin/ircd/script/meetbot.py index cd12a1147..f039edfd7 100755 --- a/bin/ircd/script/meetbot.py +++ b/bin/ircd/script/meetbot.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import asyncio import logging +import pickle from base58 import b58decode from nacl.public import PrivateKey, Box @@ -10,131 +11,171 @@ from meetbot_cfg import config # Initialized channels from the configuration CHANS = {} +# Pickle DB +PICKLE_DB = "meetbot.pickle" + +# TODO: while this is nice to support, it would perhaps be better to do it +# all over the same connection rather than opening a socket for each channel. async def channel_listen(host, port, nick, chan): - global CHANS - - logging.info(f"Connecting to {host}:{port}") + logging.info("%s: Connecting to %s:%s", chan, host, port) reader, writer = await asyncio.open_connection(host, port) - logging.info(f"{host}:{port} Send CAP msg") - cap_msg = "CAP REQ : no-history\r\n" - writer.write(cap_msg.encode("utf-8")) + logging.debug("%s: Send CAP msg", chan) + msg = "CAP REQ : no-history\r\n" + writer.write(msg.encode("utf-8")) - logging.info(f"{host}:{port} Send NICK msg") - nick_msg = f"NICK {nick}\r\n" - writer.write(nick_msg.encode("utf-8")) + logging.debug("%s: Send NICK msg", chan) + msg = f"NICK {nick}\r\n" + writer.write(msg.encode("utf-8")) - logging.info(f"{host}:{port} Send CAP END msg") - cap_end_msg = "CAP END\r\n" - writer.write(cap_end_msg.encode("utf-8")) + logging.debug("%s: Send CAP END msg", chan) + msg = "CAP END\r\n" + writer.write(msg.encode("utf-8")) - logging.info(f"{host}:{port} Send JOIN msg for {chan}") - join_msg = f"JOIN {chan}\r\n" - writer.write(join_msg.encode("utf-8")) + logging.debug("%s: Send JOIN msg", chan) + msg = f"JOIN {chan}\r\n" + writer.write(msg.encode("utf-8")) - logging.info(f"{host}:{port} Listening to channel: {chan}") + logging.info("%s: Listening to channel", chan) while True: - msg = await reader.read(1024) + msg = await reader.readline() msg = msg.decode("utf8") if not msg: continue command = msg.split(" ")[1] + logging.debug("%s: Recv: %s", chan, msg.rstrip()) if command == "PRIVMSG": msg_title = msg.split(" ")[3][1:].rstrip() if not msg_title: - logging.info("Got empty PRIVMSG, ignoring") + logging.info("%s: Recv empty PRIVMSG, ignoring", chan) continue if msg_title == "!start": + logging.info("%s: Got !start", chan) topics = CHANS[chan]["topics"] - reply = f"PRIVMSG {chan} :Meeting started\r\n" - writer.write(reply.encode("utf-8")) + reply = f"PRIVMSG {chan} :Meeting started" + logging.info("%s: Send: %s", chan, reply) + writer.write((reply + "\r\n").encode("utf-8")) await writer.drain() - reply = f"PRIVMSG {chan} :Topics:\r\n" - writer.write(reply.encode("utf-8")) + if len(topics) == 0: + reply = f"PRIVMSG {chan} :No topics" + logging.info("%s: Send: %s", chan, reply) + writer.write((reply + "\r\n").encode("utf-8")) + await writer.drain() + continue + + reply = f"PRIVMSG {chan} :Topics:" + logging.info("%s: Send: %s", chan, reply) + writer.write((reply + "\r\n").encode("utf-8")) await writer.drain() for i, topic in enumerate(topics): - reply = f"PRIVMSG {chan} :1. {topic}\r\n" - writer.write(reply.encode("utf-8")) + reply = f"PRIVMSG {chan} :{i+1}. {topic}" + logging.info("%s: Send: %s", chan, reply) + writer.write((reply + "\r\n").encode("utf-8")) await writer.drain() - if len(topics) > 0: - cur_topic = topics.pop(0) - reply = f"PRIVMSG {chan} :Current topic: {cur_topic}\r\n" - else: - reply = f"PRIVMSG {chan} :No further topics\r\n" - + cur_topic = topics.pop(0) + reply = f"PRIVMSG {chan} :Current topic: {cur_topic}\r\n" CHANS[chan]["topics"] = topics - writer.write(reply.encode("utf-8")) await writer.drain() continue if msg_title == "!end": - reply = f"PRIVMSG {chan} :Meeting ended\r\n" - writer.write(reply.encode("utf-8")) + logging.info("%s: Got !end", chan) + reply = f"PRIVMSG {chan} :Meeting ended" + logging.info("%s: Send: %s", chan, reply) + writer.write((reply + "\r\n").encode("utf-8")) await writer.drain() continue if msg_title == "!topic": + logging.info("%s: Got !topic", chan) topic = msg.split(" ", 4) + if len(topic) != 5: + logging.debug("%s: Topic msg len not 5, skipping", chan) continue + topic = topic[4].rstrip() + if topic == "": + logging.debug("%s: Topic message empty, skipping", chan) continue + topics = CHANS[chan]["topics"] topics.append(topic) CHANS[chan]["topics"] = topics - reply = f"PRIVMSG {chan} :Added topic: {topic}\r\n" - writer.write(reply.encode("utf-8")) + logging.debug("%s: Appended topic to channel topics", chan) + + reply = f"PRIVMSG {chan} :Added topic: {topic}" + logging.info("%s: Send: %s", chan, reply) + writer.write((reply + "\r\n").encode("utf-8")) await writer.drain() continue if msg_title == "!list": + logging.info("%s: Got !list", chan) topics = CHANS[chan]["topics"] if len(topics) == 0: - reply = f"PRIVMSG {chan} :No set topics\r\n" + reply = f"PRIVMSG {chan} :No topics" else: - reply = f"PRIVMSG {chan} :Topics:\r\n" - writer.write(reply.encode("utf-8")) + reply = f"PRIVMSG {chan} :Topics:" + + logging.info("%s: Send: %s", chan, reply) + writer.write((reply + "\r\n").encode("utf-8")) await writer.drain() for i, topic in enumerate(topics): - reply = f"PRIVMSG {chan} :1. {topic}\r\n" - writer.write(reply.encode("utf-8")) + reply = f"PRIVMSG {chan} :{i+1}. {topic}" + logging.info("%s: Send: %s", chan, reply) + writer.write((reply + "\r\n").encode("utf-8")) await writer.drain() continue if msg_title == "!next": + logging.info("%s: Got !next", chan) topics = CHANS[chan]["topics"] if len(topics) == 0: - reply = f"PRIVMSG {chan} :No further topics\r\n" + reply = f"PRIVMSG {chan} :No further topics" else: cur_topic = topics.pop(0) CHANS[chan]["topics"] = topics - reply = f"PRIVMSG {chan} :Current topic: {cur_topic}\r\n" + reply = f"PRIVMSG {chan} :Current topic: {cur_topic}" - writer.write(reply.encode("utf-8")) + logging.info("%s: Send: %s", chan, reply) + writer.write((reply + "\r\n").encode("utf-8")) await writer.drain() continue return -async def main(): - format = "%(asctime)s: %(message)s" - logging.basicConfig(format=format, level=logging.INFO, datefmt="%H:%M:%S") +async def main(debug=False): + global CHANS + + loglevel = logging.DEBUG if debug else logging.INFO + logfmt = "%(asctime)s [%(levelname)s]\t%(message)s" + logging.basicConfig(format=logfmt, + level=loglevel, + datefmt="%Y-%m-%d %H:%M:%S") + + try: + with open(PICKLE_DB, "rb") as pickle_fd: + CHANS = pickle.load(pickle_fd) + logging.info("Loaded pickle database") + except: + logging.info("Did not find pickle database") for i in config["channels"]: name = i["name"] - logging.info(f"Found config for channel {name}") + logging.info("Found config for channel %s", name) # TODO: This will be useful when ircd has a CAP that tells it to # give **all** messages to the connected client, no matter if ircd @@ -143,20 +184,25 @@ async def main(): # they can rather only be held by this bot. In turn this means the bot # can be deployed with any ircd. if i["secret"]: - logging.info(f"Instantiating NaCl box for {name}") - sk = b58decode(i["secret"].encode("utf-8")) - sk = PrivateKey(sk) - pk = sk.public_key - box = Box(sk, pk) + logging.info("Instantiating NaCl box for %s", name) + secret = b58decode(i["secret"].encode("utf-8")) + secret = PrivateKey(secret) + public = secret.public_key + box = Box(secret, public) else: box = None - CHANS[name] = {} + if not CHANS.get(name): + CHANS[name] = {} + + if not CHANS[name].get("topics"): + CHANS[name]["topics"] = [] + CHANS[name]["box"] = box - CHANS[name]["topics"] = [] coroutines = [] for i in CHANS.keys(): + logging.debug("Creating async task for %s", i) task = asyncio.create_task( channel_listen(config["host"], config["port"], config["nick"], i)) coroutines.append(task) @@ -164,4 +210,14 @@ async def main(): await asyncio.gather(*coroutines) -asyncio.run(main()) +if __name__ == "__main__": + from sys import argv + DBG = bool(len(argv) == 2 and argv[1] == "-v") + + try: + asyncio.run(main(debug=DBG)) + except: + print("\rCaught ^C, saving pickle and exiting") + + with open(PICKLE_DB, "wb") as fdesc: + pickle.dump(CHANS, fdesc, protocol=pickle.HIGHEST_PROTOCOL)