ircclient.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. #!/usr/bin/env python3
  2. import logging
  3. import os
  4. import queue
  5. import socket
  6. import subprocess
  7. import time
  8. import utils
  9. from unzipper import unar
  10. from threading import Thread
  11. logging.basicConfig(level=logging.DEBUG)
  12. log = logging.getLogger(__name__)
  13. log.setLevel(logging.DEBUG)
  14. MODE_SEARCH = 'search'
  15. MODE_BOOK = 'book'
  16. class IRCClient(Thread):
  17. TIME_TO_FIRST_COMMAND = 30
  18. HOST = "irc.irchighway.net"
  19. PORT = 6667
  20. IGNORE = [
  21. "NOTICE",
  22. "PART",
  23. "QUIT",
  24. "332",
  25. "333",
  26. "372",
  27. "353",
  28. "366",
  29. "251",
  30. "252",
  31. "254",
  32. "255",
  33. "265",
  34. "266",
  35. "396"]
  36. CHANNEL = "#ebooks"
  37. PATH = "/tmp/"
  38. joined_channel = False
  39. connected = False
  40. name = "bookbot" + utils.random_hash()
  41. readbuffer = b''
  42. time_joined = None
  43. def __init__(self, q):
  44. super(IRCClient, self).__init__(daemon=True)
  45. self.command_queue = q
  46. self.send_queue = queue.Queue()
  47. nickstr = "NICK %s" % self.name
  48. userstr = "USER %s %s bla :%s" % (self.name, self.HOST, self.name)
  49. self.send_queue.put(nickstr)
  50. self.send_queue.put(userstr)
  51. def run(self):
  52. self.handle_connect()
  53. while True:
  54. if self.connected and self.joined_channel:
  55. self.handle_commands()
  56. self.process_send_queue()
  57. try:
  58. self.handle_read()
  59. except socket.timeout:
  60. continue
  61. except socket.error as e:
  62. log.error('socket error')
  63. log.exception(e)
  64. self.handle_connect()
  65. except Exception as e:
  66. log.exception(e)
  67. break
  68. self.handle_close()
  69. log.info('Exiting RUN')
  70. def handle_commands(self):
  71. if self.command_queue.empty():
  72. time.sleep(0.2)
  73. return
  74. elapsed = time.time() - self.time_joined
  75. if elapsed < self.TIME_TO_FIRST_COMMAND:
  76. log.info("commands to process, but we have to wait %d", self.TIME_TO_FIRST_COMMAND - elapsed)
  77. time.sleep(1)
  78. return
  79. command = self.command_queue.get()
  80. log.info("command %s", command)
  81. if command['mode'] == MODE_SEARCH:
  82. self.send_queue.put("PRIVMSG %s :@searchook %s " % (self.CHANNEL, command['query']))
  83. self.EXTENSION = command['grep'] # FIXME
  84. return
  85. self.send_queue.put("PRIVMSG %s :%s " % (self.CHANNEL, command['query']))
  86. def handle_connect(self):
  87. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  88. self.socket.connect((self.HOST, self.PORT))
  89. self.socket.settimeout(2)
  90. log.info("connected")
  91. self.set_status("CONNECTED")
  92. self.connected = True
  93. def handle_close(self):
  94. log.info("closed")
  95. self.set_status("DISCONNECTED")
  96. self.connected = False
  97. self.socket.close()
  98. def get_data_from_irc(self):
  99. data = self.socket.recv(4096)
  100. if len(data) < 2:
  101. return ""
  102. # newline at the end?
  103. if not (data[-1] == 10 and data[-2] == 13):
  104. self.readbuffer += data
  105. return ""
  106. data = self.readbuffer + data
  107. self.readbuffer = b''
  108. # purge crap
  109. data = (data.replace(b'\x95', b'').replace(b'0xc2', b'').decode('utf-8', 'ignore'))
  110. return data
  111. def handle_read(self):
  112. lines = self.get_data_from_irc().splitlines()
  113. for line in lines:
  114. words = line.split(' ')
  115. if len(words) < 2:
  116. continue
  117. msg_from = words[0]
  118. comm = words[1]
  119. if self.joined_channel:
  120. # log.debug(line)
  121. pass
  122. if comm in self.IGNORE:
  123. continue
  124. if comm == "JOIN":
  125. if self.name not in msg_from: # msg "NICK joined the channel" not about me
  126. continue
  127. self.set_status("JOINED")
  128. log.info("Joined channel %s" % self.CHANNEL)
  129. self.time_joined = time.time()
  130. self.joined_channel = True
  131. continue
  132. if comm == "PRIVMSG":
  133. # private message not addressed to me
  134. if words[2] != self.name:
  135. continue
  136. log.info("privmsg: %s", line)
  137. dcc_args = self.get_dcc_args(line)
  138. if dcc_args is None:
  139. continue
  140. ip, port, size, filename = dcc_args
  141. downloaded_filename = self.netcat(ip, port, size, filename)
  142. # TODO save state in redis on ip port size filename + output of handle files
  143. files = unar(downloaded_filename, self.PATH)
  144. self.handle_files(files)
  145. if comm == "PING" or msg_from == "PING": # respond ping to avoid getting kicked
  146. self.pong(line)
  147. continue
  148. if comm == "376": # END MOTD
  149. # MOTD complete, lets join the channel
  150. self.join_channel(self.CHANNEL)
  151. continue
  152. def get_dcc_args(self, msg):
  153. msg = msg.split(':')[2]
  154. msg = msg.replace("\x01", "")
  155. if not msg.startswith("DCC"):
  156. return None
  157. args = msg.replace("DCC SEND ", "").split(" ")
  158. size = int(args.pop())
  159. port = int(args.pop())
  160. ip = utils.ip_from_decimal(int(args.pop()))
  161. filename = "_".join(args).replace('"', '')
  162. return ip, port, size, filename
  163. def handle_files(self, files):
  164. log.info("Unarchived files %s", files)
  165. list_of_books = False
  166. out = []
  167. for f in files:
  168. if "searchbot" in f.lower() or "searchook" in f.lower():
  169. list_of_books = True
  170. out.extend(self.list_books(f))
  171. if list_of_books:
  172. log.info("Final output %s", out)
  173. return
  174. log.info("Files: %s", files)
  175. ret = []
  176. for f in files:
  177. if f.lower().endswith(".epub"):
  178. log.info("EPUB %s" % f)
  179. new_fname = f.replace("epub", "mobi")
  180. p = subprocess.Popen(["ebook-convert", f, new_fname])
  181. p.wait()
  182. ret.append(new_fname)
  183. ret.append(f)
  184. # make paths absolute?
  185. ret = [i.replace(self.PATH, "") for i in ret]
  186. for filename in ret:
  187. log.info(filename)
  188. def list_books(self, f):
  189. f = open(f, "r")
  190. lines = f.readlines()
  191. ret = []
  192. for l in lines:
  193. if self.EXTENSION in l.lower() and l.startswith('!') and "htm" not in l.lower():
  194. ret.append(l.strip())
  195. log.info("Book matches: %s", l.strip())
  196. ret = list(set(ret)) # dedup
  197. return ret
  198. def netcat(self, ip, port, size, filename):
  199. filename = os.path.basename(filename).replace(" ", "_")
  200. log.info('netcat: %s %d %d %s', ip, port, size, filename)
  201. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  202. log.info("Receiving file")
  203. s.connect((ip, port))
  204. fname = os.path.join(self.PATH, filename)
  205. f = open(fname, 'wb')
  206. count = 0
  207. while True:
  208. data = s.recv(16384)
  209. if len(data) == 0:
  210. log.info("No data received - finished")
  211. break
  212. count += len(data)
  213. f.write(data)
  214. perc = int(100 * count / size)
  215. self.PROGRESS = perc
  216. self.set_status("DOWNLOADING") # % perc #progress
  217. log.info("Download percentage: %d", perc)
  218. if count >= size:
  219. break
  220. s.close()
  221. f.close()
  222. return fname
  223. def pong(self, data):
  224. msg = data.replace("PING ", "")
  225. self.send_queue.put("PONG %s" % msg)
  226. def join_channel(self, channel):
  227. self.send_queue.put("JOIN %s" % channel)
  228. def process_send_queue(self):
  229. if self.send_queue.empty():
  230. return
  231. data = self.send_queue.get()
  232. log.info("Sending %s", data)
  233. add = bytes(str(data), "utf-8") + bytes([13, 10])
  234. self.socket.send(add)
  235. def set_status(self, value):
  236. self.STATUS = value