Jelajahi Sumber

darkirc/meetbot: Rework code to work over a single connection

parazyd 2 tahun lalu
induk
melakukan
f8543f4143

+ 1 - 0
bin/darkirc/script/bots/meetbot/.gitignore

@@ -0,0 +1 @@
+meetbot.pickle

+ 123 - 139
bin/darkirc/script/bots/meetbot/meetbot.py

@@ -4,9 +4,6 @@ import logging
 import pickle
 from time import time
 
-from base58 import b58decode
-from nacl.public import PrivateKey, Box
-
 from meetbot_cfg import config
 
 # Initialized channels from the configuration
@@ -16,77 +13,122 @@ CHANS = {}
 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):
+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 existing pickle database")
+
+    host = config["host"]
+    port = config["port"]
+    nick = config["nick"]
+
     try:
-        logging.info("%s: Connecting to %s:%s", chan, host, port)
+        logging.info("Connecting to %s/%d", host, port)
         reader, writer = await asyncio.open_connection(host, port)
 
-        logging.debug("%s: Send CAP msg", chan)
-        msg = "CAP REQ : no-history\r\n"
+        logging.debug("--> %s/%d: CAP LS 302", host, port)
+        msg = "CAP LS 302\r\n"
         writer.write(msg.encode("utf-8"))
         await writer.drain()
 
-        logging.debug("%s: Send NICK msg", chan)
+        logging.debug("--> %s/%d: NICK %s", host, port, nick)
         msg = f"NICK {nick}\r\n"
         writer.write(msg.encode("utf-8"))
         await writer.drain()
 
-        logging.debug("%s: Send CAP END msg", chan)
-        msg = "CAP END\r\n"
+        logging.debug("--> %s/%d: USER %s * 0 %s", host, port, nick, nick)
+        msg = f"USER {nick} * 0 :{nick}\r\n"
         writer.write(msg.encode("utf-8"))
         await writer.drain()
 
-        logging.debug("%s: Send JOIN msg", chan)
-        msg = f"JOIN {chan}\r\n"
+        msg = await reader.readline()
+        msg = msg.decode("utf-8")
+        logging.debug("<-- %s/%d: %s", host, port, msg)
+
+        logging.debug("--> %s/%d: CAP REQ :no-history", host, port)
+        msg = "CAP REQ :no-history\r\n"
         writer.write(msg.encode("utf-8"))
         await writer.drain()
 
-        elapsed=0
+        msg = await reader.readline()
+        msg = msg.decode("utf-8")
+        logging.debug("<-- %s/%d: %s", host, port, msg)
+
+        logging.debug("--> %s/%d: CAP END", host, port)
+        msg = "CAP END\r\n"
+        writer.write(msg.encode("utf-8"))
+        await writer.drain()
+
+        msg = await reader.readline()
+        msg = msg.decode("utf-8")
+        logging.debug("<-- %s/%d: %s", host, port, msg)
+
+        for chan in config["channels"]:
+            chan = chan["name"]
+            logging.debug("--> %s/%d: JOIN %s", host, port, chan)
+            msg = f"JOIN {chan}\r\n"
+            writer.write(msg.encode("utf-8"))
+            await writer.drain()
+
+        channels = [chan["name"] for chan in config["channels"]]
+        logging.info("%s/%d: Listening to channels: %s", host, port, channels)
+
+        elapsed = 0
 
-        logging.info("%s: Listening to channel", chan)
         while True:
             msg = await reader.readline()
-            msg = msg.decode("utf8")
+            msg = msg.decode("utf-8")
             if not msg:
                 continue
 
             split_msg = msg.split(" ")
             command = split_msg[1]
+            chan = split_msg[2]
             nick_c = split_msg[0][1:].rsplit("!", 1)[0]
-            logging.debug("%s: Recv: %s", chan, msg.rstrip())
+            logging.debug("<-- %s/%d: %s", host, port, msg.rstrip())
 
             if command == "PRIVMSG":
                 msg_title = msg.split(" ")[3][1:].rstrip()
                 if not msg_title:
-                    logging.info("%s: Recv empty PRIVMSG, ignoring", chan)
                     continue
 
                 if msg_title == "!start":
                     logging.info("%s: Got !start", chan)
+
+                    if not CHANS.get(chan):
+                        CHANS[chan] = {}
+                        CHANS[chan]["topics"] = {}
+
                     topics = CHANS[chan]["topics"]
-                    reply = f"PRIVMSG {chan} :Meeting started"
-                    logging.info("%s: Send: %s", chan, reply)
-                    writer.write((reply + "\r\n").encode("utf-8"))
+
+                    reply = f"PRIVMSG {chan} :Meeting started\r\n"
+                    writer.write(reply.encode("utf-8"))
                     await writer.drain()
 
                     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"))
+                        reply = f"PRIVMSG {chan} :No topics\r\n"
+                        writer.write(reply.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"))
+                    reply = f"PRIVMSG {chan} :Topics:\r\n"
+                    writer.write(reply.encode("utf-8"))
                     await writer.drain()
 
                     for i, topic in enumerate(topics):
-                        reply = f"PRIVMSG {chan} :{i+1}. {topic}"
-                        logging.info("%s: Send: %s", chan, reply)
-                        writer.write((reply + "\r\n").encode("utf-8"))
+                        reply = f"PRIVMSG {chan} :{i+1}. {topic}\e\n"
+                        writer.write(reply.encode("utf-8"))
                         await writer.drain()
 
                     cur_topic = topics.pop(0)
@@ -94,19 +136,17 @@ async def channel_listen(host, port, nick, chan):
                     CHANS[chan]["topics"] = topics
                     writer.write(reply.encode("utf-8"))
                     await writer.drain()
-                    elapsed=time()
+                    elapsed = time()
                     continue
 
                 if msg_title == "!end":
                     logging.info("%s: Got !end", chan)
-                    reply = f"PRIVMSG {chan} :Elapsed time: {round((time() - elapsed)/60, 1)} min"
-                    elapsed=0
-                    logging.info("%s: Send: %s", chan, reply)
-                    writer.write((reply + "\r\n").encode("utf-8"))
+                    reply = f"PRIVMSG {chan} :Elapsed time: {round((time() - elapsed)/60, 1)} min\r\n"
+                    elapsed = 0
+                    writer.write(reply.encode("utf-8"))
                     await writer.drain()
-                    reply = f"PRIVMSG {chan} :Meeting ended"
-                    logging.info("%s: Send: %s", chan, reply)
-                    writer.write((reply + "\r\n").encode("utf-8"))
+                    reply = f"PRIVMSG {chan} :Meeting ended\r\n"
+                    writer.write(reply.encode("utf-8"))
                     await writer.drain()
                     continue
 
@@ -115,42 +155,41 @@ async def channel_listen(host, port, nick, chan):
                     topic = msg.split(" ", 4)
 
                     if len(topic) != 5:
-                        logging.debug("%s: Topic msg len not 5, skipping",
-                                      chan)
                         continue
 
                     topic = topic[4].rstrip() + f" (by {nick_c})"
 
                     if topic == "":
-                        logging.debug("%s: Topic message empty, skipping",
-                                      chan)
                         continue
 
+                    if not CHANS.get(chan):
+                        CHANS[chan] = {}
+                        CHANS[chan]["topics"] = {}
+
                     topics = CHANS[chan]["topics"]
                     if topic not in topics:
                         topics.append(topic)
                         CHANS[chan]["topics"] = topics
-                        logging.debug("%s: Appended topic to channel topics",
-                                      chan)
-                        reply = f"PRIVMSG {chan} :Added topic: {topic}"
-                        logging.info("%s: Send: %s", chan, reply)
+                        reply = f"PRIVMSG {chan} :Added topic: {topic}\r\n"
                     else:
-                        logging.debug("%s: Topic already in list of topics",
-                                      chan)
-                        reply = f"PRIVMSG {chan} :Topic already in list"
-                        logging.info("%s: Send: %s", chan, reply)
+                        reply = f"PRIVMSG {chan} :Topic already in list\r\n"
 
-                    writer.write((reply + "\r\n").encode("utf-8"))
+                    writer.write(reply.encode("utf-8"))
                     await writer.drain()
                     continue
 
                 if msg_title == "!deltopic":
                     logging.info("%s: Got !deltopic", chan)
+
+                    if not CHANS.get(chan):
+                        CHANS[chan] = {}
+                        CHANS[chan]["topics"] = {}
+
                     topics = CHANS[chan]["topics"]
+
                     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"))
+                        reply = f"PRIVMSG {chan} :No topics\r\n"
+                        writer.write(reply.encode("utf-8"))
                         await writer.drain()
                         continue
 
@@ -159,132 +198,77 @@ async def channel_listen(host, port, nick, chan):
                         topic = int(topic[4].rstrip())
                         del topics[topic-1]
                         CHANS[chan]["topics"] = topics
-                        reply = f"PRIVMSG {chan} :Removed topic {topic}"
-                        logging.info("%s: Send: %s", chan, reply)
-                        writer.write((reply + "\r\n").encode("utf-8"))
+                        reply = f"PRIVMSG {chan} :Removed topic {topic}\r\n"
+                        writer.write(reply.encode("utf-8"))
                         await writer.drain()
                     except:
-                        reply = f"PRIVMSG {chan} :Topic not found"
-                        logging.info("%s: Send: %s", chan, reply)
-                        writer.write((reply + "\r\n").encode("utf-8"))
+                        reply = f"PRIVMSG {chan} :Topic not found\r\n"
+                        writer.write(reply.encode("utf-8"))
                         await writer.drain()
 
                     continue
 
                 if msg_title == "!list":
                     logging.info("%s: Got !list", chan)
+
+                    if not CHANS.get(chan):
+                        CHANS[chan] = {}
+                        CHANS[chan]["topics"] = []
+
                     topics = CHANS[chan]["topics"]
                     if len(topics) == 0:
-                        reply = f"PRIVMSG {chan} :No topics"
+                        reply = f"PRIVMSG {chan} :No topics\r\n"
                     else:
-                        reply = f"PRIVMSG {chan} :Topics:"
+                        reply = f"PRIVMSG {chan} :Topics:\r\n"
 
-                    logging.info("%s: Send: %s", chan, reply)
-                    writer.write((reply + "\r\n").encode("utf-8"))
+                    writer.write(reply.encode("utf-8"))
                     await writer.drain()
 
                     for i, topic in enumerate(topics):
-                        reply = f"PRIVMSG {chan} :{i+1}. {topic}"
-                        logging.info("%s: Send: %s", chan, reply)
-                        writer.write((reply + "\r\n").encode("utf-8"))
+                        reply = f"PRIVMSG {chan} :{i+1}. {topic}\r\n"
+                        writer.write(reply.encode("utf-8"))
                         await writer.drain()
 
                     continue
 
                 if msg_title == "!next":
                     logging.info("%s: Got !next", chan)
+
+                    if not CHANS.get(chan):
+                        CHANS[chan] = {}
+                        CHANS[chan]["topics"] = []
+
                     topics = CHANS[chan]["topics"]
 
-                    reply = f"PRIVMSG {chan} :Elapsed time: {round((time() - elapsed)/60, 1)} min"
-                    logging.info("%s: Send: %s", chan, reply)
-                    writer.write((reply + "\r\n").encode("utf-8"))
+                    reply = f"PRIVMSG {chan} :Elapsed time: {round((time() - elapsed)/60, 1)}, min\r\n"
+                    writer.write(reply.encode("utf-8"))
                     await writer.drain()
-                    elapsed=time()
+                    elapsed = time()
 
                     if len(topics) == 0:
-                        reply = f"PRIVMSG {chan} :No further topics"
+                        reply = f"PRIVMSG {chan} :No further topics\r\n"
                     else:
                         cur_topic = topics.pop(0)
                         CHANS[chan]["topics"] = topics
-                        reply = f"PRIVMSG {chan} :Current topic: {cur_topic}"
+                        reply = f"PRIVMSG {chan} :Current topic: {cur_topic}\r\n"
 
-                    logging.info("%s: Send: %s", chan, reply)
-                    writer.write((reply + "\r\n").encode("utf-8"))
+                    writer.write(reply.encode("utf-8"))
                     await writer.drain()
                     continue
 
     except KeyboardInterrupt:
         return
     except ConnectionRefusedError:
-        logging.warning("%s: Connection refused, trying again in 3s...", chan)
-        await asyncio.sleep(3)
-        await channel_listen(host, port, nick, chan)
-    except Exception as e:
-        logging.error("EXCEPTION: %s", e)
-        logging.warn("%s: Connection interrupted. Reconnecting in 3s...", chan)
-        await asyncio.sleep(3)
-        await channel_listen(host, port, nick, chan)
-
-
-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("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
-        # itself has a configured secret or not.
-        # This way the ircd itself doesn't have to keep channel secrets, but
-        # 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("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
-
-        if not CHANS.get(name):
-            CHANS[name] = {}
-
-        if not CHANS[name].get("topics"):
-            CHANS[name]["topics"] = []
-
-        CHANS[name]["box"] = box
-
-    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)
-
-    await asyncio.gather(*coroutines)
+        logging.error("%s/%d: Connection refused", host, port)
+        return
 
 
 if __name__ == "__main__":
     from sys import argv
-    DBG = bool(len(argv) == 2 and argv[1] == "-v")
+    debug = "-v" in argv
 
     try:
-        asyncio.run(main(debug=DBG))
+        asyncio.run(main(debug=debug))
     except KeyboardInterrupt:
         print("\rCaught ^C, saving pickle and exiting")
 

+ 4 - 7
bin/darkirc/script/bots/meetbot/meetbot_cfg.py

@@ -7,17 +7,14 @@ config = {
 
     # IRC nickname
     "nick": "meetbot",
+
+    # Channels to join
     "channels": [
         {
-            "name": "#foo",
-            "secret": None,
+            "name": "#dev",
         },
         {
-            "name": "#secret_channel",
-            # TODO: This is useless right now, but it would be nice
-            # to add a CAP in ircd to give all incoming PRIVMSG to be
-            # able to check them.
-            "secret": "HNEKcUmwsspdaL9b8sFn45b8Rf3bzv1LdYS1JVNvkPGL",
+            "name": "#philosophy",
         },
     ],
 }