meetbot.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. #!/usr/bin/env python3
  2. import asyncio
  3. import logging
  4. import pickle
  5. from time import time
  6. from meetbot_cfg import config
  7. # Initialized channels from the configuration
  8. CHANS = {}
  9. # Pickle DB
  10. PICKLE_DB = "meetbot.pickle"
  11. async def main(debug=False):
  12. global CHANS
  13. loglevel = logging.DEBUG if debug else logging.INFO
  14. logfmt = "%(asctime)s [%(levelname)s]\t%(message)s"
  15. logging.basicConfig(format=logfmt,
  16. level=loglevel,
  17. datefmt="%Y-%m-%d %H:%M:%S")
  18. try:
  19. with open(PICKLE_DB, "rb") as pickle_fd:
  20. CHANS = pickle.load(pickle_fd)
  21. logging.info("Loaded pickle database")
  22. except:
  23. logging.info("Did not find existing pickle database")
  24. host = config["host"]
  25. port = config["port"]
  26. nick = config["nick"]
  27. try:
  28. logging.info("Connecting to %s/%d", host, port)
  29. reader, writer = await asyncio.open_connection(host, port)
  30. logging.debug("--> %s/%d: CAP LS 302", host, port)
  31. msg = "CAP LS 302\r\n"
  32. writer.write(msg.encode("utf-8"))
  33. await writer.drain()
  34. logging.debug("--> %s/%d: NICK %s", host, port, nick)
  35. msg = f"NICK {nick}\r\n"
  36. writer.write(msg.encode("utf-8"))
  37. await writer.drain()
  38. logging.debug("--> %s/%d: USER %s * 0 %s", host, port, nick, nick)
  39. msg = f"USER {nick} * 0 :{nick}\r\n"
  40. writer.write(msg.encode("utf-8"))
  41. await writer.drain()
  42. msg = await reader.readline()
  43. msg = msg.decode("utf-8")
  44. logging.debug("<-- %s/%d: %s", host, port, msg)
  45. logging.debug("--> %s/%d: CAP REQ :no-history", host, port)
  46. msg = "CAP REQ :no-history\r\n"
  47. writer.write(msg.encode("utf-8"))
  48. await writer.drain()
  49. logging.debug("--> %s/%d: CAP REQ :no-autojoin", host, port)
  50. msg = "CAP REQ :no-autojoin\r\n"
  51. writer.write(msg.encode("utf-8"))
  52. await writer.drain()
  53. msg = await reader.readline()
  54. msg = msg.decode("utf-8")
  55. logging.debug("<-- %s/%d: %s", host, port, msg)
  56. logging.debug("--> %s/%d: CAP END", host, port)
  57. msg = "CAP END\r\n"
  58. writer.write(msg.encode("utf-8"))
  59. await writer.drain()
  60. msg = await reader.readline()
  61. msg = msg.decode("utf-8")
  62. logging.debug("<-- %s/%d: %s", host, port, msg)
  63. for chan in config["channels"]:
  64. chan = chan["name"]
  65. logging.debug("--> %s/%d: JOIN %s", host, port, chan)
  66. msg = f"JOIN {chan}\r\n"
  67. writer.write(msg.encode("utf-8"))
  68. await writer.drain()
  69. channels = [chan["name"] for chan in config["channels"]]
  70. logging.info("%s/%d: Listening to channels: %s", host, port, channels)
  71. elapsed = 0
  72. while True:
  73. msg = await reader.readline()
  74. msg = msg.decode("utf-8")
  75. if not msg:
  76. continue
  77. split_msg = msg.split(" ")
  78. command = split_msg[1]
  79. chan = split_msg[2]
  80. nick_c = split_msg[0][1:].rsplit("!", 1)[0]
  81. logging.debug("<-- %s/%d: %s", host, port, msg.rstrip())
  82. if command == "PRIVMSG":
  83. msg_title = msg.split(" ")[3][1:].rstrip()
  84. if not msg_title:
  85. continue
  86. if msg_title == "!start":
  87. logging.info("%s: Got !start", chan)
  88. if not CHANS.get(chan):
  89. CHANS[chan] = {}
  90. CHANS[chan]["topics"] = {}
  91. topics = CHANS[chan]["topics"]
  92. reply = f"PRIVMSG {chan} :Meeting started\r\n"
  93. writer.write(reply.encode("utf-8"))
  94. await writer.drain()
  95. if len(topics) == 0:
  96. reply = f"PRIVMSG {chan} :No topics\r\n"
  97. writer.write(reply.encode("utf-8"))
  98. await writer.drain()
  99. continue
  100. reply = f"PRIVMSG {chan} :Topics:\r\n"
  101. writer.write(reply.encode("utf-8"))
  102. await writer.drain()
  103. for i, topic in enumerate(topics):
  104. reply = f"PRIVMSG {chan} :{i+1}. {topic}\r\n"
  105. writer.write(reply.encode("utf-8"))
  106. await writer.drain()
  107. cur_topic = topics.pop(0)
  108. reply = f"PRIVMSG {chan} :Current topic: {cur_topic}\r\n"
  109. CHANS[chan]["topics"] = topics
  110. writer.write(reply.encode("utf-8"))
  111. await writer.drain()
  112. elapsed = time()
  113. continue
  114. if msg_title == "!end":
  115. logging.info("%s: Got !end", chan)
  116. reply = f"PRIVMSG {chan} :Elapsed time: {round((time() - elapsed)/60, 1)} min\r\n"
  117. elapsed = 0
  118. writer.write(reply.encode("utf-8"))
  119. await writer.drain()
  120. reply = f"PRIVMSG {chan} :Meeting ended\r\n"
  121. writer.write(reply.encode("utf-8"))
  122. await writer.drain()
  123. continue
  124. if msg_title == "!topic":
  125. logging.info("%s: Got !topic", chan)
  126. topic = msg.split(" ", 4)
  127. if len(topic) != 5:
  128. continue
  129. topic = topic[4].rstrip() + f" (by {nick_c})"
  130. if topic == "":
  131. continue
  132. if not CHANS.get(chan):
  133. CHANS[chan] = {}
  134. CHANS[chan]["topics"] = {}
  135. topics = CHANS[chan]["topics"]
  136. if topic not in topics:
  137. topics.append(topic)
  138. CHANS[chan]["topics"] = topics
  139. reply = f"PRIVMSG {chan} :Added topic: {topic}\r\n"
  140. else:
  141. reply = f"PRIVMSG {chan} :Topic already in list\r\n"
  142. writer.write(reply.encode("utf-8"))
  143. await writer.drain()
  144. continue
  145. if msg_title == "!deltopic":
  146. logging.info("%s: Got !deltopic", chan)
  147. if not CHANS.get(chan):
  148. CHANS[chan] = {}
  149. CHANS[chan]["topics"] = {}
  150. topics = CHANS[chan]["topics"]
  151. if len(topics) == 0:
  152. reply = f"PRIVMSG {chan} :No topics\r\n"
  153. writer.write(reply.encode("utf-8"))
  154. await writer.drain()
  155. continue
  156. try:
  157. topic = msg.split(" ", 4)
  158. topic = int(topic[4].rstrip())
  159. del topics[topic-1]
  160. CHANS[chan]["topics"] = topics
  161. reply = f"PRIVMSG {chan} :Removed topic {topic}\r\n"
  162. writer.write(reply.encode("utf-8"))
  163. await writer.drain()
  164. except:
  165. reply = f"PRIVMSG {chan} :Topic not found\r\n"
  166. writer.write(reply.encode("utf-8"))
  167. await writer.drain()
  168. continue
  169. if msg_title == "!list":
  170. logging.info("%s: Got !list", chan)
  171. if not CHANS.get(chan):
  172. CHANS[chan] = {}
  173. CHANS[chan]["topics"] = []
  174. topics = CHANS[chan]["topics"]
  175. if len(topics) == 0:
  176. reply = f"PRIVMSG {chan} :No topics\r\n"
  177. else:
  178. reply = f"PRIVMSG {chan} :Topics:\r\n"
  179. writer.write(reply.encode("utf-8"))
  180. await writer.drain()
  181. for i, topic in enumerate(topics):
  182. reply = f"PRIVMSG {chan} :{i+1}. {topic}\r\n"
  183. writer.write(reply.encode("utf-8"))
  184. await writer.drain()
  185. continue
  186. if msg_title == "!next":
  187. logging.info("%s: Got !next", chan)
  188. if not CHANS.get(chan):
  189. CHANS[chan] = {}
  190. CHANS[chan]["topics"] = []
  191. topics = CHANS[chan]["topics"]
  192. reply = f"PRIVMSG {chan} :Elapsed time: {round((time() - elapsed)/60, 1)}, min\r\n"
  193. writer.write(reply.encode("utf-8"))
  194. await writer.drain()
  195. elapsed = time()
  196. if len(topics) == 0:
  197. reply = f"PRIVMSG {chan} :No further topics\r\n"
  198. else:
  199. cur_topic = topics.pop(0)
  200. CHANS[chan]["topics"] = topics
  201. reply = f"PRIVMSG {chan} :Current topic: {cur_topic}\r\n"
  202. writer.write(reply.encode("utf-8"))
  203. await writer.drain()
  204. continue
  205. except KeyboardInterrupt:
  206. return
  207. except ConnectionRefusedError:
  208. logging.error("%s/%d: Connection refused", host, port)
  209. return
  210. if __name__ == "__main__":
  211. from sys import argv
  212. debug = "-v" in argv
  213. try:
  214. asyncio.run(main(debug=debug))
  215. except KeyboardInterrupt:
  216. print("\rCaught ^C, saving pickle and exiting")
  217. with open(PICKLE_DB, "wb") as fdesc:
  218. pickle.dump(CHANS, fdesc, protocol=pickle.HIGHEST_PROTOCOL)