bot.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. #!/usr/bin/env python3
  2. import random
  3. import threading
  4. import socket
  5. import subprocess
  6. import sys
  7. from unzipper import unar
  8. class IRCClient():
  9. HOST = "irc.irchighway.net"
  10. PORT = 6667
  11. NICK = ""
  12. IDENT = ""
  13. REALNAME = ""
  14. IGNORE = [
  15. "NOTICE",
  16. "PART",
  17. "QUIT",
  18. "332",
  19. "333",
  20. "372",
  21. "353",
  22. "366",
  23. "251",
  24. "252",
  25. "254",
  26. "255",
  27. "265",
  28. "266",
  29. "396"]
  30. CHANNEL = "#ebooks"
  31. STATUS = ""
  32. PROGRESS = 0
  33. TYPE = "SEARCH"
  34. OUTPUT = ""
  35. buffer = []
  36. readbuffer = b''
  37. NOT_EXITED = True
  38. EXTRA_OUTPUT = ""
  39. PATH = ""
  40. joined = False
  41. connected = False
  42. def __init__(self, book, extension, t, cb, logging=False, path="/tmp/"):
  43. self.TYPE = t
  44. self.EXTENSION = extension
  45. self.LOOKING_FOR = book
  46. self.LOGGING = logging
  47. self.PATH = path
  48. name = "bookbot" + random_hash()
  49. self.NICK = name
  50. self.IDENT = name
  51. self.REALNAME = name
  52. self.callback = cb
  53. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  54. self.socket.connect((self.HOST, self.PORT))
  55. self.handle_connect()
  56. nickstr = "NICK %s" % self.NICK
  57. userstr = "USER %s %s bla :%s" % (self.IDENT, self.HOST, self.REALNAME)
  58. self.t = threading.Thread(target=self.handle_read)
  59. self.t.start()
  60. self.send_queue(nickstr)
  61. self.send_queue(userstr)
  62. def handle_connect(self):
  63. self.log("connected")
  64. self.set_status("CONNECTED")
  65. self.connected = True
  66. def handle_close(self):
  67. self.NOT_EXITED = False
  68. self.log("closed")
  69. self.set_status("DISCONNECTED")
  70. self.connected = False
  71. self.socket.close()
  72. def do_timeout(self):
  73. if not self.NOT_EXITED:
  74. return
  75. self.NOT_EXITED = False
  76. self.log("Abort mission")
  77. self.set_status("TIMED OUT")
  78. self.connected = False
  79. self.socket.close()
  80. def handle_read(self):
  81. while self.NOT_EXITED:
  82. try:
  83. data = self.socket.recv(1024)
  84. except socket.error:
  85. break
  86. if len(data) < 2:
  87. continue
  88. if not (data[-1] == 10 and data[-2] == 13):
  89. self.readbuffer += data
  90. continue
  91. data = self.readbuffer + data
  92. # purge mojibake
  93. data = (data.replace(b'\x95', b'')
  94. .replace(b'0xc2', b'')
  95. .decode('utf-8', 'ignore'))
  96. self.readbuffer = b''
  97. lines = data.splitlines()
  98. for line in lines:
  99. words = line.split(' ')
  100. if len(words) < 2:
  101. continue
  102. msg_from = words[0]
  103. comm = words[1]
  104. if comm in self.IGNORE:
  105. continue
  106. if comm == "JOIN":
  107. if self.NICK not in msg_from: # msg "NICK joined the channel" not about me
  108. continue
  109. self.set_status("JOINED")
  110. self.log("Joined channel %s" % self.CHANNEL)
  111. self.run_query()
  112. self.joined = True
  113. continue
  114. if comm == "PRIVMSG":
  115. # private message not addressed to me
  116. if words[2] != self.NICK:
  117. continue
  118. self.parse_msg(line)
  119. if comm == "PING" or msg_from == "PING": # respond ping to avoid getting kicked
  120. self.pong(line)
  121. continue
  122. if comm == "376": # END MOTD
  123. # MOTD complete, lets join the channel
  124. self.join_channel(self.CHANNEL)
  125. continue
  126. if not self.joined: # waiting
  127. continue
  128. def run_query(self):
  129. self.set_status("WAITING")
  130. if self.TYPE == "SEARCH":
  131. self.search(self.LOOKING_FOR)
  132. return
  133. self.book(self.LOOKING_FOR)
  134. def parse_msg(self, msg):
  135. msg = msg.split(':')[2]
  136. msg = msg.replace("\x01", "")
  137. if not msg.startswith("DCC"):
  138. self.EXTRA_OUTPUT = msg
  139. self.log(msg)
  140. return
  141. args = msg.replace("DCC SEND ", "").split(" ")
  142. size = int(args.pop())
  143. port = int(args.pop())
  144. ip = ip_from_decimal(int(args.pop()))
  145. filename = "_".join(args).replace('"', '')
  146. self.set_status("RECEIVING")
  147. n = self.netcat(ip, port, size, filename)
  148. files = unar(n, self.PATH)
  149. print("Unarchived files", files)
  150. out = []
  151. for f in files:
  152. if "searchbot" in f.lower() or "searchook" in f.lower():
  153. out.append(self.list_books(f))
  154. print("Current output", out)
  155. self.OUTPUT = []
  156. if len(out) > 0:
  157. if isinstance(out[0], list):
  158. print("1")
  159. self.OUTPUT = [item for sublist in out for item in sublist]
  160. else:
  161. print("2")
  162. self.OUTPUT = out
  163. print("3")
  164. self.OUTPUT = list(set(self.OUTPUT))
  165. else:
  166. print("4")
  167. self.OUTPUT = files
  168. print(self.OUTPUT)
  169. if self.TYPE == "BOOK":
  170. self.log("Files: ")
  171. self.log(self.OUTPUT)
  172. for f in self.OUTPUT:
  173. if f.lower().endswith(".epub"):
  174. print("EPUB %s" % f)
  175. new_fname = f.replace("epub", "mobi")
  176. p = subprocess.Popen(["ebook-convert", f, new_fname])
  177. p.wait()
  178. self.OUTPUT.append(new_fname)
  179. break
  180. self.OUTPUT = [i.replace(self.PATH, "") for i in self.OUTPUT]
  181. self.handle_close()
  182. self.set_status("FINISHED")
  183. def list_books(self, f):
  184. f = open(f, "r")
  185. lines = f.readlines()
  186. lines = [l.strip().replace('\r', '') for l in lines if self.EXTENSION in l.lower(
  187. ) and l.startswith('!') and "htm" not in l.lower()]
  188. for l in lines:
  189. self.log(l)
  190. return lines
  191. def netcat(self, ip, port, size, filename):
  192. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  193. self.log("Receiving file")
  194. s.connect((ip, port))
  195. fname = ('%s/%s' % (self.PATH, filename)).replace("//", "/")
  196. f = open(fname, 'wb')
  197. count = 0
  198. while True:
  199. data = s.recv(4096)
  200. if len(data) == 0:
  201. self.log("empty")
  202. break
  203. count += len(data)
  204. f.write(data)
  205. perc = int(100 * count / size)
  206. self.PROGRESS = perc
  207. self.set_status("DOWNLOADING") # % perc #progress
  208. self.log("\r%d%%" % perc, instant=True)
  209. if count >= size:
  210. break
  211. s.close()
  212. f.close()
  213. self.log("") # newline
  214. return fname
  215. def search(self, book):
  216. self.log("searching for %s" % book)
  217. self.send_queue("PRIVMSG %s :@search %s " % (self.CHANNEL, book))
  218. def pong(self, data):
  219. msg = data.replace("PING ", "")
  220. self.send_queue("PONG %s" % msg)
  221. def join_channel(self, channel):
  222. self.send_queue("JOIN %s" % channel)
  223. def send_queue(self, msg):
  224. add = bytes(str(msg), "utf-8") + bytes([13, 10])
  225. self.socket.send(add)
  226. def book(self, book):
  227. self.log("Asking for '%s'" % book)
  228. self.send_queue("PRIVMSG %s :%s " % (self.CHANNEL, book))
  229. def who(self):
  230. self.send_queue("WHO %s" % self.CHANNEL)
  231. def log(self, val, instant=False):
  232. if self.LOGGING:
  233. if not instant:
  234. print(val)
  235. else:
  236. sys.stdout.write(val)
  237. def set_status(self, value):
  238. self.STATUS = value
  239. self.callback(value=="FINISHED")
  240. def ip_from_decimal(dec):
  241. return ".".join([str(int(b, 2)) for b in b_octets(bin(dec)[2:])])
  242. def b_octets(l):
  243. l = l.zfill(32)
  244. return [l[0:8], l[8:16], l[16:24], l[24:32]]
  245. def random_hash():
  246. return ("%032x" % random.getrandbits(128))[:10]
  247. def usage():
  248. print("USAGE:")
  249. print("\t %s SEARCH <BOOK> <FORMAT>" % sys.argv[0])
  250. print("\t %s BOOK <BOT COMMAND>" % sys.argv[0])
  251. sys.exit(1)
  252. if __name__ == "__main__":
  253. if len(sys.argv) == 4:
  254. if(sys.argv[1] != "SEARCH"):
  255. usage()
  256. mode = sys.argv[1]
  257. query = sys.argv[2]
  258. grep = sys.argv[3]
  259. elif len(sys.argv) == 3:
  260. if(sys.argv[1] != "BOOK"):
  261. usage()
  262. mode = sys.argv[1]
  263. query = sys.argv[2]
  264. grep = ""
  265. else:
  266. usage()
  267. if mode == "SEARCH":
  268. print("Looking for '%s' in format '%s'" % (query, grep))
  269. if mode == "BOOK":
  270. print("Downloading %s" % query)
  271. client = IRCClient(query, grep, mode, logging=True)