meetbot.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. #!/usr/bin/env python3
  2. import asyncio
  3. import logging
  4. from base58 import b58decode
  5. from nacl.public import PrivateKey, Box
  6. from meetbot_cfg import config
  7. # Initialized channels from the configuration
  8. CHANS = {}
  9. async def channel_listen(host, port, nick, chan):
  10. global CHANS
  11. logging.info(f"Connecting to {host}:{port}")
  12. reader, writer = await asyncio.open_connection(host, port)
  13. logging.info(f"{host}:{port} Send CAP msg")
  14. cap_msg = "CAP REQ : no-history\r\n"
  15. writer.write(cap_msg.encode("utf-8"))
  16. logging.info(f"{host}:{port} Send NICK msg")
  17. nick_msg = f"NICK {nick}\r\n"
  18. writer.write(nick_msg.encode("utf-8"))
  19. logging.info(f"{host}:{port} Send CAP END msg")
  20. cap_end_msg = "CAP END\r\n"
  21. writer.write(cap_end_msg.encode("utf-8"))
  22. logging.info(f"{host}:{port} Send JOIN msg for {chan}")
  23. join_msg = f"JOIN {chan}\r\n"
  24. writer.write(join_msg.encode("utf-8"))
  25. logging.info(f"{host}:{port} Listening to channel: {chan}")
  26. while True:
  27. msg = await reader.read(1024)
  28. msg = msg.decode("utf8")
  29. if not msg:
  30. continue
  31. command = msg.split(" ")[1]
  32. if command == "PRIVMSG":
  33. msg_title = msg.split(" ")[3][1:].rstrip()
  34. if not msg_title:
  35. logging.info("Got empty PRIVMSG, ignoring")
  36. continue
  37. if msg_title == "!start":
  38. topics = CHANS[chan]["topics"]
  39. reply = f"PRIVMSG {chan} :Meeting started\r\n"
  40. writer.write(reply.encode("utf-8"))
  41. await writer.drain()
  42. reply = f"PRIVMSG {chan} :Topics:\r\n"
  43. writer.write(reply.encode("utf-8"))
  44. await writer.drain()
  45. for i, topic in enumerate(topics):
  46. reply = f"PRIVMSG {chan} :1. {topic}\r\n"
  47. writer.write(reply.encode("utf-8"))
  48. await writer.drain()
  49. if len(topics) > 0:
  50. cur_topic = topics.pop(0)
  51. reply = f"PRIVMSG {chan} :Current topic: {cur_topic}\r\n"
  52. else:
  53. reply = f"PRIVMSG {chan} :No further topics\r\n"
  54. CHANS[chan]["topics"] = topics
  55. writer.write(reply.encode("utf-8"))
  56. await writer.drain()
  57. continue
  58. if msg_title == "!end":
  59. reply = f"PRIVMSG {chan} :Meeting ended\r\n"
  60. writer.write(reply.encode("utf-8"))
  61. await writer.drain()
  62. continue
  63. if msg_title == "!topic":
  64. topic = msg.split(" ", 4)
  65. if len(topic) != 5:
  66. continue
  67. topic = topic[4].rstrip()
  68. if topic == "":
  69. continue
  70. topics = CHANS[chan]["topics"]
  71. topics.append(topic)
  72. CHANS[chan]["topics"] = topics
  73. reply = f"PRIVMSG {chan} :Added topic: {topic}\r\n"
  74. writer.write(reply.encode("utf-8"))
  75. await writer.drain()
  76. continue
  77. if msg_title == "!list":
  78. topics = CHANS[chan]["topics"]
  79. if len(topics) == 0:
  80. reply = f"PRIVMSG {chan} :No set topics\r\n"
  81. else:
  82. reply = f"PRIVMSG {chan} :Topics:\r\n"
  83. writer.write(reply.encode("utf-8"))
  84. await writer.drain()
  85. for i, topic in enumerate(topics):
  86. reply = f"PRIVMSG {chan} :1. {topic}\r\n"
  87. writer.write(reply.encode("utf-8"))
  88. await writer.drain()
  89. continue
  90. if msg_title == "!next":
  91. topics = CHANS[chan]["topics"]
  92. if len(topics) == 0:
  93. reply = f"PRIVMSG {chan} :No further topics\r\n"
  94. else:
  95. cur_topic = topics.pop(0)
  96. CHANS[chan]["topics"] = topics
  97. reply = f"PRIVMSG {chan} :Current topic: {cur_topic}\r\n"
  98. writer.write(reply.encode("utf-8"))
  99. await writer.drain()
  100. continue
  101. return
  102. async def main():
  103. format = "%(asctime)s: %(message)s"
  104. logging.basicConfig(format=format, level=logging.INFO, datefmt="%H:%M:%S")
  105. for i in config["channels"]:
  106. name = i["name"]
  107. logging.info(f"Found config for channel {name}")
  108. # TODO: This will be useful when ircd has a CAP that tells it to
  109. # give **all** messages to the connected client, no matter if ircd
  110. # itself has a configured secret or not.
  111. # This way the ircd itself doesn't have to keep channel secrets, but
  112. # they can rather only be held by this bot. In turn this means the bot
  113. # can be deployed with any ircd.
  114. if i["secret"]:
  115. logging.info(f"Instantiating NaCl box for {name}")
  116. sk = b58decode(i["secret"].encode("utf-8"))
  117. sk = PrivateKey(sk)
  118. pk = sk.public_key
  119. box = Box(sk, pk)
  120. else:
  121. box = None
  122. CHANS[name] = {}
  123. CHANS[name]["box"] = box
  124. CHANS[name]["topics"] = []
  125. coroutines = []
  126. for i in CHANS.keys():
  127. task = asyncio.create_task(
  128. channel_listen(config["host"], config["port"], config["nick"], i))
  129. coroutines.append(task)
  130. await asyncio.gather(*coroutines)
  131. asyncio.run(main())