ircclient.py 7.1 KB

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