ircclient.py 7.0 KB

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