Advertisement
Guest User

irc.py

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