poibot.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878
  1. # poibot.py: Protocol of IRC bot, an experimental IRC bot
  2. # Copyright (C) 2015, 2016, 2017 Tom Li
  3. # This program is free software: you can redistribute it and/or modify
  4. # it under the terms of the GNU Affero General Public License as published by
  5. # the Free Software Foundation, either version 3 of the License, or
  6. # (at your option) any later version.
  7. # This program is distributed in the hope that it will be useful,
  8. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. # GNU Affero General Public License for more details.
  11. # You should have received a copy of the GNU Affero General Public License
  12. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. import time
  14. import re
  15. import socket
  16. import ssl
  17. import queue
  18. import threading
  19. import copy
  20. import rpweibo
  21. import random
  22. import json
  23. import const
  24. try:
  25. NICKNAME = const.NICKNAME
  26. except AttributeError:
  27. NICKNAME = "poibot"
  28. def restart_program(reason="something happened"):
  29. import sys
  30. import os
  31. import time
  32. sys.stderr.write(reason)
  33. sys.stderr.write(", restarting...\n")
  34. python = sys.executable
  35. time.sleep(1)
  36. os.execl(python, python, *sys.argv)
  37. class IRCMessage():
  38. def __init__(self, line=None):
  39. self.nick = None
  40. self.ident = None
  41. self.prefix = None
  42. self.command = None
  43. self.dest = None
  44. self.text = None
  45. self.params = []
  46. self._line = line
  47. if line:
  48. self._parse(line)
  49. def __str__(self):
  50. return self._line
  51. @staticmethod
  52. def is_nospcrlfcl(char):
  53. for i in char:
  54. if i in ("\0", "\r", "\n", " ", ":"):
  55. return False
  56. return True
  57. def _parse(self, line):
  58. assert line[-2], line[-1] == "\r\n"
  59. # parse prefix
  60. if line.startswith(":"):
  61. for idx, chr in enumerate(line):
  62. if chr == " ":
  63. break
  64. self.prefix = line[1:idx]
  65. line = line[idx:]
  66. # split prefix to nick and ident
  67. if "!" in self.prefix:
  68. self.nick, self.ident = self.prefix.split("!", 1)
  69. else:
  70. self.ident = self.prefix
  71. # eat space
  72. assert line[0] == " "
  73. line = line[1:]
  74. # parse command
  75. for idx, chr in enumerate(line):
  76. if not (chr.isalpha() or chr.isdigit()):
  77. break
  78. self.command = line[0:idx]
  79. assert (len(self.command) >= 1 and self.command.isalpha() or
  80. len(self.command) == 3 and self.command.isdigit())
  81. line = line[idx:]
  82. # no params
  83. if line == "\r\n":
  84. return
  85. # parse params
  86. assert line.startswith(" ")
  87. iters = 0
  88. end = False
  89. while iters < 14 and not end:
  90. if not line.startswith(" "):
  91. break
  92. else:
  93. line = line[1:]
  94. for idx, chr in enumerate(line):
  95. if idx == 0 and not self.is_nospcrlfcl(chr):
  96. end = True
  97. break
  98. elif not self.is_nospcrlfcl(chr) and chr != ":":
  99. self.params.append(line[0:idx])
  100. line = line[idx:]
  101. break
  102. iters += 1
  103. line = line[idx:]
  104. if line == "\r\n":
  105. return
  106. if iters != 13:
  107. line = line[1:]
  108. for idx, chr in enumerate(line):
  109. if chr in ["\0", "\r", "\n"]:
  110. break
  111. self.params.append(line[0:idx])
  112. if self.command == "PRIVMSG":
  113. self.dest = self.params[0]
  114. self.text = self.params[1]
  115. def say(self, to, msg):
  116. self._line = "PRIVMSG %s :%s\r\n" % (to, msg)
  117. print(self._line)
  118. return self._line
  119. def me(self, to, msg):
  120. self.say(to, '\x01ACTION %s\x01' % msg)
  121. def setnick(self, nick):
  122. self._line = "NICK %s\r\n" % nick
  123. return self._line
  124. def setuser(self, user, realname):
  125. self._line = "USER %s %s %s :%s\r\n" % (user, user, user, realname)
  126. return self._line
  127. def join(self, channel):
  128. self._line = "JOIN %s\r\n" % channel
  129. return self._line
  130. def whois(self, user):
  131. self._line = "WHOIS %s\r\n" % (user)
  132. return self._line
  133. def kick(self, channel, user, reason=""):
  134. if reason:
  135. self._line = "KICK %s %s %s\r\n" % (channel, user, reason)
  136. else:
  137. self._line = "KICK %s %s\r\n" % (channel, user)
  138. return self._line
  139. def raw(self, msg):
  140. return "%s\r\n" % msg
  141. @staticmethod
  142. def normalize_bot_message(msg):
  143. msg = copy.deepcopy(msg)
  144. if not msg.text:
  145. return msg
  146. if msg.nick in ["Orizon", "fakeOrizon", "xmppbot", "teleboto", "OrzIrc2P"]:
  147. try:
  148. if msg.text[0] == "\x03":
  149. text = msg.text.lstrip("\x03")
  150. text = text.replace("] \x0f", "] ")
  151. try:
  152. int(text[0])
  153. text = text[1:]
  154. int(text[0])
  155. text = text[1:]
  156. except ValueError:
  157. pass
  158. msg.text = text
  159. text = msg.text.lstrip("[")
  160. stripped = text.split("]")
  161. msg.nick = stripped[-2]
  162. msg.text = stripped[-1].lstrip()
  163. except IndexError:
  164. pass
  165. if msg.nick in ["toxsync"]:
  166. try:
  167. text = msg.text.lstrip("(")
  168. stripped = text.split(")")
  169. msg.nick = stripped[-2]
  170. msg.text = stripped[-1].lstrip()
  171. except IndexError:
  172. pass
  173. if msg.nick in ["OrzGTalk", "blugbot", "OrzXMPP"]:
  174. if msg.nick == "OrzGTalk":
  175. prefix = "(GTalk) "
  176. elif msg.nick in ["blugbot", "OrzXMPP"]:
  177. prefix = "(XMPP) "
  178. try:
  179. text = msg.text.replace(prefix, "")
  180. text = text.split(": ", maxsplit=1)
  181. msg.nick = text[0]
  182. if (not msg.nick) or (len(text) != 2):
  183. raise IndexError
  184. msg.text = text[1]
  185. except IndexError:
  186. pass
  187. return msg
  188. class IRCAbstractHook():
  189. def __init__(self):
  190. self._queue = None
  191. def set_queue(self, queue):
  192. self._queue = queue
  193. def send(self, msg):
  194. self._queue.put(msg)
  195. def handle(self, msg):
  196. raise NotImplementedError
  197. class IRCConnection():
  198. class IRCInitHook(IRCAbstractHook):
  199. def __init__(self, sasl_external=False):
  200. super().__init__()
  201. self.sasl_external = sasl_external
  202. def handle(self, msg):
  203. if msg.command and self.sasl_external:
  204. self.send(IRCMessage().raw('CAP REQ :sasl'))
  205. self.send(IRCMessage().raw('AUTHENTICATE EXTERNAL'))
  206. self.send(IRCMessage().raw('AUTHENTICATE +'))
  207. self.send(IRCMessage().raw('CAP END'))
  208. if msg.command == "NOTICE":
  209. self.send(IRCMessage().setnick(NICKNAME))
  210. self.send(IRCMessage().setuser(NICKNAME, NICKNAME))
  211. for chan in const.CHANNELS:
  212. self.send(IRCMessage().join(chan))
  213. class IRCDebugHook(IRCAbstractHook):
  214. def __init__(self):
  215. super().__init__()
  216. def handle(self, msg):
  217. print(msg.prefix, msg.command, msg.params)
  218. class IRCPingHook(IRCAbstractHook):
  219. def __init__(self):
  220. super().__init__()
  221. def handle(self, msg):
  222. if msg.command == "PING":
  223. self.send("PONG :%s\r\n" % msg.params[0])
  224. def __init__(self, addr, port, client_cert=None):
  225. self.addr = addr
  226. self.port = port
  227. ctx = ssl.create_default_context()
  228. if client_cert:
  229. ctx.load_cert_chain(client_cert)
  230. ctx.options &= ssl.PROTOCOL_TLSv1_2
  231. ctx.options &= ssl.OP_NO_SSLv2
  232. ctx.options &= ssl.OP_NO_SSLv3
  233. ctx.options &= ssl.OP_NO_TLSv1
  234. ctx.options &= ssl.OP_NO_TLSv1_1
  235. ctx.options &= ssl.OP_NO_COMPRESSION # no CRIME attack
  236. ctx.options &= ssl.CERT_REQUIRED
  237. # XXX: failed to pass any checks... DO NOT CHECK REVOKED CERTIFICATE FOR NOW
  238. # ctx.verify_flags = ssl.VERIFY_CRL_CHECK_LEAF
  239. ctx.set_ciphers(
  240. "ECDHE-RSA-AES256-GCM-SHA384:"
  241. "ECDHE-RSA-AES128-GCM-SHA256:"
  242. "DHE-RSA-AES256-GCM-SHA384:"
  243. "DHE-RSA-AES128-GCM-SHA256"
  244. )
  245. ctx.check_hostname = True
  246. if addr.endswith(".onion"):
  247. ctx.check_hostname = False
  248. ctx.load_default_certs()
  249. ctx.set_default_verify_paths()
  250. for res in socket.getaddrinfo(self.addr, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM):
  251. af, socktype, proto, canonname, sa = res
  252. try:
  253. self.sock = ctx.wrap_socket(socket.socket(af, socktype, proto), server_hostname=addr)
  254. self.sock.settimeout(10)
  255. self.sock.connect(sa)
  256. except socket.error:
  257. self.sock.close()
  258. self.sock = None
  259. continue
  260. break
  261. if self.sock is None:
  262. restart_program("sock.connect() failed!")
  263. self.sock.settimeout(None)
  264. import pprint
  265. print("TLS Ceritificate:")
  266. pprint.pprint(self.sock.getpeercert())
  267. if hasattr(self.sock, "version"):
  268. print("TLS Version:", self.sock.version())
  269. print("TLS Cipher:", self.sock.cipher()[0])
  270. self._sendq = queue.Queue()
  271. self._readq = queue.Queue()
  272. self._hooks = []
  273. _reader = threading.Thread(group=None, target=self._read)
  274. _reader.start()
  275. reader = threading.Thread(group=None, target=self.read)
  276. reader.start()
  277. sender = threading.Thread(group=None, target=self.send)
  278. sender.start()
  279. self.register_hook(self.IRCInitHook(sasl_external=client_cert), oneshot=True)
  280. self.register_hook(self.IRCPingHook())
  281. self.register_hook(self.IRCDebugHook())
  282. def _read(self):
  283. msg = []
  284. status = 0
  285. while True:
  286. try:
  287. buf = self.sock.recv()
  288. if not buf:
  289. self.sock.close()
  290. restart_program("TCP error")
  291. except OSError:
  292. # timeout?
  293. self.sock.close()
  294. restart_program("TCP error (OSError)")
  295. for i in buf:
  296. if status == 0:
  297. if i == 13:
  298. status = 1
  299. else:
  300. status = 0
  301. elif status == 1:
  302. if i == 10:
  303. status = 2
  304. else:
  305. status = 0
  306. elif status == 2:
  307. status = 0
  308. if status == 0:
  309. msg.append(i)
  310. elif status == 1:
  311. pass
  312. elif status == 2:
  313. msg.append(13)
  314. msg.append(10)
  315. bytestring = bytes(msg)
  316. string = bytestring.decode("UTF-8", errors="ignore")
  317. self._readq.put(string)
  318. msg.clear()
  319. def register_hook(self, hook, oneshot=False):
  320. hook.set_queue(self._sendq)
  321. self._hooks.append((hook, oneshot))
  322. def read(self):
  323. while True:
  324. data = IRCMessage(self._readq.get())
  325. if data.command == "ERROR":
  326. self.sock.close()
  327. restart_program("IRC error")
  328. for hook, oneshot in self._hooks:
  329. worker = threading.Thread(group=None, target=hook.handle, args=(data,))
  330. worker.start()
  331. if oneshot:
  332. self._hooks.remove((hook, oneshot))
  333. def send(self):
  334. while True:
  335. data = self._sendq.get()
  336. try:
  337. self.sock.sendall(str(data).encode("UTF-8", errors="ignore"))
  338. except OSError:
  339. self.sock.close()
  340. restart_program("TCP Error (Send, OSError)")
  341. class IRCBuiltinHook(IRCAbstractHook):
  342. def __init__(self):
  343. super().__init__()
  344. self._prev_msgs = {}
  345. def handle(self, msg):
  346. msg = msg.normalize_bot_message(msg)
  347. if msg.command == "PRIVMSG":
  348. self.handle_privmsg(msg)
  349. self.handle_poi(msg)
  350. elif msg.command == "NICK":
  351. self.handle_nick(msg)
  352. elif msg.command in ["PART", "QUIT"]:
  353. self.handle_quit(msg)
  354. def handle_poi(self, msg):
  355. if msg.text.startswith("%s: " % NICKNAME) or msg.text.startswith("%s, " % NICKNAME):
  356. command = msg.text.lstrip("%s" % NICKNAME)
  357. if "poi" in command:
  358. self.send(IRCMessage().say(msg.dest, "%s: poi~" % msg.nick))
  359. elif "help" in command:
  360. self.send(IRCMessage().say(msg.dest, "%s: Hello, I'm a Protocol of IRC bot, a.k.a poibot." % msg.nick))
  361. elif "say" in command:
  362. self.send(IRCMessage().say(msg.dest, "%s: 少说话,多 poi~" % msg.nick))
  363. def handle_privmsg(self, msg):
  364. text = msg.text.lstrip().rstrip()
  365. status = 0
  366. for idx, chr in enumerate(text):
  367. if idx == 0 and chr == 's':
  368. status = 1
  369. elif idx == 1 and chr == "/":
  370. status = 2
  371. elif status == 2 and chr == "/":
  372. status = 3
  373. elif status == 3 and chr == "/":
  374. status = 4
  375. if status == 4 and chr == "g":
  376. status = 5
  377. if status < 4:
  378. self.add_to_dict(msg)
  379. return
  380. else:
  381. text = text.lstrip("s/")
  382. if status == 5:
  383. text = text.rstrip("/g")
  384. elif status == 4:
  385. text = text.rstrip("/")
  386. sed_replacement = text.split("/")
  387. print(sed_replacement, self._prev_msgs)
  388. if len(sed_replacement) in [1, 2]:
  389. try:
  390. if len(sed_replacement) == 1:
  391. newtext = self._prev_msgs[msg.nick].replace(sed_replacement[0], "")
  392. else:
  393. newtext = self._prev_msgs[msg.nick].replace(sed_replacement[0], sed_replacement[1])
  394. if newtext == self._prev_msgs[msg.nick]:
  395. return
  396. except KeyError:
  397. return
  398. self.send(IRCMessage().say(msg.dest, "%s meant to say: %s" % (msg.nick, newtext)))
  399. def handle_nick(self, msg):
  400. nick_orig = msg.nick
  401. nick_new = msg.params[0]
  402. assert nick_orig != nick_new
  403. try:
  404. self._prev_msgs[nick_new] = self._prev_msgs.pop(nick_orig)
  405. except KeyError:
  406. pass
  407. def handle_quit(self, msg):
  408. try:
  409. del self._prev_msgs[msg.nick]
  410. except KeyError:
  411. pass
  412. def add_to_dict(self, msg):
  413. self._prev_msgs[msg.nick] = msg.text
  414. class IRCWhoisHook(IRCAbstractHook):
  415. NEED_QUERY = 0
  416. NOT_SECURE = 1
  417. SECURE = 2
  418. REMINDER = "%s: 欢迎加入聊天。但您没有使用 SSL/TLS 加密连接。为了大家的网络安全,强烈推荐您加密连接到 IRC,请阅读教程 https://orz.chat/tls.html"
  419. WARNING = "%s: 使用安全连接是此频道的方针政策。若您依然拒绝安全,阁下将会遭到封禁,剩余机会: %d 次"
  420. SUCCESS = "%s: 恭喜,您已成功启用安全连接。水表有保障,就用 TLS!Make Big Brother's Life Harder Again!"
  421. BAN = "%s: 由于您多次拒绝使用安全连接,我们只得暂时拉黑您。使用安全连接即可重新加入聊天。"
  422. WHOIS_USER_PATH = "./whois_users.db"
  423. def __init__(self):
  424. super().__init__()
  425. self.users = {}
  426. self._force_channel_tls = []
  427. try:
  428. self._path = const.WHOIS_USER_PATH
  429. except AttributeError:
  430. self._path = self.WHOIS_USER_PATH
  431. try:
  432. self._force_channel_tls = const.FORCE_CHANNEL_TLS
  433. except AttributeError:
  434. pass
  435. self.load_database()
  436. def load_database(self):
  437. try:
  438. with open(self._path, "r") as f:
  439. self.users = json.loads(f.read())
  440. except FileNotFoundError:
  441. pass
  442. def save_database(self):
  443. db = json.dumps(self.users, sort_keys=True, indent=4, separators=(',', ': '))
  444. with open(self._path, "w") as f:
  445. f.write(db)
  446. @staticmethod
  447. def new_user_attributes():
  448. return {"channels": [], "security": None, "violations": 0}
  449. def handle(self, msg):
  450. if msg.nick in [NICKNAME, "ChanServ"]:
  451. return
  452. if msg.command == "JOIN" and msg.params[0] in const.CHANNELS:
  453. try:
  454. user = self.users[msg.nick]
  455. except KeyError:
  456. user = self.new_user_attributes()
  457. self.users[msg.nick] = user
  458. user["security"] = self.NEED_QUERY
  459. user["channels"].append(msg.params[0])
  460. user["channels"] = list(set(user["channels"]))
  461. self.send(IRCMessage().whois(msg.nick))
  462. elif msg.command == "PART":
  463. try:
  464. self.users[msg.nick]["channels"].remove(msg.params[0])
  465. except KeyError:
  466. pass
  467. elif msg.command == "QUIT":
  468. try:
  469. self.users[msg.nick]["channels"].clear()
  470. if msg.params[0] == "Changing host":
  471. self.users[msg.nick]["violations"] -= 1
  472. except KeyError:
  473. pass
  474. elif msg.command == "401":
  475. # No such nick
  476. self.users.pop(msg.nick, None)
  477. elif msg.command == "311":
  478. # connection nicknames recieved, mark NOT_SECURE at first
  479. self.users[msg.params[1]]["security"] = self.NOT_SECURE
  480. elif msg.command == "671":
  481. # is using a secure connection
  482. self.users[msg.params[1]]["security"] = self.SECURE
  483. elif msg.command == "318":
  484. # End of /WHOIS list.
  485. user = self.users.get(msg.params[1], None)
  486. if not user:
  487. return
  488. if user["security"] == self.NOT_SECURE:
  489. user["violations"] += 1
  490. if self.judge(msg.params[1], user):
  491. return
  492. for chan in user["channels"]:
  493. self.send(IRCMessage().say(chan, self.REMINDER % msg.params[1]))
  494. if chan in self._force_channel_tls:
  495. self.send(IRCMessage().say(chan, self.WARNING % (msg.params[1], 3 - user["violations"])))
  496. elif user["security"] == self.SECURE and user["violations"] > 0:
  497. user["violations"] = 0
  498. for chan in user["channels"]:
  499. self.send(IRCMessage().say(chan, self.SUCCESS % msg.params[1]))
  500. self.users.pop(msg.params[1])
  501. else:
  502. self.users.pop(msg.params[1])
  503. self.save_database()
  504. def judge(self, nick, user):
  505. if user["violations"] < 3:
  506. return False
  507. for chan in user["channels"]:
  508. if chan in self._force_channel_tls:
  509. self.send(IRCMessage().say("ChanServ", "op %s %s" % (chan, NICKNAME)))
  510. self.send(IRCMessage().say(chan, self.BAN % nick))
  511. self.send(IRCMessage().kick(chan, nick, self.BAN % nick))
  512. self.send(IRCMessage().say("ChanServ", "deop %s %s" % (chan, NICKNAME)))
  513. return True
  514. class WeiboHook(IRCAbstractHook):
  515. WEIBO_URL_RE = re.compile(r"(http://w{0,3}\.{0,1}weibo.com/[0-9]+/[a-zA-Z0-9]{9})")
  516. def __init__(self):
  517. super().__init__()
  518. self._weibo = None
  519. self._login_lock = False
  520. def handle(self, msg):
  521. if self._weibo is None:
  522. self._login()
  523. if not msg.text:
  524. return
  525. links = self.WEIBO_URL_RE.findall(msg.text)
  526. for link in links:
  527. mid = link.split("/")[-1]
  528. id = WeiboHook.mid2id(mid)
  529. print("...Fetching %s/%s" % (mid, id))
  530. try:
  531. tweet = self._weibo.api("statuses/show").get(id=id)
  532. except:
  533. self._login()
  534. tweet = self._weibo.api("statuses/show").get(id=id)
  535. if "retweeted_status" in tweet:
  536. say_orig = "⇪转发: @%s: " % tweet.user.screen_name
  537. say_orig += tweet.text
  538. tweet = tweet.retweeted_status
  539. else:
  540. say_orig = ""
  541. say = "⇪微博: @%s: " % tweet.user.screen_name + tweet.text
  542. if "original_pic" in tweet:
  543. say += " " + tweet.original_pic
  544. say = say.replace("\n", " ")
  545. self.send(IRCMessage().say(msg.dest, say))
  546. time.sleep(2)
  547. if say_orig:
  548. say_orig = say_orig.replace("\n", " ")
  549. self.send(IRCMessage().say(msg.dest, say_orig))
  550. def _login(self):
  551. if self._login_lock:
  552. return
  553. try:
  554. import const
  555. except ImportError:
  556. raise ImportError("Please create const.py and provide KEY, SECRET, REDIR, USER and PASS")
  557. self._login_lock = True
  558. app = rpweibo.Application(const.KEY, const.SECRET, const.REDIR)
  559. self._weibo = rpweibo.Weibo(app)
  560. authenticator = rpweibo.UserPassAutheticator(const.USER, const.PASS)
  561. try:
  562. self._weibo.auth(authenticator)
  563. print("good login")
  564. self.send(IRCMessage().setnick(NICKNAME))
  565. except Exception as e:
  566. print("bad login", e)
  567. self._weibo = None
  568. self.send(IRCMessage().setnick(NICKNAME + "_error"))
  569. self._login_lock = False
  570. @staticmethod
  571. def mid2id(mid):
  572. def base10(base62):
  573. """Convert the base."""
  574. CHAR = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  575. digit = len(base62) - 1
  576. num = 0
  577. while digit >= 0:
  578. for i in base62:
  579. i = CHAR.index(i)
  580. num += i * 62 ** digit
  581. digit -= 1
  582. return num
  583. id = ""
  584. id += str(base10(mid[0]))
  585. id += str(base10(mid[1:5])).rjust(7, "0")
  586. id += str(base10(mid[5:9])).rjust(7, "0")
  587. return id
  588. class ProgramRestartHook(IRCAbstractHook):
  589. def __init__(self):
  590. super().__init__()
  591. self._timer_queue = queue.Queue()
  592. self._timer_thread = threading.Thread(group=None, target=self._timer)
  593. self._timer_thread.start()
  594. def handle(self, msg):
  595. if msg.command == "PING":
  596. self._timer_queue.put(item=True)
  597. def _timer(self):
  598. while 1:
  599. try:
  600. print("checking & waiting for ping")
  601. self._timer_queue.get(block=True, timeout=600)
  602. print("connection is still alive!")
  603. except queue.Empty:
  604. restart_program("Long time no ping!")
  605. class LaTeXCorrectionHook(IRCAbstractHook):
  606. INCORRECT_LATEX_SPELLING = ["Latex", "LaTex", "laTeX", "laTex", "lateX"]
  607. def __init__(self):
  608. super().__init__()
  609. def handle(self, msg):
  610. if not msg.text:
  611. return
  612. msg = msg.normalize_bot_message(msg)
  613. for spell in self.INCORRECT_LATEX_SPELLING:
  614. if spell in msg.text:
  615. say = "%s: 不是 %s,是 %s!" % (msg.nick, spell, "LaTeX")
  616. self.send(IRCMessage().say(msg.dest, say))
  617. class CorebootCorrectionHook(IRCAbstractHook):
  618. def __init__(self):
  619. super().__init__()
  620. def handle(self, msg):
  621. if not msg.text:
  622. return
  623. msg = msg.normalize_bot_message(msg)
  624. allcb = [msg.text[m.start():m.start() + 8]
  625. for m in re.finditer("coreboot", msg.text.lower())]
  626. for spell in allcb:
  627. if spell != "coreboot":
  628. say = "%s: 不是 %s,是 coreboot!" % (msg.nick, spell)
  629. self.send(IRCMessage().say(msg.dest, say))
  630. class TorCorrectionHook(IRCAbstractHook):
  631. def __init__(self):
  632. super().__init__()
  633. def handle(self, msg):
  634. if not msg.text:
  635. return
  636. msg = msg.normalize_bot_message(msg)
  637. allcb = [msg.text[m.start():m.start() + 8]
  638. for m in re.finditer("tor", msg.text.lower())]
  639. for spell in allcb:
  640. if spell == "TOR":
  641. say = "%s: 不是 TOR,是 Tor! https://www.torproject.org/docs/faq#WhyCalledTor"
  642. self.send(IRCMessage().say(msg.dest, say))
  643. class SchneierQuoteHook(IRCAbstractHook):
  644. WEB_RSS = "https://www.schneierfacts.com/rss/random"
  645. def __init__(self):
  646. super().__init__()
  647. def handle(self, msg):
  648. if not msg.text:
  649. return
  650. msg = msg.normalize_bot_message(msg)
  651. if "'schneier" in msg.text:
  652. quote = self.fetch_schneier_quote()
  653. if not quote:
  654. quote = "Failed to get a Schneier quote, maybe the HTTPS connection is cracked by Schneier?"
  655. self.send(IRCMessage().say(msg.dest, quote))
  656. def fetch_schneier_quote(self):
  657. from time import sleep
  658. from xml.etree.ElementTree import ElementTree
  659. import urllib.request
  660. for i in range(3):
  661. try:
  662. rss_resource = urllib.request.urlopen(self.WEB_RSS)
  663. listing = list(ElementTree(file=rss_resource).iter("item"))
  664. text = listing[1].find("description").text.strip()
  665. return text.replace("\n", " ")
  666. except Exception:
  667. sleep(1)
  668. else:
  669. return
  670. class InterjectHook(IRCAbstractHook):
  671. def __init__(self):
  672. super().__init__()
  673. random.seed()
  674. try:
  675. self.ratio = const.INTERJECT
  676. except AttributeError:
  677. self.ratio = 40
  678. def handle(self, msg):
  679. if not msg.text:
  680. return
  681. msg = msg.normalize_bot_message(msg)
  682. txt = msg.text.lower()
  683. if "linux" in txt:
  684. for kws in ["gnu", "kernel", "内核", "http", "arch", "android", "bsd", "嵌入", "selinux", "harden", "util", "journal", "."]:
  685. if kws in txt:
  686. return
  687. interjection = "%s: 你所说的 Linux,应该是 GNU/Linux!" % (msg.nick)
  688. rnd = random.randrange(100)
  689. if rnd < self.ratio:
  690. self.send(IRCMessage().say(msg.dest, interjection))
  691. else:
  692. print("rnd = %d >= %d, do not interject" % (rnd, self.ratio))
  693. if __name__ == "__main__":
  694. try:
  695. conn = IRCConnection("irc.freenode.net", 7000, client_cert="freenode.pem")
  696. except Exception as e:
  697. conn.socket.close()
  698. restart_program(str(e))
  699. conn.register_hook(IRCBuiltinHook())
  700. #conn.register_hook(WeiboHook())
  701. conn.register_hook(IRCWhoisHook())
  702. conn.register_hook(ProgramRestartHook())
  703. conn.register_hook(LaTeXCorrectionHook())
  704. conn.register_hook(CorebootCorrectionHook())
  705. conn.register_hook(TorCorrectionHook())
  706. conn.register_hook(SchneierQuoteHook())
  707. conn.register_hook(InterjectHook())