Guest User

Untitled

a guest
Jan 15th, 2016
108
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.80 KB | None | 0 0
  1. # minqlx - A Quake Live server administrator bot.
  2. # Copyright (C) 2015 Mino <mino@minomino.org>
  3.  
  4. # This file is part of minqlx.
  5.  
  6. # minqlx is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10.  
  11. # minqlx is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15.  
  16. # You should have received a copy of the GNU General Public License
  17. # along with minqlx. If not, see <http://www.gnu.org/licenses/>.
  18. # Original by Mino. Modified by b1ngo
  19.  
  20.  
  21. import minqlx
  22. import threading
  23. import asyncio
  24. import random
  25. import time
  26. import re
  27.  
  28. # Colors using the mIRC color standard palette (which several other clients also comply with).
  29. COLORS = ("\x0301", "\x0304", "\x0303", "\x0308", "\x0302", "\x0311", "\x0306", "\x0300")
  30.  
  31. class irc(minqlx.Plugin):
  32. def __init__(self):
  33. self.add_hook("chat", self.handle_chat, priority=minqlx.PRI_LOWEST)
  34. self.add_hook("unload", self.handle_unload)
  35. self.add_hook("player_connect", self.handle_player_connect, priority=minqlx.PRI_LOWEST)
  36. self.add_hook("player_disconnect", self.handle_player_disconnect, priority=minqlx.PRI_LOWEST)
  37. self.add_hook("vote_started", self.handle_vote_started)
  38. self.add_hook("vote_ended", self.handle_vote_ended)
  39. self.add_hook("map", self.handle_map)
  40. self.add_hook("round_end", self.handle_round_end)
  41. self.add_hook("game_start", self.handle_game_start)
  42. self.add_hook("game_end", self.handle_game_end)
  43.  
  44.  
  45. self.set_cvar_once("qlx_ircServer", "irc.quakenet.org")
  46. self.set_cvar_once("qlx_ircRelayChannel", "")
  47. self.set_cvar_once("qlx_ircRelayIrcChat", "1")
  48. self.set_cvar_once("qlx_ircIdleChannels", "")
  49. self.set_cvar_once("qlx_ircNickname", "minqlx-{}".format(random.randint(1000, 9999)))
  50. self.set_cvar_once("qlx_ircPassword", "")
  51. self.set_cvar_once("qlx_ircColors", "0")
  52. self.set_cvar_once("qlx_ircQuakenetUser", "")
  53. self.set_cvar_once("qlx_ircQuakenetPass", "")
  54. self.set_cvar_once("qlx_ircQuakenetHidden", "0")
  55.  
  56. self.server = self.get_cvar("qlx_ircServer")
  57. self.relay = self.get_cvar("qlx_ircRelayChannel")
  58. self.idle = self.get_cvar("qlx_ircIdleChannels", list)
  59. self.nickname = self.get_cvar("qlx_ircNickname")
  60. self.password = self.get_cvar("qlx_ircPassword")
  61. self.qnet = (self.get_cvar("qlx_ircQuakenetUser"),
  62. self.get_cvar("qlx_ircQuakenetPass"),
  63. self.get_cvar("qlx_ircQuakenetHidden", bool))
  64. self.is_relaying = self.get_cvar("qlx_ircRelayIrcChat", bool)
  65.  
  66. self.authed = set()
  67. self.auth_attempts = {}
  68.  
  69. if not self.server:
  70. self.logger.warning("IRC plugin loaded, but no IRC server specified.")
  71. elif not self.relay and not self.idle and not self.password:
  72. self.logger.warning("IRC plugin loaded, but no channels or password set. Not connecting.")
  73. else:
  74. self.irc = SimpleAsyncIrc(self.server, self.nickname, self.handle_msg, self.handle_perform, self.handle_raw)
  75. self.irc.start()
  76. self.logger.info("Connecting to {}...".format(self.server))
  77.  
  78. def handle_chat(self, player, msg, channel):
  79. if self.irc and self.relay:
  80. if channel == "chat":
  81. text = "General Chat: {} ^2{}".format(player.name, msg)
  82. self.irc.msg(self.relay, self.translate_colors(text))
  83. elif channel == "red_team_chat":
  84. text = "Red Team: {} ^1{}".format(player.name, msg)
  85. self.irc.msg(self.relay, self.translate_colors(text))
  86. elif channel == "blue_team_chat":
  87. text = "Blue Team: {} ^4{}".format(player.name, msg)
  88. self.irc.msg(self.relay, self.translate_colors(text))
  89. elif channel == "spectator_chat":
  90. text = "Spectator: {} ^7{}".format(player.name, msg)
  91. self.irc.msg(self.relay, self.translate_colors(text))
  92.  
  93. def handle_game_start(self, data):
  94. text = "The game has started."
  95. self.irc.msg(self.relay, self.translate_colors(text))
  96.  
  97. def handle_game_end(self, data):
  98. text = "The game had ended. Final Scores: ^1Red Team: {}^0, ^4Blue Team: {}^0.".format(self.game.red_score, self.game.blue_score)
  99. self.irc.msg(self.relay, self.translate_colors(text))
  100.  
  101. def handle_round_end(self, data):
  102. text = "Round ended: ^1Red Team: {}^0, ^4Blue Team {}^0.".format(self.game.red_score, self.game.blue_score)
  103. self.irc.msg(self.relay, self.translate_colors(text))
  104.  
  105. def handle_unload(self, plugin):
  106. if plugin == self.__class__.__name__ and self.irc and self.irc.is_alive():
  107. self.irc.quit("Plugin unloaded!")
  108. self.irc.stop()
  109.  
  110. def handle_player_connect(self, player):
  111. if self.irc and self.relay:
  112. self.irc.msg(self.relay, self.translate_colors("{} connected.".format(player.name)))
  113.  
  114. def handle_player_disconnect(self, player, reason):
  115. if reason and reason[-1] not in ("?", "!", "."):
  116. reason = reason + "."
  117.  
  118. if self.irc and self.relay:
  119. self.irc.msg(self.relay, self.translate_colors("{} {}".format(player.name, reason)))
  120.  
  121. def handle_vote_started(self, caller, vote, args):
  122. if self.irc and self.relay:
  123. caller = caller.name if caller else "The server"
  124. self.irc.msg(self.relay, self.translate_colors("{} ^3called a vote: ^7{} {}".format(caller, vote, args)))
  125.  
  126. def handle_vote_ended(self, votes, vote, args, passed):
  127. if self.irc and self.relay:
  128. if passed:
  129. self.irc.msg(self.relay, self.translate_colors("^3Vote passed |^2{} ^3- ^1{}^3|".format(*votes)))
  130. else:
  131. self.irc.msg(self.relay, self.translate_colors("^3Vote failed"))
  132.  
  133. def handle_map(self, map, factory):
  134. if self.irc and self.relay:
  135. self.irc.msg(self.relay, self.translate_colors("^3Changing map to ^7{}".format(map)))
  136.  
  137. def handle_msg(self, irc, user, channel, msg):
  138. if not msg:
  139. return
  140.  
  141. cmd = msg[0].lower()
  142. if channel.lower() == self.relay.lower():
  143. if cmd in (".players", ".status", ".info", ".map", ".server"):
  144. self.server_report(self.relay)
  145. elif self.is_relaying:
  146. minqlx.CHAT_CHANNEL.reply("|irc| ^6{}^7:^2 {}".format(user[0], " ".join(msg)))
  147. elif channel == user[0]: # Is PM?
  148. if len(msg) > 1 and msg[0].lower() == ".auth" and self.password:
  149. if user in self.authed:
  150. irc.msg(channel, "You are already authenticated.")
  151. elif msg[1] == self.password:
  152. self.authed.add(user)
  153. irc.msg(channel, "You have been successfully authenticated. You can now use .qlx to execute commands.")
  154. else:
  155. # Allow up to 3 attempts for the user's IP to authenticate.
  156. if user[2] not in self.auth_attempts:
  157. self.auth_attempts[user[2]] = 3
  158. self.auth_attempts[user[2]] -= 1
  159. if self.auth_attempts[user[2]] > 0:
  160. irc.msg(channel, "Wrong password. You have {} attempts left.".format(self.auth_attempts[user[2]]))
  161. elif len(msg) > 1 and user in self.authed and msg[0].lower() == ".qlx":
  162. @minqlx.next_frame
  163. def f():
  164. try:
  165. minqlx.COMMANDS.handle_input(IrcDummyPlayer(self.irc, user[0]), " ".join(msg[1:]), IrcChannel(self.irc, user[0]))
  166. except Exception as e:
  167. irc.msg(channel, "{}: {}".format(e.__class__.__name__, e))
  168. minqlx.log_exception()
  169. f()
  170.  
  171. def handle_perform(self, irc):
  172. self.logger.info("Connected to IRC!".format(self.server))
  173.  
  174. quser, qpass, qhidden = self.qnet
  175. if quser and qpass and "NETWORK" in self.irc.server_options and self.irc.server_options["NETWORK"] == "QuakeNet":
  176. self.logger.info("Authenticating on Quakenet as \"{}\"...".format(quser))
  177. self.irc.msg("Q@CServe.quakenet.org", "AUTH {} {}".format(quser, qpass))
  178. if qhidden:
  179. self.irc.mode(self.irc.nickname, "+x")
  180.  
  181. for channel in self.idle:
  182. irc.join(channel)
  183. if self.relay:
  184. irc.join(self.relay)
  185.  
  186. def handle_raw(self, irc, msg):
  187. split_msg = msg.split()
  188. if len(split_msg) > 2 and split_msg[1] == "NICK":
  189. user = re_user.match(split_msg[0][1:])
  190. if user and user.groups() in self.authed:
  191. # Update nick if an authed user changed it.
  192. self.authed.remove(user.groups())
  193. self.authed.add((split_msg[2][1:], user.groups()[1], user.groups()[2]))
  194. elif len(split_msg) > 1 and split_msg[1] == "433":
  195. irc.nick(irc.nickname + "_")
  196.  
  197. @classmethod
  198. def translate_colors(cls, text):
  199. if not cls.get_cvar("qlx_ircColors", bool):
  200. return cls.clean_text(text)
  201.  
  202. for i, color in enumerate(COLORS):
  203. text = text.replace("^{}".format(i), color)
  204.  
  205. return text
  206.  
  207. @minqlx.next_frame
  208. def server_report(self, channel):
  209. teams = self.teams()
  210. players = teams["free"] + teams["red"] + teams["blue"] + teams["spectator"]
  211. game = self.game
  212. # Make a list of players.
  213. plist = []
  214. for t in teams:
  215. if not teams[t]:
  216. continue
  217. elif t == "free":
  218. plist.append("Free: " + ", ".join([p.clean_name for p in teams["free"]]))
  219. elif t == "red":
  220. plist.append("\x0304Red\x03: " + ", ".join([p.clean_name for p in teams["red"]]))
  221. elif t == "blue":
  222. plist.append("\x0302Blue\x03: " + ", ".join([p.clean_name for p in teams["blue"]]))
  223. elif t == "spectator":
  224. plist.append("\x02\x0307Spec\x02:\x0301 " + ", ".join([p.clean_name for p in teams["spectator"]]))
  225.  
  226.  
  227. # Info about the game state.
  228. if game.state == "in_progress":
  229. if game.type_short == "race" or game.type_short == "ffa":
  230. ginfo = "\x0304The game is in progress"
  231. else:
  232. ginfo = "\x0304The score is \x02\x0304{}\x03 - \x0302{}\x03\x02".format(game.red_score, game.blue_score)
  233. elif game.state == "countdown":
  234. ginfo = "\x0304The game is about to start"
  235. else:
  236. ginfo = "\x0304The game is in warmup"
  237.  
  238. self.irc.msg(channel, "{} \x0301on \x02\x0308{}\x02 \x0301(\x0308{}\x0301) \x0301with \x02\x0308{}\x0308/{}\x0308\x02 \x0301players:" .format(ginfo, self.clean_text(game.map_title),
  239. game.type_short.upper(), len(players), self.get_cvar("sv_maxClients")))
  240. self.irc.msg(channel, "{}".format(" ".join(plist)))
  241.  
  242. # ====================================================================
  243. # DUMMY PLAYER & IRC CHANNEL
  244. # ====================================================================
  245.  
  246. class IrcChannel(minqlx.AbstractChannel):
  247. name = "irc"
  248. def __init__(self, irc, recipient):
  249. self.irc = irc
  250. self.recipient = recipient
  251.  
  252. def __repr__(self):
  253. return "{} {}".format(str(self), self.recipient)
  254.  
  255. def reply(self, msg):
  256. for line in msg.split("\n"):
  257. self.irc.msg(self.recipient, irc.translate_colors(line))
  258.  
  259. class IrcDummyPlayer(minqlx.AbstractDummyPlayer):
  260. def __init__(self, irc, user):
  261. self.irc = irc
  262. self.user = user
  263. super().__init__(name="IRC-{}".format(irc.nickname))
  264.  
  265. @property
  266. def steam_id(self):
  267. return minqlx.owner()
  268.  
  269. @property
  270. def channel(self):
  271. return IrcChannel(self.irc, self.user)
  272.  
  273. def tell(self, msg):
  274. for line in msg.split("\n"):
  275. self.irc.msg(self.user, irc.translate_colors(line))
  276.  
  277. # ====================================================================
  278. # SIMPLE ASYNC IRC
  279. # ====================================================================
  280.  
  281. re_msg = re.compile(r"^:([^ ]+) PRIVMSG ([^ ]+) :(.*)$")
  282. re_user = re.compile(r"^(.+)!(.+)@(.+)$")
  283.  
  284. class SimpleAsyncIrc(threading.Thread):
  285. def __init__(self, address, nickname, msg_handler, perform_handler, raw_handler=None, stop_event=threading.Event()):
  286. split_addr = address.split(":")
  287. self.host = split_addr[0]
  288. self.port = int(split_addr[1]) if len(split_addr) > 1 else 6667
  289. self.nickname = nickname
  290. self.msg_handler = msg_handler
  291. self.perform_handler = perform_handler
  292. self.raw_handler = raw_handler
  293. self.stop_event = stop_event
  294. self.reader = None
  295. self.writer = None
  296. self.server_options = {}
  297. super().__init__()
  298.  
  299. self._lock = threading.Lock()
  300. self._old_nickname = self.nickname
  301.  
  302. def run(self):
  303. loop = asyncio.new_event_loop()
  304. logger = minqlx.get_logger("irc")
  305. asyncio.set_event_loop(loop)
  306. while not self.stop_event.is_set():
  307. try:
  308. loop.run_until_complete(self.connect())
  309. except Exception:
  310. minqlx.log_exception()
  311.  
  312. # Disconnected. Try reconnecting in 30 seconds.
  313. logger.info("Disconnected from IRC. Reconnecting in 30 seconds...")
  314. time.sleep(30)
  315. loop.close()
  316.  
  317. def stop(self):
  318. self.stop_event.set()
  319.  
  320. def write(self, msg):
  321. if self.writer:
  322. with self._lock:
  323. self.writer.write(msg.encode(errors="ignore"))
  324.  
  325. @asyncio.coroutine
  326. def connect(self):
  327. self.reader, self.writer = yield from asyncio.open_connection(self.host, self.port)
  328. self.write("NICK {0}\r\nUSER {0} 0 * :{0}\r\n".format(self.nickname))
  329.  
  330. while not self.stop_event.is_set():
  331. line = yield from self.reader.readline()
  332. if not line:
  333. break
  334. line = line.decode("utf-8", errors="ignore").rstrip()
  335. if line:
  336. yield from self.parse_data(line)
  337.  
  338. self.write("QUIT Quit by user.\r\n")
  339. self.writer.close()
  340.  
  341. @asyncio.coroutine
  342. def parse_data(self, msg):
  343. split_msg = msg.split()
  344. if len(split_msg) > 1 and split_msg[0] == "PING":
  345. self.pong(split_msg[1].lstrip(":"))
  346. elif len(split_msg) > 3 and split_msg[1] == "PRIVMSG":
  347. r = re_msg.match(msg)
  348. user = re_user.match(r.group(1)).groups()
  349. channel = user[0] if self.nickname == r.group(2) else r.group(2)
  350. self.msg_handler(self, user, channel, r.group(3).split())
  351. elif len(split_msg) > 2 and split_msg[1] == "NICK":
  352. user = re_user.match(split_msg[0][1:])
  353. if user and user.group(1) == self.nickname:
  354. self.nickname = split_msg[2][1:]
  355. elif split_msg[1] == "005":
  356. for option in split_msg[3:-1]:
  357. opt_pair = option.split("=", 1)
  358. if len(opt_pair) == 1:
  359. self.server_options[opt_pair[0]] = ""
  360. else:
  361. self.server_options[opt_pair[0]] = opt_pair[1]
  362. elif len(split_msg) > 1 and split_msg[1] == "433":
  363. self.nickname = self._old_nickname
  364. # Stuff to do after we get the MOTD.
  365. elif re.match(r":[^ ]+ (376|422) .+", msg):
  366. self.perform_handler(self)
  367.  
  368. # If we have a raw handler, let it do its stuff now.
  369. if self.raw_handler:
  370. self.raw_handler(self, msg)
  371.  
  372. def msg(self, recipient, msg):
  373. self.write("PRIVMSG {} :{}\r\n".format(recipient, msg))
  374.  
  375. def nick(self, nick):
  376. with self._lock:
  377. self._old_nickname = self.nickname
  378. self.nickname = nick
  379. self.write("NICK {}\r\n".format(nick))
  380.  
  381. def join(self, channels):
  382. self.write("JOIN {}\r\n".format(channels))
  383.  
  384. def part(self, channels):
  385. self.write("PART {}\r\n".format(channels))
  386.  
  387. def mode(self, what, mode):
  388. self.write("MODE {} {}\r\n".format(what, mode))
  389.  
  390. def kick(self, channel, nick, reason):
  391. self.write("KICK {} {}:{}\r\n".format(channel, nick, reason))
  392.  
  393. def quit(self, reason):
  394. self.write("QUIT :{}\r\n".format(reason))
  395.  
  396. def pong(self, n):
  397. self.write("PONG :{}\r\n".format(n))
Add Comment
Please, Sign In to add comment