ircclient.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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. self.mode = command['mode']
  95. if command['mode'] == MODE_SEARCH:
  96. self.send_queue.put("PRIVMSG %s :@searchook %s " % (self.CHANNEL, command['query']))
  97. return
  98. self.send_queue.put("PRIVMSG %s :%s " % (self.CHANNEL, command['query']))
  99. def handle_connect(self):
  100. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  101. self.socket.connect((self.HOST, self.PORT))
  102. self.socket.settimeout(2)
  103. log.info("connected")
  104. self.set_status("CONNECTED")
  105. self.connected = True
  106. def handle_close(self):
  107. log.info("closed")
  108. self.set_status("DISCONNECTED")
  109. self.connected = False
  110. self.socket.close()
  111. def get_data_from_irc(self):
  112. data = self.socket.recv(4096)
  113. if len(data) < 2:
  114. return ""
  115. # newline at the end?
  116. if not (data[-1] == 10 and data[-2] == 13):
  117. self.readbuffer += data
  118. return ""
  119. data = self.readbuffer + data
  120. self.readbuffer = b''
  121. # purge crap
  122. data = (data.replace(b'\x95', b'').replace(b'0xc2', b'').decode('utf-8', 'ignore'))
  123. return data
  124. def handle_read(self):
  125. lines = self.get_data_from_irc().splitlines()
  126. for line in lines:
  127. words = line.split(' ')
  128. if len(words) < 2:
  129. continue
  130. msg_from = words[0]
  131. comm = words[1]
  132. if self.joined_channel:
  133. # log.debug(line)
  134. pass
  135. if comm in self.IGNORE:
  136. continue
  137. if comm == "JOIN":
  138. if self.name not in msg_from: # msg "NICK joined the channel" not about me
  139. continue
  140. self.set_status("JOINED")
  141. log.info("Joined channel %s", self.CHANNEL)
  142. self.time_joined = time.time()
  143. self.joined_channel = True
  144. continue
  145. if comm == "PRIVMSG":
  146. # private message not addressed to me
  147. if words[2] != self.name:
  148. continue
  149. log.info("privmsg: %s", line)
  150. dcc_args = get_dcc_args(line)
  151. if dcc_args is None:
  152. continue
  153. ip, port, size, filename = dcc_args
  154. downloaded_filename = self.netcat(ip, port, size, filename)
  155. # TODO save state in redis on ip port size filename + output of handle files
  156. files = unar(downloaded_filename, self.PATH)
  157. self.results_queue.put({'type': 'files', 'files': files, 'mode': self.mode})
  158. if comm == "PING" or msg_from == "PING": # respond ping to avoid getting kicked
  159. self.pong(line)
  160. continue
  161. if comm == "376": # END MOTD
  162. # MOTD complete, lets join the channel
  163. self.join_channel(self.CHANNEL)
  164. continue
  165. def netcat(self, ip, port, size, filename):
  166. filename = os.path.basename(filename).replace(" ", "_")
  167. log.info('netcat: %s %d %d %s', ip, port, size, filename)
  168. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  169. log.info("Receiving file")
  170. s.connect((ip, port))
  171. fname = os.path.join(self.PATH, filename)
  172. f = open(fname, 'wb')
  173. count = 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:
  184. log.info("Download percentage: %d", perc)
  185. if count >= size:
  186. break
  187. s.close()
  188. f.close()
  189. return fname
  190. def pong(self, data):
  191. msg = data.replace("PING ", "")
  192. self.send_queue.put("PONG %s" % msg)
  193. def join_channel(self, channel):
  194. self.send_queue.put("JOIN %s" % channel)
  195. def process_send_queue(self):
  196. if self.send_queue.empty():
  197. return
  198. data = self.send_queue.get()
  199. log.info("Sending %s", data)
  200. add = bytes(str(data), "utf-8") + bytes([13, 10])
  201. self.socket.send(add)
  202. def set_status(self, value):
  203. self.STATUS = value