ircclient.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. #!/usr/bin/env python3
  2. import logging
  3. import os
  4. import queue
  5. import socket
  6. import subprocess
  7. import time
  8. from unzipper import unar
  9. from threading import Thread
  10. logging.basicConfig(level=logging.DEBUG)
  11. log = logging.getLogger(__name__)
  12. log.setLevel(logging.DEBUG)
  13. MODE_SEARCH = 'search'
  14. MODE_BOOK = 'book'
  15. class IRCClient(Thread):
  16. TIME_TO_FIRST_COMMAND = 30
  17. HOST = "irc.irchighway.net"
  18. PORT = 6667
  19. IGNORE = [
  20. "NOTICE",
  21. "PART",
  22. "QUIT",
  23. "332",
  24. "333",
  25. "372",
  26. "353",
  27. "366",
  28. "251",
  29. "252",
  30. "254",
  31. "255",
  32. "265",
  33. "266",
  34. "396"]
  35. CHANNEL = "#ebooks"
  36. PATH = "/tmp/"
  37. joined_channel = False
  38. connected = False
  39. name = "bookbot" + utils.random_hash()
  40. readbuffer = b''
  41. time_joined = None
  42. def __init__(self, q):
  43. super(IRCClient, self).__init__(daemon=True)
  44. self.command_queue = q
  45. self.send_queue = queue.Queue()
  46. nickstr = "NICK %s" % self.name
  47. userstr = "USER %s %s bla :%s" % (self.name, self.HOST, self.name)
  48. self.send_queue.put(nickstr)
  49. self.send_queue.put(userstr)
  50. def run(self):
  51. self.handle_connect()
  52. while True:
  53. if self.connected and self.joined_channel:
  54. self.handle_commands()
  55. self.process_send_queue()
  56. try:
  57. self.handle_read()
  58. except socket.timeout:
  59. continue
  60. except socket.error as e:
  61. log.error('socket error')
  62. log.exception(e)
  63. self.handle_connect()
  64. except Exception as e:
  65. log.exception(e)
  66. break
  67. self.handle_close()
  68. log.info('Exiting RUN')
  69. def handle_commands(self):
  70. if self.command_queue.empty():
  71. time.sleep(0.2)
  72. return
  73. elapsed = time.time() - self.time_joined
  74. if elapsed < self.TIME_TO_FIRST_COMMAND:
  75. log.info("commands to process, but we have to wait %d", self.TIME_TO_FIRST_COMMAND - elapsed)
  76. time.sleep(1)
  77. return
  78. command = self.command_queue.get()
  79. log.info("command %s", command)
  80. if command['mode'] == MODE_SEARCH:
  81. self.send_queue.put("PRIVMSG %s :@searchook %s " % (self.CHANNEL, command['query']))
  82. self.EXTENSION = command['grep'] # FIXME
  83. return
  84. self.send_queue.put("PRIVMSG %s :%s " % (self.CHANNEL, command['query']))
  85. def handle_connect(self):
  86. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  87. self.socket.connect((self.HOST, self.PORT))
  88. self.socket.settimeout(2)
  89. log.info("connected")
  90. self.set_status("CONNECTED")
  91. self.connected = True
  92. def handle_close(self):
  93. log.info("closed")
  94. self.set_status("DISCONNECTED")
  95. self.connected = False
  96. self.socket.close()
  97. def handle_read(self):
  98. try:
  99. data = self.socket.recv(4096)
  100. except socket.error as e:
  101. raise
  102. except Exception as e:
  103. raise
  104. if len(data) < 2:
  105. return
  106. # newline at the end?
  107. if not (data[-1] == 10 and data[-2] == 13):
  108. self.readbuffer += data
  109. return
  110. data = self.readbuffer + data
  111. self.readbuffer = b''
  112. # purge crap
  113. data = (data.replace(b'\x95', b'').replace(b'0xc2', b'').decode('utf-8', 'ignore'))
  114. lines = data.splitlines()
  115. for line in lines:
  116. words = line.split(' ')
  117. if len(words) < 2:
  118. continue
  119. msg_from = words[0]
  120. comm = words[1]
  121. if self.joined_channel:
  122. # log.debug(line)
  123. pass
  124. if comm in self.IGNORE:
  125. continue
  126. if comm == "JOIN":
  127. if self.name not in msg_from: # msg "NICK joined the channel" not about me
  128. continue
  129. self.set_status("JOINED")
  130. log.info("Joined channel %s" % self.CHANNEL)
  131. self.time_joined = time.time()
  132. self.joined_channel = True
  133. continue
  134. if comm == "PRIVMSG":
  135. # private message not addressed to me
  136. if words[2] != self.name:
  137. continue
  138. self.parse_msg(line)
  139. if comm == "PING" or msg_from == "PING": # respond ping to avoid getting kicked
  140. self.pong(line)
  141. continue
  142. if comm == "376": # END MOTD
  143. # MOTD complete, lets join the channel
  144. self.join_channel(self.CHANNEL)
  145. continue
  146. def handle_dcc(self, msg):
  147. msg = msg.split(':')[2]
  148. msg = msg.replace("\x01", "")
  149. log.info('msg %s', msg)
  150. if not msg.startswith("DCC"):
  151. return
  152. args = msg.replace("DCC SEND ", "").split(" ")
  153. size = int(args.pop())
  154. port = int(args.pop())
  155. ip = utils.ip_from_decimal(int(args.pop()))
  156. filename = "_".join(args).replace('"', '')
  157. self.set_status("RECEIVING")
  158. return self.netcat(ip, port, size, filename)
  159. # FIXME rename
  160. def parse_msg(self, msg):
  161. log.info('complete msg %s', msg)
  162. downloaded_filename = self.handle_dcc(msg)
  163. files = unar(downloaded_filename, self.PATH)
  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