예제 #1
0
class Protocol(LineOnlyReceiver, NoChannelsProtocol):
    __version__ = "0.0.1"
    __inter_version__ = 3

    TYPE = "inter"
    CHANNELS = False

    factory = None
    config = None
    log = None
    event_manager = None
    command_manager = None

    nickname = ""
    ourselves = None
    channel = None
    can_flood = False

    control_chars = "."

    handshake_done = False

    inter_servers = {}

    _users = []

    def __init__(self, name, factory, config):
        NoChannelsProtocol.__init__(self, name, factory, config)

        self.name = name
        self.log = getLogger(self.name)
        self.event_manager = EventManager()
        self.command_manager = CommandManager()

        reactor.connectTCP(self.config["connection"]["host"],
                           self.config["connection"]["port"], self.factory,
                           120)

    def connectionMade(self):
        self.handshake_done = False
        self.inter_servers = {}
        self.nickname = ""

        self.control_chars = self.config["control_char"]

        self.ourselves = User(self.config["nickname"], self, True)
        self.channel = Channel(protocol=self)

    def lineReceived(self, line):
        try:
            self.log.trace("<- %s" % repr(line))
            message = json.loads(line)
        except Exception:
            self.log.exception("Failed to parse line")
        else:
            if "version" in message:
                v = message["version"]

                if v != self.__inter_version__:
                    self.log.error("Protocol version mismatch!")
                    self.log.error("Ours: %s | Theirs: %s" %
                                   (self.__inter_version__, v))

                    self.factory.manager.remove_protocol(self.name)
                    return

                self.log.info("Connected to Inter, version %s" % v)

                message = {"api_key": self.config["connection"]["api_key"]}

                self.send(message)

            if "from" in message:
                origin = message["from"]

                for case, default in Switch(origin):
                    if case("chat"):
                        source = message["source"]
                        msg = message["message"]

                        user = self.get_user(message["user"],
                                             server=source,
                                             create=True)
                        if not user.server:
                            user.server = source

                        if user == self.ourselves:
                            break  # Since, well, this is us.

                        if source == self.nickname:
                            break  # Since this is also us.

                        event = general_events.PreMessageReceived(
                            self,
                            user,
                            user,
                            msg,
                            "message"  # No channels
                        )
                        self.event_manager.run_callback(
                            "PreMessageReceived", event)
                        if event.printable:
                            for line in event.message.split("\n"):
                                self.log.info("<%s> %s" % (user, line))

                        if not event.cancelled:
                            result = self.command_manager.process_input(
                                event.message, user, user, self,
                                self.control_chars, self.nickname)

                            for c, d in Switch(result[0]):
                                if c(CommandState.RateLimited):
                                    self.log.debug("Command rate-limited")
                                    user.respond("That command has been "
                                                 "rate-limited, please try "
                                                 "again later.")
                                    return  # It was a command
                                if c(CommandState.NotACommand):
                                    self.log.debug("Not a command")
                                    break
                                if c(CommandState.UnknownOverridden):
                                    self.log.debug("Unknown command "
                                                   "overridden")
                                    return  # It was a command
                                if c(CommandState.Unknown):
                                    self.log.debug("Unknown command")
                                    break
                                if c(CommandState.Success):
                                    self.log.debug("Command ran successfully")
                                    return  # It was a command
                                if c(CommandState.NoPermission):
                                    self.log.debug("No permission to run "
                                                   "command")
                                    return  # It was a command
                                if c(CommandState.Error):
                                    user.respond("Error running command: "
                                                 "%s" % result[1])
                                    return  # It was a command
                                if d:
                                    self.log.debug("Unknown command state: "
                                                   "%s" % result[0])
                                    break
                            second_event = general_events.MessageReceived(
                                self, user, user, msg, "message")

                            self.event_manager.run_callback(
                                "MessageReceived", second_event)
                        break
                    if case("players"):
                        _type = message["type"]
                        target = message["target"]

                        if _type == "list":
                            if target == "all":
                                # All servers, we can just overwrite the list.
                                self.inter_servers = {}
                                players = message["players"]

                                for key in players:
                                    self.inter_servers[key] = []

                                    for user in players[key]:
                                        obj = User(user, self, True)
                                        obj.server = key

                                        self.inter_servers[key].append(obj)

                                        if obj not in self._users:
                                            self._users.append(obj)

                                self.log.info("Got player list.")

                                for key in self.inter_servers.keys():
                                    self.log.info(
                                        "%s: %s players" %
                                        (key, len(self.inter_servers[key])))

                                event = inter_events.InterServerListReceived(
                                    self, self.inter_servers)

                                self.event_manager.run_callback(
                                    "Inter/ServerListReceived", event)
                            else:
                                # Unexpected!
                                self.log.warn("Unknown list target: %s" %
                                              target)
                        elif _type == "offline":
                            player = self.get_user(message["player"], target,
                                                   True)

                            player.server = target

                            if target not in self.inter_servers:
                                self.inter_servers[target] = []

                            if player in self.inter_servers[target]:
                                self.inter_servers[target].remove(player)

                            if player in self._users:
                                self._users.remove(player)

                            self.log.info("%s disconnected from %s." %
                                          (player, target))

                            event = general_events.UserDisconnected(
                                self, player)

                            self.event_manager.run_callback(
                                "UserDisconnected", event)

                            second_event = inter_events.InterPlayerDisonnected(
                                self, player)

                            self.event_manager.run_callback(
                                "Inter/PlayerDisconnected", second_event)

                        elif _type == "online":
                            player = self.get_user(message["player"], target,
                                                   True)

                            player.server = target

                            if target not in self.inter_servers:
                                self.inter_servers[target] = []

                            if player not in self.inter_servers[target]:
                                self.inter_servers[target].append(player)

                            if player not in self._users:
                                self._users.append(player)

                            self.log.info("%s connected to %s." %
                                          (player, target))

                            event = inter_events.InterPlayerConnected(
                                self, player)

                            self.event_manager.run_callback(
                                "Inter/PlayerConnected", event)
                        break
                    if case("auth"):
                        action = message["action"]

                        if action == "authenticated":
                            if not self.handshake_done and "status" in message:
                                status = message["status"]

                                if status == "success":
                                    self.nickname = message["name"]
                                    self.log.info("Authenticated as %s" %
                                                  self.nickname)
                                    self.handshake_done = True

                                    # Get players
                                    self.send_get_players()

                                    self.send_connect(self.ourselves.nickname)

                                    event = inter_events.InterAuthenticated(
                                        self)

                                    self.event_manager.run_callback(
                                        "Inter/Authenticated", event)
                                else:
                                    error = message["error"]
                                    self.log.error("Error authenticating: %s" %
                                                   error)

                                    event = (
                                        inter_events.InterAuthenticationError(
                                            self, error))

                                    self.event_manager.run_callback(
                                        "Inter/AuthenticationError", event)

                                    self.transport.close()
                                    self.factory.manager.remove_protocol(
                                        self.name)
                            else:
                                name = message["name"]
                                self.log.info("Server connected to Inter: %s" %
                                              name)

                                event = inter_events.InterServerConnected(
                                    self, name)

                                self.event_manager.run_callback(
                                    "Inter/ServerConnected", event)
                        else:
                            name = message["name"]
                            self.log.info(
                                "Server disconnected from Inter: %s" % name)

                            event = inter_events.InterServerDisonnected(
                                self, name)

                            self.event_manager.run_callback(
                                "Inter/ServerDisconnected", event)

                            if name in self.inter_servers:
                                del self.inter_servers[name]
                        break
                    if case("core"):
                        event = inter_events.InterCoreMessage(self, message)

                        self.event_manager.run_callback(
                            "Inter/CoreMessage", event)
                        break
                    if case("ping"):
                        timestamp = message["timestamp"]

                        event = inter_events.InterPing(self, timestamp)

                        self.event_manager.run_callback("Inter/Ping", event)

                        self.send_pong(timestamp)
                        break
                    if default:
                        self.log.warn("Unknown message origin: %s" % origin)

                        event = inter_events.InterUnknownMessage(self, message)

                        self.event_manager.run_callback(
                            "Inter/UnknownMessage", event)

                        break

    def send(self, _json):
        self.sendLine(json.dumps(_json))

    def sendLine(self, line):
        self.log.trace("-> %s" % repr(line))
        LineOnlyReceiver.sendLine(self, line)

    def shutdown(self):
        """
        Called when a protocol needs to disconnect. Cleanup should be done
        here.
        """
        self.transport.loseConnection()

    def get_user(self, username, server=None, create=False):
        """
        Used to retrieve a user. Return None if we can't find it.
        :param user: string representing the user we need.
        """
        if server is None:
            for key in self.inter_servers:
                for user in self.inter_servers[key]:
                    if user.name.lower() == username.lower():
                        return user
        else:
            if server in self.inter_servers:
                for user in self.inter_servers[server]:
                    if user.name.lower() == username.lower():
                        return user

        if create:
            return User(username, self)

        return None

    def send_msg(self, target, message, target_type=None, use_event=True):
        """
        Send a message.
        :param target: Ignored.
        :param message: The message to send.
        :param target_type: Ignored.
        :param use_event: Whether to fire the MessageSent event or not.
        :return: Boolean describing whether the target was found and messaged.
        """
        # Target and target type are ignored here.
        self.send({
            "action": "chat",
            "message": message,
            "user": str(self.ourselves),
            "primaryGroup": self.name,
            "prefix": "",
            "suffix": "",
            "UUID": "N/A",
            "world": self.name
        })

        return True

    def send_msg_other(self, user, message):
        self.send({
            "action": "chat",
            "message": message,
            "user": str(user),
            "primaryGroup": self.name,
            "prefix": "",
            "suffix": "",
            "UUID": "N/A",
            "world": self.name
        })

        return True

    def send_action(self, target, message, target_type=None, use_event=True):
        # Target and target type are ignored here.
        return self.send_msg(target, "*%s*" % message, target_type, use_event)

    def send_action_other(self, user, message):
        self.send({
            "action": "chat",
            "message": "*%s*" % message,
            "user": str(user),
            "primaryGroup": self.name,
            "prefix": "",
            "suffix": "",
            "UUID": "N/A",
            "world": self.name
        })

    def send_connect(self, username):
        message = {"action": "players", "type": "online", "player": username}

        self.send(message)

    def send_disconnect(self, username):
        message = {"action": "players", "type": "offline", "player": username}

        self.send(message)

    def send_get_players(self):
        message = {"action": "players", "type": "list"}

        self.send(message)

    def send_pong(self, timestamp):
        message = {
            "pong": timestamp,
        }

        self.send(message)
예제 #2
0
class WebPlugin(PluginObject):
    """
    Web plugin object
    """

    api_log = None
    api_keys = None

    api_key_data = {}
    config = {}
    data = {}

    namespace = {}  # Global, not used right now

    handlers = {}  # Cyclone handlers

    interface = ""  # Listening interface
    listen_port = 8080

    running = False

    application = None  # Cyclone application
    port = None  # Twisted's server port
    storage = None
    template_loader = None

    navbar_items = {}

    ## Stuff plugins might find useful

    commands = None
    events = None
    packages = None
    plugins = None
    sessions = None
    stats = None

    ## Internal(ish) functions

    def setup(self):
        self.storage = StorageManager()

        try:
            self.config = self.storage.get_file(self, "config", YAML,
                                                "plugins/web.yml")
            self.logger.debug("Config loaded")
        except Exception:
            self.logger.exception(_("Error loading configuration"))
            self._disable_self()
            return
        if not self.config.exists:
            self.logger.error(_("Unable to find config/plugins/web.yml"))
            self._disable_self()
            return

        try:
            self.data = self.storage.get_file(self, "data", JSON,
                                              "plugins/web/data.json")
            self.logger.debug("Data loaded")
        except Exception:
            self.logger.exception("Error loading data file!")
            return self._disable_self()

        try:
            _sessions = self.storage.get_file(self, "data", JSON,
                                              "plugins/web/sessions.json")
            self.logger.debug("Sessions loaded")
        except Exception:
            self.logger.exception("Error loading sessions file!")
            return self._disable_self()

        try:
            self.api_log = open("logs/api.log", "w")
        except Exception:
            self.logger.exception("Unable to open api log file!")
            return self._disable_self()

        try:
            self.api_key_data = self.storage.get_file(
                self, "data", JSON, "plugins/web/apikeys.json"
            )
            self.logger.debug("Sessions loaded")
        except Exception:
            self.logger.exception("Error loading API keys!")
            return self._disable_self()

        try:
            self.api_log = open("logs/api.log", "w")
        except Exception:
            self.logger.exception("Unable to open api log file!")
            return self._disable_self()

        self.config.add_callback(self.restart)
        self.data.add_callback(self.restart)

        # Index page

        self.add_handler(r"/", "plugins.web.routes.index.Route")

        # Login-related

        self.add_handler(r"/login", "plugins.web.routes.login.Route")
        self.add_handler(r"/logout", "plugins.web.routes.logout.Route")
        self.add_handler(
            r"/login/reset",
            "plugins.web.routes.login-reset.Route"
        )

        # Accounts-related

        self.add_handler(r"/account", "plugins.web.routes.account.index.Route")
        self.add_handler(
            r"/account/password/change",
            "plugins.web.routes.account.password.change.Route"
        )
        self.add_handler(
            r"/account/apikeys/create",
            "plugins.web.routes.account.apikeys.create.Route"
        )
        self.add_handler(
            r"/account/apikeys/delete",
            "plugins.web.routes.account.apikeys.delete.Route"
        )
        self.add_handler(
            r"/account/users/logout",
            "plugins.web.routes.account.users.logout.Route"
        )

        # Admin-related

        self.add_handler(
            r"/admin",
            "plugins.web.routes.admin.index.Route"
        )
        self.add_handler(
            r"/admin/files",
            "plugins.web.routes.admin.files.Route"
        )
        self.add_handler(
            r"/admin/files/(config|data)/(.*)",
            "plugins.web.routes.admin.file.Route"
        )
        self.add_handler(
            r"/api/admin/get_stats",
            "plugins.web.routes.api.admin.get_stats.Route"
        )

        self.add_navbar_entry("admin", "/admin", "settings")

        # API routes

        self.add_api_handler(
            r"/plugins/web/get_username",
            "plugins.web.routes.api.plugins.web.get_username.Route"
        )

        # Stuff routes might find useful

        self.api_keys = APIKeys(self, self.api_key_data)
        self.commands = CommandManager()
        self.events = EventManager()
        self.packages = Packages(False)
        self.plugins = PluginManager()
        self.sessions = Sessions(self, _sessions)
        self.stats = Stats()

        # Load 'er up!

        r = self.load()

        if not r:
            self._disable_self()
            return

        if not self.factory_manager.running:
            self.events.add_callback(
                "ReactorStarted", self, self.start, 0
            )
        else:
            self.start()

    def load(self):
        if "secret" not in self.data:
            self.logger.warn("Generating secret. DO NOT SHARE IT WITH ANYONE!")
            self.logger.warn("It's stored in data/plugins/web/data.json - "
                             "keep this file secure!")
            with self.data:
                self.data["secret"] = mkpasswd(60, 20, 20, 20)

        self.template_loader = TemplateLoader(self)

        if self.config.get("output_requests", True):
            log_function = self.log_request
        else:
            log_function = self.null_log

        self.application = Application(
            list(self.handlers.items()),  # Handler list

            ## General settings
            xheaders=True,
            log_function=log_function,
            gzip=True,  # Are there browsers that don't support this now?
            # error_handler=ErrorHandler,

            ## Security settings
            cookie_secret=self.data["secret"],
            login_url="/login",

            ## Template settings
            template_loader=self.template_loader,

            ## Static file settings
            static_path="web/static"
        )

        if self.config.get("hosted", False):
            hosted = self.config["hosted"]

            if isinstance(hosted, dict):
                self.interface = os.environ.get(hosted["hostname"], False)
                self.port = os.environ.get(hosted["port"], False)

                if not self.interface:
                    self.logger.error(
                        "Unknown env var: %s" % hosted["hostname"]
                    )
                    return False
                if not self.port:
                    self.logger.error(
                        "Unknown env var: %s" % hosted["port"]
                    )
                    return False
            else:
                if hosted in ["openshift"]:
                    self.interface = os.environ.get("OPENSHIFT__IP", False)
                    self.port = os.environ.get("OPENSHIFT__PORT", False)

                    if not self.interface:
                        self.logger.error(
                            "Unknown env var: OPENSHIFT__IP - Are you on "
                            "OpenShift?"
                        )
                        return False
                    if not self.port:
                        self.logger.error(
                            "Unknown env var: OPENSHIFT__PORT - Are you on "
                            "OpenShift?"
                        )
                        return False
                else:
                    self.logger.error("Unknown hosted service: %s" % hosted)
                    return False

        else:
            if self.config.get("hostname", "0.0.0.0").strip() == "0.0.0.0":
                self.interface = ""
            else:
                self.interface = self.config.get("hostname")

            self.listen_port = self.config.get("port", 8080)

        return True

    def start(self, _=None):
        self.stats.start()

        self.port = reactor.listenTCP(
            self.listen_port, self.application, interface=self.interface
        )

        self.logger.info("Server started")
        self.running = True

        self.events.run_callback(
            "Web/ServerStartedEvent",
            ServerStartedEvent(self, self.application)
        )

    def stop(self):
        self.application.doStop()
        d = self.port.stopListening()

        d.addCallback(lambda _: self.logger.info("Server stopped"))
        d.addCallback(lambda _: setattr(self, "running", False))
        d.addCallback(lambda _: self.events.run_callback(
            "Web/ServerStopped", ServerStoppedEvent(self)
        ))

        d.addErrback(lambda f: self.logger.error("Failed to stop: %s" % f))

        self.stats.stop()

        return d

    def restart(self):
        d = self.stop()

        d.addCallback(lambda _: [self.load(), self.start()])

    def deactivate(self):
        d = self.stop()

        self.handlers.clear()
        self.navbar_items.clear()

        return d

    def log_request(self, request):
        log = self.logger.info

        status_code = request.get_status()

        if status_code >= 500:
            log = self.logger.error
        elif status_code >= 400:
            log = self.logger.warn

        path = request.request.path

        # Check if this is an API method and hide the key if so

        matched = re.match(r"/api/v[0-9]/([a-zA-Z0-9]+)/.*", path)

        if matched:
            key = matched.groups()[0]
            user = self.api_keys.get_username(key)

            if user:
                path = path.replace(key, "<API: %s>" % user)
            else:
                path = path.replace(key, "<API: Invalid key>")

        log(
            "[%s] %s %s -> HTTP %s"
            % (
                request.request.remote_ip,
                request.request.method,
                path,
                request.get_status()
            )
        )

    def null_log(self, *args, **kwargs):
        pass

    ## Public API functions

    def add_api_handler(self, pattern, handler, version=1):
        if not pattern.startswith("/"):
            pattern = "/%s" % pattern
        pattern = "/api/v%s/([A-Za-z0-9]+)%s" % (version, pattern)

        return self.add_handler(pattern, handler)

    def add_handler(self, pattern, handler):
        self.logger.debug("Adding route: %s -> %s" % (pattern, handler))

        if pattern in self.handlers:
            self.logger.debug("Route already exists.")
            return False

        self.handlers[pattern] = handler

        if self.application is not None:
            self.application.add_handlers(r".*$", [(pattern, handler)])

        self.logger.debug("Handlers list: %s" % list(self.handlers.values()))

    def add_navbar_entry(self, title, url, icon="question"):
        if title in self.navbar_items:
            return False
        self.logger.debug("Adding navbar entry: %s -> %s" % (title, url))
        self.navbar_items[title] = {"url": url, "active": False, "icon": icon}
        return True

    def check_permission(self, perm, session=None):
        if session is None:
            username = None
        elif isinstance(session, str) or isinstance(session, unicode):
            username = session
        else:
            username = session["username"]

        return self.commands.perm_handler.check(
            perm, username, "web", "plugin-web"
        )

    def remove_api_handlers(self, *names, **kwargs):
        """
        :param names:
        :param version:
        :return:
        """

        version = kwargs.get("version", 1)
        patterns = []

        for pattern in names:
            if not pattern.startswith("/"):
                pattern = "/%s" % pattern
            pattern = "/api/v%s/([A-Za-z0-9]+)%s" % (version, pattern)

            patterns.append(pattern)

        return self.remove_handlers(*patterns)

    def remove_handlers(self, *names):
        found = False

        for name in names:
            if name in self.handlers:
                found = True
                del self.handlers[name]

        if found:
            self.restart()

    def write_api_log(self, address, key, username, message):
        self.api_log.write(
            "%s | %s (%s) | %s\n" % (address, key, username, message)
        )
예제 #3
0
class Protocol(LineOnlyReceiver, NoChannelsProtocol):
    __version__ = "0.0.1"
    __inter_version__ = 3

    TYPE = "inter"
    CHANNELS = False

    factory = None
    config = None
    log = None
    event_manager = None
    command_manager = None

    nickname = ""
    ourselves = None
    channel = None
    can_flood = False

    control_chars = "."

    handshake_done = False

    inter_servers = {}

    _users = []

    def __init__(self, name, factory, config):
        NoChannelsProtocol.__init__(self, name, factory, config)

        self.name = name
        self.log = getLogger(self.name)
        self.event_manager = EventManager()
        self.command_manager = CommandManager()

        reactor.connectTCP(
            self.config["connection"]["host"],
            self.config["connection"]["port"],
            self.factory,
            120
        )

    def connectionMade(self):
        self.handshake_done = False
        self.inter_servers = {}
        self.nickname = ""

        self.control_chars = self.config["control_char"]

        self.ourselves = User(self.config["nickname"], self, True)
        self.channel = Channel(protocol=self)

    def lineReceived(self, line):
        try:
            self.log.trace("<- %s" % repr(line))
            message = json.loads(line)
        except Exception:
            self.log.exception("Failed to parse line")
        else:
            if "version" in message:
                v = message["version"]

                if v != self.__inter_version__:
                    self.log.error("Protocol version mismatch!")
                    self.log.error("Ours: %s | Theirs: %s"
                                   % (self.__inter_version__, v))

                    self.factory.manager.remove_protocol(self.name)
                    return

                self.log.info("Connected to Inter, version %s" % v)

                message = {
                    "api_key": self.config["connection"]["api_key"]
                }

                self.send(message)

            if "from" in message:
                origin = message["from"]

                for case, default in Switch(origin):
                    if case("chat"):
                        source = message["source"]
                        msg = message["message"]

                        user = self.get_user(
                            message["user"], server=source, create=True
                        )
                        if not user.server:
                            user.server = source

                        if user == self.ourselves:
                            break  # Since, well, this is us.

                        if source == self.nickname:
                            break  # Since this is also us.

                        event = general_events.PreMessageReceived(
                            self, user, user, msg, "message"  # No channels
                        )
                        self.event_manager.run_callback("PreMessageReceived",
                                                        event)
                        if event.printable:
                            for line in event.message.split("\n"):
                                self.log.info("<%s> %s" % (user, line))

                        if not event.cancelled:
                            result = self.command_manager.process_input(
                                event.message, user, user, self,
                                self.control_chars, self.nickname
                            )

                            for c, d in Switch(result[0]):
                                if c(CommandState.RateLimited):
                                    self.log.debug("Command rate-limited")
                                    user.respond("That command has been "
                                                 "rate-limited, please try "
                                                 "again later.")
                                    return  # It was a command
                                if c(CommandState.NotACommand):
                                    self.log.debug("Not a command")
                                    break
                                if c(CommandState.UnknownOverridden):
                                    self.log.debug("Unknown command "
                                                   "overridden")
                                    return  # It was a command
                                if c(CommandState.Unknown):
                                    self.log.debug("Unknown command")
                                    break
                                if c(CommandState.Success):
                                    self.log.debug("Command ran successfully")
                                    return  # It was a command
                                if c(CommandState.NoPermission):
                                    self.log.debug("No permission to run "
                                                   "command")
                                    return  # It was a command
                                if c(CommandState.Error):
                                    user.respond("Error running command: "
                                                 "%s" % result[1])
                                    return  # It was a command
                                if d:
                                    self.log.debug("Unknown command state: "
                                                   "%s" % result[0])
                                    break
                            second_event = general_events.MessageReceived(
                                self, user, user, msg, "message"
                            )

                            self.event_manager.run_callback(
                                "MessageReceived", second_event
                            )
                        break
                    if case("players"):
                        _type = message["type"]
                        target = message["target"]

                        if _type == "list":
                            if target == "all":
                                # All servers, we can just overwrite the list.
                                self.inter_servers = {}
                                players = message["players"]

                                for key in players:
                                    self.inter_servers[key] = []

                                    for user in players[key]:
                                        obj = User(user, self, True)
                                        obj.server = key

                                        self.inter_servers[key].append(obj)

                                        if obj not in self._users:
                                            self._users.append(obj)

                                self.log.info("Got player list.")

                                for key in self.inter_servers.keys():
                                    self.log.info(
                                        "%s: %s players" % (
                                            key,
                                            len(self.inter_servers[key])
                                        )
                                    )

                                event = inter_events.InterServerListReceived(
                                    self, self.inter_servers
                                )

                                self.event_manager.run_callback(
                                    "Inter/ServerListReceived", event
                                )
                            else:
                                # Unexpected!
                                self.log.warn("Unknown list target: %s"
                                              % target)
                        elif _type == "offline":
                            player = self.get_user(
                                message["player"], target, True
                            )

                            player.server = target

                            if target not in self.inter_servers:
                                self.inter_servers[target] = []

                            if player in self.inter_servers[target]:
                                self.inter_servers[target].remove(player)

                            if player in self._users:
                                self._users.remove(player)

                            self.log.info("%s disconnected from %s."
                                          % (player, target))

                            event = general_events.UserDisconnected(
                                self, player
                            )

                            self.event_manager.run_callback(
                                "UserDisconnected", event
                            )

                            second_event = inter_events.InterPlayerDisonnected(
                                self, player
                            )

                            self.event_manager.run_callback(
                                "Inter/PlayerDisconnected", second_event
                            )

                        elif _type == "online":
                            player = self.get_user(
                                message["player"], target, True
                            )

                            player.server = target

                            if target not in self.inter_servers:
                                self.inter_servers[target] = []

                            if player not in self.inter_servers[target]:
                                self.inter_servers[target].append(player)

                            if player not in self._users:
                                self._users.append(player)

                            self.log.info("%s connected to %s."
                                          % (player, target))

                            event = inter_events.InterPlayerConnected(
                                self, player
                            )

                            self.event_manager.run_callback(
                                "Inter/PlayerConnected", event
                            )
                        break
                    if case("auth"):
                        action = message["action"]

                        if action == "authenticated":
                            if not self.handshake_done and "status" in message:
                                status = message["status"]

                                if status == "success":
                                    self.nickname = message["name"]
                                    self.log.info("Authenticated as %s"
                                                  % self.nickname)
                                    self.handshake_done = True

                                    # Get players
                                    self.send_get_players()

                                    self.send_connect(self.ourselves.nickname)

                                    event = inter_events.InterAuthenticated(
                                        self
                                    )

                                    self.event_manager.run_callback(
                                        "Inter/Authenticated", event
                                    )
                                else:
                                    error = message["error"]
                                    self.log.error("Error authenticating: %s"
                                                   % error)

                                    event = (
                                        inter_events.InterAuthenticationError(
                                            self, error
                                        )
                                    )

                                    self.event_manager.run_callback(
                                        "Inter/AuthenticationError", event
                                    )

                                    self.transport.close()
                                    self.factory.manager.remove_protocol(
                                        self.name
                                    )
                            else:
                                name = message["name"]
                                self.log.info("Server connected to Inter: %s"
                                              % name)

                                event = inter_events.InterServerConnected(
                                    self, name
                                )

                                self.event_manager.run_callback(
                                    "Inter/ServerConnected", event
                                )
                        else:
                            name = message["name"]
                            self.log.info("Server disconnected from Inter: %s"
                                          % name)

                            event = inter_events.InterServerDisonnected(
                                self, name
                            )

                            self.event_manager.run_callback(
                                "Inter/ServerDisconnected", event
                            )

                            if name in self.inter_servers:
                                del self.inter_servers[name]
                        break
                    if case("core"):
                        event = inter_events.InterCoreMessage(
                            self, message
                        )

                        self.event_manager.run_callback(
                            "Inter/CoreMessage", event
                        )
                        break
                    if case("ping"):
                        timestamp = message["timestamp"]

                        event = inter_events.InterPing(
                            self, timestamp
                        )

                        self.event_manager.run_callback(
                            "Inter/Ping", event
                        )

                        self.send_pong(timestamp)
                        break
                    if default:
                        self.log.warn("Unknown message origin: %s" % origin)

                        event = inter_events.InterUnknownMessage(
                            self, message
                        )

                        self.event_manager.run_callback(
                            "Inter/UnknownMessage", event
                        )

                        break

    def send(self, _json):
        self.sendLine(
            json.dumps(_json)
        )

    def sendLine(self, line):
        self.log.trace("-> %s" % repr(line))
        LineOnlyReceiver.sendLine(self, line)

    def shutdown(self):
        """
        Called when a protocol needs to disconnect. Cleanup should be done
        here.
        """
        self.transport.loseConnection()

    def get_user(self, username, server=None, create=False):
        """
        Used to retrieve a user. Return None if we can't find it.
        :param user: string representing the user we need.
        """
        if server is None:
            for key in self.inter_servers:
                for user in self.inter_servers[key]:
                    if user.name.lower() == username.lower():
                        return user
        else:
            if server in self.inter_servers:
                for user in self.inter_servers[server]:
                    if user.name.lower() == username.lower():
                        return user

        if create:
            return User(username, self)

        return None

    def send_msg(self, target, message, target_type=None, use_event=True):
        """
        Send a message.
        :param target: Ignored.
        :param message: The message to send.
        :param target_type: Ignored.
        :param use_event: Whether to fire the MessageSent event or not.
        :return: Boolean describing whether the target was found and messaged.
        """
        # Target and target type are ignored here.
        self.send(
            {
                "action": "chat",
                "message": message,
                "user": str(self.ourselves),
                "primaryGroup": self.name,
                "prefix": "",
                "suffix": "",
                "UUID": "N/A",
                "world": self.name
            }
        )

        return True

    def send_msg_other(self, user, message):
        self.send(
            {
                "action": "chat",
                "message": message,
                "user": str(user),
                "primaryGroup": self.name,
                "prefix": "",
                "suffix": "",
                "UUID": "N/A",
                "world": self.name
            }
        )

        return True

    def send_action(self, target, message, target_type=None, use_event=True):
        # Target and target type are ignored here.
        return self.send_msg(target, "*%s*" % message, target_type, use_event)

    def send_action_other(self, user, message):
        self.send(
            {
                "action": "chat",
                "message": "*%s*" % message,
                "user": str(user),
                "primaryGroup": self.name,
                "prefix": "",
                "suffix": "",
                "UUID": "N/A",
                "world": self.name
            }
        )

    def send_connect(self, username):
        message = {
            "action": "players",
            "type": "online",
            "player": username
        }

        self.send(message)

    def send_disconnect(self, username):
        message = {
            "action": "players",
            "type": "offline",
            "player": username
        }

        self.send(message)

    def send_get_players(self):
        message = {
            "action": "players",
            "type": "list"
        }

        self.send(message)

    def send_pong(self, timestamp):
        message = {
            "pong": timestamp,
        }

        self.send(message)
예제 #4
0
class Manager(object):
    """
    Manager for keeping track of multiple factories - one per protocol.

    This is so that the bot can connect to multiple services at once, and have
    them communicate with each other.
    """

    __metaclass__ = Singleton

    #: Instance of the storage manager
    storage = None

    #: Storage for all of our factories.
    factories = {}

    #: Storage for all of the protocol configs.
    configs = {}

    #: The main configuration is stored here.
    main_config = None

    #: Whether the manager is already running or not
    running = False

    def __init__(self):
        self.commands = CommandManager()
        self.event_manager = EventManager()
        self.logger = getLogger("Manager")
        self.plugman = PluginManager(self)
        self.yapsy_logger = getLogger("yapsy")

        self.metrics = None

    @property
    def all_plugins(self):
        return self.plugman.info_objects

    @property
    def loaded_plugins(self):
        return self.plugman.plugin_objects

    def setup(self):
        signal.signal(signal.SIGINT, self.signal_callback)

        self.yapsy_logger.debug_ = self.yapsy_logger.debug
        self.yapsy_logger.debug = self.yapsy_logger.trace

        self.storage = StorageManager()
        self.main_config = self.storage.get_file(self, "config", YAML,
                                                 "settings.yml")

        self.commands.set_factory_manager(self)

        self.load_config()  # Load the configuration

        try:
            self.metrics = Metrics(self.main_config, self)
        except Exception:
            self.logger.exception(_("Error setting up metrics."))

        self.plugman.scan()
        self.load_plugins()  # Load the configured plugins
        self.load_protocols()  # Load and set up the protocols

        if not len(self.factories):
            self.logger.info(_("It seems like no protocols are loaded. "
                               "Shutting down.."))
            return

    def run(self):
        if not self.running:
            event = ReactorStartedEvent(self)

            reactor.callLater(0, self.event_manager.run_callback,
                              "ReactorStarted", event)

            self.running = True
            reactor.run()
        else:
            raise RuntimeError(_("Manager is already running!"))

    def signal_callback(self, signum, frame):
        try:
            try:
                self.unload()
            except Exception:
                self.logger.exception(_("Error while unloading!"))
                try:
                    reactor.stop()
                except Exception:
                    try:
                        reactor.crash()
                    except Exception:
                        pass
        except Exception:
            exit(0)

    # Load stuff

    def load_config(self):
        """
        Load the main configuration file.

        :return: Whether the config was loaded or not
        :rtype: bool
        """

        try:
            self.logger.info(_("Loading global configuration.."))
            if not self.main_config.exists:
                self.logger.error(_(
                    "Main configuration not found! Please correct this and try"
                    " again."))
                return False
        except IOError:
            self.logger.error(_(
                "Unable to load main configuration at config/settings.yml"))
            self.logger.error(_("Please check that this file exists."))
            return False
        except Exception:
            self.logger.exception(_(
                "Unable to load main configuration at config/settings.yml"))
            return False
        return True

    def load_plugins(self):
        """
        Attempt to load all of the plugins.
        """

        self.logger.info(_("Loading plugins.."))

        self.logger.trace(_("Configured plugins: %s")
                          % ", ".join(self.main_config["plugins"]))

        self.plugman.load_plugins(self.main_config.get("plugins", []))

        event = PluginsLoadedEvent(self, self.plugman.plugin_objects)
        self.event_manager.run_callback("PluginsLoaded", event)

    @deprecated("Use the plugin manager directly")
    def load_plugin(self, name, unload=False):
        """
        Load a single plugin by name.

        This will return one of the system.enums.PluginState values.

        :param name: The plugin to load.
        :type name: str

        :param unload: Whether to unload the plugin, if it's already loaded.
        :type unload: bool
        """

        result = self.plugman.load_plugin(name)

        if result is PluginState.AlreadyLoaded:
            if unload:
                result_two = self.plugman.unload_plugin(name)

                if result_two is not PluginState.Unloaded:
                    return result_two

                result = self.plugman.load_plugin(name)

        return result

    @deprecated("Use the plugin manager directly")
    def collect_plugins(self):
        """
        Collect all possible plugin candidates.
        """

        self.plugman.scan()

    def load_protocols(self):
        """
        Load and set up all of the configured protocols.
        """

        self.logger.info(_("Setting up protocols.."))

        for protocol in self.main_config["protocols"]:
            if protocol.lower().startswith("plugin-"):
                self.logger.error("Invalid protocol name: %s" % protocol)
                self.logger.error(
                    "Protocol names beginning with \"plugin-\" are reserved "
                    "for plugin use."
                )
                continue

            self.logger.info(_("Setting up protocol: %s") % protocol)
            conf_location = "protocols/%s.yml" % protocol
            result = self.load_protocol(protocol, conf_location)

            if result is not PROTOCOL_LOADED:
                if result is PROTOCOL_ALREADY_LOADED:
                    self.logger.warn(_("Protocol is already loaded."))
                elif result is PROTOCOL_CONFIG_NOT_EXISTS:
                    self.logger.warn(_("Unable to find protocol "
                                       "configuration."))
                elif result is PROTOCOL_LOAD_ERROR:
                    self.logger.warn(_("Error detected while loading "
                                       "protocol."))
                elif result is PROTOCOL_SETUP_ERROR:
                    self.logger.warn(_("Error detected while setting up "
                                       "protocol."))

    def load_protocol(self, name, conf_location):
        """
        Attempt to load a protocol by name. This can return one of the
        following, from system.constants:

        * PROTOCOL_ALREADY_LOADED
        * PROTOCOL_CONFIG_NOT_EXISTS
        * PROTOCOL_LOAD_ERROR
        * PROTOCOL_LOADED
        * PROTOCOL_SETUP_ERROR

        :param name: The name of the protocol
        :type name: str

        :param conf_location: The location of the config file, relative
            to the config/ directory, or a Config object
        :type conf_location: str, Config
        """

        if name in self.factories:
            return PROTOCOL_ALREADY_LOADED

        config = conf_location
        if not isinstance(conf_location, Config):
            # TODO: Prevent upward directory traversal properly
            conf_location = conf_location.replace("..", "")
            try:
                config = self.storage.get_file(self, "config", YAML,
                                               conf_location)
                if not config.exists:
                    return PROTOCOL_CONFIG_NOT_EXISTS
            except Exception:
                self.logger.exception(
                    _("Unable to load configuration for the '%s' protocol.")
                    % name)
                return PROTOCOL_LOAD_ERROR
        try:
            self.factories[name] = Factory(name, config, self)
            self.factories[name].setup()
            return PROTOCOL_LOADED
        except Exception:
            if name in self.factories:
                del self.factories[name]
            self.logger.exception(
                _("Unable to create factory for the '%s' protocol!")
                % name)
            return PROTOCOL_SETUP_ERROR

    # Reload stuff

    @deprecated("Use the plugin manager directly")
    def reload_plugin(self, name):
        """
        Attempt to reload a plugin by name.

        This will return one of the system.enums.PluginState values.

        :param name: The name of the plugin
        :type name: str
        """
        return self.plugman.reload_plugin(name)

    def reload_protocol(self, name):
        factory = self.get_factory(name)

        if name is not None:
            factory.shutdown()
            factory.setup()
            return True

    # Unload stuff

    @deprecated("Use the plugin manager directly")
    def unload_plugin(self, name):
        """
        Attempt to unload a plugin by name.

        This will return one of the system.enums.PluginState values.

        :param name: The name of the plugin
        :type name: str
        """

        return self.plugman.unload_plugin(name)

    def unload_protocol(self, name):  # Removes with a shutdown
        """
        Attempt to unload a protocol by name. This will also shut it down.

        :param name: The name of the protocol
        :type name: str

        :return: Whether the protocol was unloaded
        :rtype: bool
        """

        if name in self.factories:
            proto = self.factories[name]
            try:
                proto.shutdown()
            except Exception:
                self.logger.exception(_("Error shutting down protocol %s")
                                      % name)
            finally:
                try:
                    self.storage.release_file(self, "config",
                                              "protocols/%s.yml" % name)
                    self.storage.release_files(proto)
                    self.storage.release_files(proto.protocol)
                except Exception:
                    self.logger.exception("Error releasing files for protocol "
                                          "%s" % name)
            del self.factories[name]
            return True
        return False

    def unload(self):
        """
        Shut down and unload everything.
        """

        # Shut down!
        for name in self.factories.keys():
            self.logger.info(_("Unloading protocol: %s") % name)
            self.unload_protocol(name)

        self.plugman.unload_plugins()

        if reactor.running:
            try:
                reactor.stop()
            except Exception:
                self.logger.exception("Error stopping reactor")

    # Grab stuff

    def get_protocol(self, name):
        """
        Get the instance of a protocol, by name.

        :param name: The name of the protocol
        :type name: str

        :return: The protocol, or None if it doesn't exist.
        """

        if name in self.factories:
            return self.factories[name].protocol
        return None

    def get_factory(self, name):
        """
        Get the instance of a protocol's factory, by name.

        :param name: The name of the protocol
        :type name: str

        :return: The factory, or None if it doesn't exist.
        """

        if name in self.factories:
            return self.factories[name]
        return None

    @deprecated("Use the plugin manager directly")
    def get_plugin(self, name):
        """
        Get the insatnce of a plugin, by name.
        :param name: The name of the plugin
        :type name: str

        :return: The plugin, or None if it isn't loaded.
        """

        return self.plugman.get_plugin(name)

    def remove_protocol(self, protocol):  # Removes without shutdown
        """
        Remove a protocol without shutting it down. You shouldn't use this.

        :param protocol: The name of the protocol
        :type protocol: str

        :return: Whether the protocol was removed.
        :rtype: bool
        """

        if protocol in self.factories:
            del self.factories[protocol]
            return True
        return False
예제 #5
0
class FactoidsPlugin(plugin.PluginObject):

    CHANNEL = "channel"
    PROTOCOL = "protocol"
    GLOBAL = "global"

    PERM_ADD = "factoids.add.%s"
    PERM_SET = "factoids.set.%s"
    PERM_DEL = "factoids.delete.%s"
    PERM_GET = "factoids.get.%s"

    (RES_INVALID_LOCATION,
     RES_INVALID_METHOD,  # _FOR_LOCATION - i.e. CHANNEL in PM
     RES_NO_PERMS,
     RES_MISSING_FACTOID) = xrange(4)

    def setup(self):
        # ## Grab important shit
        self.commands = CommandManager()
        self.events = EventManager()
        self.storage = StorageManager()
        self.plugman = PluginManager()

        # ## Set up database
        self.database = self.storage.get_file(
            self,
            "data",
            DBAPI,
            "sqlite3:data/plugins/factoids.sqlite",
            "data/plugins/factoids.sqlite",
            check_same_thread=False
        )

        self.database.add_callback(self.reload)
        self.reload()

        # ## Register commands
        # We have multiple possible permissions per command, so we have to do
        # permission handling ourselves
        self.commands.register_command("addfactoid",
                                       self.factoid_add_command,
                                       self,
                                       None)
        self.commands.register_command("setfactoid",
                                       self.factoid_set_command,
                                       self,
                                       None)
        self.commands.register_command("deletefactoid",
                                       self.factoid_delete_command,
                                       self,
                                       None,
                                       ["delfactoid"])
        self.commands.register_command("getfactoid",
                                       self.factoid_get_command,
                                       self,
                                       None, default=True)

        # ## Register events
        self.events.add_callback("MessageReceived",
                                 self,
                                 self.message_handler,
                                 1)

        self.events.add_callback("Web/ServerStartedEvent",
                                 self,
                                 self.web_routes,
                                 1)

    def reload(self):
        with self.database as db:
            db.runQuery("CREATE TABLE IF NOT EXISTS factoids ("
                        "factoid_key TEXT, "
                        "location TEXT, "
                        "protocol TEXT, "
                        "channel TEXT, "
                        "factoid_name TEXT, "
                        "info TEXT, "
                        "UNIQUE(factoid_key, location, protocol, channel) "
                        "ON CONFLICT REPLACE)")

    # region Util functions

    def __check_perm(self, perm, caller, source, protocol):
        self.logger.trace(_("Checking for permission: '%s'"), perm)
        allowed = self.commands.perm_handler.check(perm,
                                                   caller,
                                                   source,
                                                   protocol)
        return allowed

    def _parse_args(self, raw_args):
        """
        Grabs the location, factoid name, and info from a raw_args string
        """
        pos = raw_args.find(" ")
        if pos < 0:
            raise ValueError(_("Invalid args"))
        location = raw_args[:pos]
        pos2 = raw_args.find(" ", pos + 1)
        if pos2 < 0:
            raise ValueError(_("Invalid args"))
        factoid = raw_args[pos + 1:pos2]
        # pos3 = raw_args.find(" ", pos2 + 1)
        info = raw_args[pos2 + 1:]
        if info == "":
            raise ValueError(_("Invalid args"))
        return location, factoid, info

    def valid_location(self, location, source=None):
        """
        Checks if a given location is one of channel, protocol or global, and
        if it's a channel request, that it's in a channel.
        """
        location = location.lower()
        result = location in (self.CHANNEL, self.PROTOCOL, self.GLOBAL)
        if not result:
            raise InvalidLocationError(_("'%s' is not a valid location") %
                                       location)
        if source is not None:
            if location == self.CHANNEL and not isinstance(source, Channel):
                raise InvalidMethodError(_("'channel' location can only be "
                                           "used inside a channel"))
        return True

    # endregion

    # region API functions to access factoids

    def _add_factoid_interaction(self, txn, factoid_key, location, protocol,
                                 channel, factoid, info):
        """
        Appends a factoid to an existing one if there, otherwise creates it.
        :return: True if already exists, otherwise False
        """
        txn.execute("SELECT * FROM factoids WHERE "
                    "factoid_key = ? AND location = ? AND "
                    "protocol = ? AND channel = ?",
                    (
                        to_unicode(factoid_key),
                        to_unicode(location),
                        to_unicode(protocol),
                        to_unicode(channel)
                    ))
        results = txn.fetchall()
        if len(results) == 0:
            # Factoid doesn't exist yet, create it
            txn.execute("INSERT INTO factoids VALUES(?, ?, ?, ?, ?, ?)",
                        (
                            to_unicode(factoid_key),
                            to_unicode(location),
                            to_unicode(protocol),
                            to_unicode(channel),
                            to_unicode(factoid),
                            to_unicode(info)
                        ))

            e = FactoidAddedEvent(self, factoid_key, factoid)
            self.events.run_callback("Factoids/Added", e, from_thread=True)
            return False
        else:
            # Factoid already exists, append
            txn.execute("INSERT INTO factoids VALUES(?, ?, ?, ?, ?, ?)",
                        (
                            to_unicode(results[0][0]),
                            to_unicode(results[0][1]),
                            to_unicode(results[0][2]),
                            to_unicode(results[0][3]),
                            to_unicode(results[0][4]),
                            results[0][5] + u"\n" + to_unicode(info)
                        ))
            e = FactoidUpdatedEvent(self, factoid_key, factoid)
            self.events.run_callback("Factoids/Updated", e, from_thread=True)
            return True

    def _delete_factoid_interaction(self, txn, factoid_key, location, protocol,
                                    channel):
        """
        Deletes a factoid if it exists, otherwise raises MissingFactoidError
        """

        self.logger.trace("DELETE | Key: %s | Loc: %s | Pro: %s | Cha: %s"
                          % (factoid_key, location, protocol, channel))

        if location == self.CHANNEL:
            txn.execute("DELETE FROM factoids WHERE factoid_key = ? AND "
                        "location = ? AND protocol = ? AND channel = ?",
                        (
                            to_unicode(factoid_key),
                            to_unicode(location),
                            to_unicode(protocol),
                            to_unicode(channel)
                        ))
        else:
            txn.execute("DELETE FROM factoids WHERE factoid_key = ? AND "
                        "location = ? AND protocol = ?",
                        (
                            to_unicode(factoid_key),
                            to_unicode(location),
                            to_unicode(protocol)
                        ))
        if txn.rowcount == 0:
            raise MissingFactoidError(_("Factoid '%s' does not exist") %
                                      factoid_key)

        e = FactoidDeletedEvent(self, factoid_key)
        self.events.run_callback("Factoids/Deleted", e, from_thread=True)

    def _get_factoid_interaction(self, txn, factoid_key, location, protocol,
                                 channel):
        """
        Gets a factoid if it exists, otherwise raises MissingFactoidError
        :return: (factoid_name, [entry, entry, ...])
        """
        self.logger.trace(_("Getting factoid params: factoid_key = '%s', "
                            "location = '%s', protocol = '%s', "
                            "channel = '%s'"),
                          factoid_key,
                          location,
                          protocol,
                          channel)
        if location is None:
            self.logger.trace(_("Location is None - getting all factoids with "
                                "key '%s'"), factoid_key)
            txn.execute("SELECT location, protocol, channel, factoid_name, "
                        "info FROM factoids WHERE factoid_key = ?",
                        (
                            to_unicode(factoid_key),
                        ))
            results = txn.fetchall()
            if len(results) > 0:
                # Check for channel match
                for row in results:
                    if ((row[0] == self.CHANNEL and row[1] == protocol and
                         row[2] == channel)):
                        self.logger.trace(_("Match found (channel)!"))
                        return (row[3], row[4].split("\n"))
                # Check for protocol match
                for row in results:
                    if row[0] == self.PROTOCOL and row[1] == protocol:
                        self.logger.trace(_("Match found (protocol)!"))
                        return (row[3], row[4].split("\n"))
                # Check for global match
                for row in results:
                    if row[0] == self.GLOBAL:
                        self.logger.trace(_("Match found (global)!"))
                        return (row[3], row[4].split("\n"))
        else:
            txn.execute("SELECT location, protocol, channel, factoid_name, "
                        "info FROM factoids WHERE factoid_key = ? AND "
                        "location = ? AND protocol = ? AND channel = ?",
                        (
                            to_unicode(factoid_key),
                            to_unicode(location),
                            to_unicode(protocol),
                            to_unicode(channel)
                        ))
            results = txn.fetchall()
            if len(results) > 0:
                return (results[0][3], results[0][4].split("\n"))
        raise MissingFactoidError(_("Factoid '%s' does not exist")
                                  % factoid_key)

    def _get_all_factoids_interaction(self, txn):
        """
        Gets all factoids
        :return: (factoid_name, [entry, entry, ...])
        """
        self.logger.trace("Getting all factoids.")
        txn.execute("SELECT location, protocol, channel, factoid_name, "
                    "info FROM factoids")
        results = txn.fetchall()
        return results

    def get_all_factoids(self):
        with self.database as db:
            return db.runInteraction(self._get_all_factoids_interaction)

    def add_factoid(self, caller, source, protocol, location, factoid, info):
        location = location.lower()
        factoid_key = factoid.lower()
        protocol_key = protocol.name.lower()
        channel_key = source.name.lower()
        try:
            location is None or self.valid_location(location, source)
        except Exception as ex:
            return defer.fail(ex)
        if not self.__check_perm(self.PERM_ADD % location,
                                 caller,
                                 source,
                                 protocol):
            return defer.fail(
                NoPermissionError(_("User does not have required permission"))
            )
        with self.database as db:
            return db.runInteraction(self._add_factoid_interaction,
                                     factoid_key,
                                     location,
                                     protocol_key,
                                     channel_key,
                                     factoid,
                                     info)

    def set_factoid(self, caller, source, protocol, location, factoid, info):
        location = location.lower()
        factoid_key = factoid.lower()
        protocol_key = protocol.name.lower()
        channel_key = source.name.lower()
        try:
            location is None or self.valid_location(location, source)
        except Exception as ex:
            return defer.fail(ex)
        if not self.__check_perm(self.PERM_SET % location,
                                 caller,
                                 source,
                                 protocol):
            return defer.fail(
                NoPermissionError(_("User does not have required permission"))
            )
        with self.database as db:
            return db.runQuery(
                "INSERT INTO factoids VALUES(?, ?, ?, ?, ?, ?)",
                (
                    to_unicode(factoid_key),
                    to_unicode(location),
                    to_unicode(protocol_key),
                    to_unicode(channel_key),
                    to_unicode(factoid),
                    to_unicode(info)
                ))

    def delete_factoid(self, caller, source, protocol, location, factoid):
        location = location.lower()
        factoid_key = factoid.lower()
        protocol_key = protocol.name.lower()
        channel_key = source.name.lower()
        try:
            location is None or self.valid_location(location, source)
        except Exception as ex:
            return defer.fail(ex)
        if not self.__check_perm(self.PERM_DEL % location,
                                 caller,
                                 source,
                                 protocol):
            return defer.fail(
                NoPermissionError(_("User does not have required permission"))
            )
        with self.database as db:
            return db.runInteraction(self._delete_factoid_interaction,
                                     factoid_key,
                                     location,
                                     protocol_key,
                                     channel_key)

    def get_factoid(self, caller, source, protocol, location, factoid):
        if location is not None:
            location = location.lower()
        factoid_key = factoid.lower()
        protocol_key = protocol.name.lower()
        channel_key = source.name.lower()
        try:
            location is None or self.valid_location(location, source)
        except Exception as ex:
            return defer.fail(ex)
        if not self.__check_perm(self.PERM_GET % location,
                                 caller,
                                 source,
                                 protocol):
            return defer.fail(
                NoPermissionError(_("User does not have required permission"))
            )
        with self.database as db:
            return db.runInteraction(self._get_factoid_interaction,
                                     factoid_key,
                                     location,
                                     protocol_key,
                                     channel_key)

    # endregion

    # region Command handlers for interacting with factoids

    def _factoid_command_fail(self, caller, failure):
        """
        :type failure: twisted.python.failure.Failure
        """
        if failure.check(InvalidLocationError):
            caller.respond(__("Invalid location given - possible locations "
                              "are: channel, protocol, global"))
        elif failure.check(InvalidMethodError):
            caller.respond(__("You must do that in a channel"))
        elif failure.check(NoPermissionError):
            caller.respond(__("You don't have permission to do that"))
        elif failure.check(MissingFactoidError):
            caller.respond(__("That factoid doesn't exist"))
        else:
            # TODO: We should probably handle this
            failure.raiseException()

    def _factoid_get_command_success(self, source, result, args=None):
        if not args:
            args = []

        for line in result[1]:
            # _tokens = tokens.find_tokens(line)
            _numerical = tokens.find_numerical_tokens(line)

            for i, arg in enumerate(args):
                line = line.replace("{%d}" % i, arg)

            for token in _numerical:
                line = line.replace(token, "")

            # TODO: Token handlers
            source.respond("(%s) %s" % (result[0], line))

    def factoid_add_command(self, protocol, caller, source, command, raw_args,
                            parsed_args):
        try:
            location, factoid, info = self._parse_args(raw_args)
        except Exception:
            caller.respond(__("Usage: %s <location> <factoid> <info>")
                           % command)
            return
        d = self.add_factoid(caller, source, protocol, location, factoid, info)
        d.addCallbacks(
            lambda r: caller.respond(__("Factoid added")),
            lambda f: self._factoid_command_fail(caller, f)
        )

    def factoid_set_command(self, protocol, caller, source, command, raw_args,
                            parsed_args):
        try:
            location, factoid, info = self._parse_args(raw_args)
        except Exception:
            caller.respond(__("Usage: %s <location> <factoid> <info>")
                           % command)
            return
        d = self.set_factoid(caller, source, protocol, location, factoid, info)
        d.addCallbacks(
            lambda r: caller.respond(__("Factoid set")),
            lambda f: self._factoid_command_fail(caller, f)
        )

    def factoid_delete_command(self, protocol, caller, source, command,
                               raw_args, parsed_args):
        args = raw_args.split()  # Quick fix for new command handler signature
        if len(args) != 2:
            caller.respond(__("Usage: %s <location> <factoid>")
                           % command)
            return
        location = args[0]
        factoid = args[1]
        d = self.delete_factoid(caller, source, protocol, location, factoid)
        d.addCallbacks(
            lambda r: caller.respond(__("Factoid deleted")),
            lambda f: self._factoid_command_fail(caller, f)
        )

    def factoid_get_command(self, protocol, caller, source, command, raw_args,
                            parsed_args):
        args = raw_args.split()  # Quick fix for new command handler signature
        if len(args) == 1:
            factoid = args[0]
            location = None
        elif len(args) == 2:
            location = args[0]
            factoid = args[1]
        else:
            caller.respond(__("Usage: %s [location] <factoid>")
                           % command)
            return

        d = self.get_factoid(caller, source, protocol, location, factoid)
        d.addCallbacks(
            lambda r: self._factoid_get_command_success(source, r),
            lambda f: self._factoid_command_fail(caller, f)
        )

    # endregion

    def _print_query(self, result):
        from pprint import pprint
        pprint(result)

    def web_routes(self, event=None):
        self.logger.info(_("Registering web route.."))

        #: :type: WebPlugin
        web = self.plugman.get_plugin("Web")

        if web is None:
            self.logger.debug("Web plugin not found.")
            return

        web.add_handler(r"/factoids", "plugins.factoids.route.Route")
        web.add_handler(r"/factoids/", "plugins.factoids.route.Route")

        web.add_navbar_entry("factoids", "/factoids", "text file outline")

    def message_handler(self, event):
        """
        Handle ??-style factoid "commands"
        :type event: MessageReceived
        """
        handlers = {
            "??": self._message_handler_get,
            "?<": self._message_handler_get_self,
            "??<": self._message_handler_get_self,
            "?>": self._message_handler_get_other,
            "??>": self._message_handler_get_other,
            "??+": self._message_handler_add,
            "??~": self._message_handler_set,
            "??-": self._message_handler_delete,
            "!?+": self._message_handler_add_global,
            "!?~": self._message_handler_set_global,
            "!?-": self._message_handler_delete_global,
            "@?+": self._message_handler_add_protocol,
            "@?~": self._message_handler_set_protocol,
            "@?-": self._message_handler_delete_protocol
        }
        msg = event.message
        command = None
        factoid = ""
        args = ""
        pos = msg.find(" ")
        split = msg.split(" ")
        if pos < 0:
            command = msg
        else:
            command = msg[:pos]
            pos2 = msg.find(" ", pos + 1)
            if pos2 < 0:
                factoid = msg[pos + 1:].strip()
            else:
                factoid = msg[pos + 1:pos2].strip()
                args = msg[pos2 + 1:].strip()
        if command in handlers:
            handlers[command](command, factoid, args, event, split)

    # ## Getting "commands"

    def _message_handler_get(self, command, factoid, args, event, split):
        """
        Handle ?? factoid "command"
        :type event: MessageReceived
        """
        if not factoid:
            event.source.respond(__("Usage: ?? <factoid>"))
            return
        d = self.get_factoid(event.source,
                             event.target,
                             event.caller,
                             None,
                             factoid)
        d.addCallbacks(
            lambda r: self._factoid_get_command_success(event.target, r,
                                                        split[2:]),
            lambda f: self._factoid_command_fail(event.source, f)
        )

    def _message_handler_get_self(self, command, factoid, args, event, split):
        """
        Handle ?< factoid "command"
        :type event: MessageReceived
        """
        if not factoid:
            event.source.respond(__("Usage: ?< <factoid>"))
            return
        d = self.get_factoid(event.source,
                             event.target,
                             event.caller,
                             None,
                             factoid)
        d.addCallbacks(
            lambda r: self._factoid_get_command_success(event.source, r,
                                                        split[2:]),
            lambda f: self._factoid_command_fail(event.source, f)
        )

    def _message_handler_get_other(self, command, factoid, args, event, split):
        """
        Handle ?> factoid "command"
        :type event: MessageReceived
        """
        if not len(split) > 2:
            event.source.respond(__("Usage: ?> <user> <factoid>"))
            return

        wanted = split[1]
        factoid = split[2]
        user = event.caller.get_user(wanted)

        if user is None:
            event.source.respond(__("Unable to find that user."))
            return

        d = self.get_factoid(event.source,
                             event.target,
                             event.caller,
                             None,
                             factoid)
        d.addCallbacks(
            lambda r: self._factoid_get_command_success(user, r, split[3:]),
            lambda f: self._factoid_command_fail(event.source, f)
        )

    # ## Channel "commands"

    def _message_handler_add(self, command, factoid, args, event, split):
        """
        Handle ??+ factoid "command"
        :type event: MessageReceived
        """
        if not factoid or not args:
            event.source.respond(__("Usage: ??+ <factoid> <info>"))
            return
        d = self.add_factoid(event.source,
                             event.target,
                             event.caller,
                             self.CHANNEL,
                             factoid,
                             args)
        d.addCallbacks(
            lambda r: event.source.respond(__("Factoid added")),
            lambda f: self._factoid_command_fail(event.source, f)
        )

    def _message_handler_set(self, command, factoid, args, event, split):
        """
        Handle ??~ factoid "command"
        :type event: MessageReceived
        """
        if not factoid or not args:
            event.source.respond(__("Usage: ??~ <factoid> <info>"))
            return
        d = self.set_factoid(event.source,
                             event.target,
                             event.caller,
                             self.CHANNEL,
                             factoid,
                             args)
        d.addCallbacks(
            lambda r: event.source.respond(__("Factoid set")),
            lambda f: self._factoid_command_fail(event.source, f)
        )

    def _message_handler_delete(self, command, factoid, args, event, split):
        """
        Handle ??- factoid "command"
        :type event: MessageReceived
        """
        if factoid is None:
            event.source.respond(__("Usage: ??- <factoid>"))
            return
        d = self.delete_factoid(event.source,
                                event.target,
                                event.caller,
                                self.CHANNEL,
                                factoid)
        d.addCallbacks(
            lambda r: event.source.respond(__("Factoid deleted")),
            lambda f: self._factoid_command_fail(event.source, f)
        )

    # ## Global "commands"

    def _message_handler_add_global(self, command, factoid, args, event,
                                    split):
        """
        Handle !?+ factoid "command"
        :type event: MessageReceived
        """
        if not factoid or not args:
            event.source.respond(__("Usage: !?+ <factoid> <info>"))
            return
        d = self.add_factoid(event.source,
                             event.target,
                             event.caller,
                             self.GLOBAL,
                             factoid,
                             args)
        d.addCallbacks(
            lambda r: event.source.respond(__("Factoid added")),
            lambda f: self._factoid_command_fail(event.source, f)
        )

    def _message_handler_set_global(self, command, factoid, args, event,
                                    split):
        """
        Handle !?~ factoid "command"
        :type event: MessageReceived
        """
        if not factoid or not args:
            event.source.respond(__("Usage: !?~ <factoid> <info>"))
            return
        d = self.set_factoid(event.source,
                             event.target,
                             event.caller,
                             self.GLOBAL,
                             factoid,
                             args)
        d.addCallbacks(
            lambda r: event.source.respond(__("Factoid set")),
            lambda f: self._factoid_command_fail(event.source, f)
        )

    def _message_handler_delete_global(self, command, factoid, args, event,
                                       split):
        """
        Handle !?- factoid "command"
        :type event: MessageReceived
        """
        if factoid is None:
            event.source.respond(__("Usage: !?- <factoid>"))
            return
        d = self.delete_factoid(event.source,
                                event.target,
                                event.caller,
                                self.GLOBAL,
                                factoid)
        d.addCallbacks(
            lambda r: event.source.respond(__("Factoid deleted")),
            lambda f: self._factoid_command_fail(event.source, f)
        )

    # ## Protocol-specific "commands"

    def _message_handler_add_protocol(self, command, factoid, args, event,
                                      split):
        """
        Handle @?+ factoid "command"
        :type event: MessageReceived
        """
        if not factoid or not args:
            event.source.respond(__("Usage: @?+ <factoid> <info>"))
            return
        d = self.add_factoid(event.source,
                             event.target,
                             event.caller,
                             self.PROTOCOL,
                             factoid,
                             args)
        d.addCallbacks(
            lambda r: event.source.respond(__("Factoid added")),
            lambda f: self._factoid_command_fail(event.source, f)
        )

    def _message_handler_set_protocol(self, command, factoid, args, event,
                                      split):
        """
        Handle @?~ factoid "command"
        :type event: MessageReceived
        """
        if not factoid or not args:
            event.source.respond(__("Usage: @?~ <factoid> <info>"))
            return
        d = self.set_factoid(event.source,
                             event.target,
                             event.caller,
                             self.PROTOCOL,
                             factoid,
                             args)
        d.addCallbacks(
            lambda r: event.source.respond(__("Factoid set")),
            lambda f: self._factoid_command_fail(event.source, f)
        )

    def _message_handler_delete_protocol(self, command, factoid, args, event,
                                         split):
        """
        Handle @?- factoid "command"
        :type event: MessageReceived
        """
        if factoid is None:
            event.source.respond(__("Usage: @?- <factoid>"))
            return
        d = self.delete_factoid(event.source,
                                event.target,
                                event.caller,
                                self.PROTOCOL,
                                factoid)
        d.addCallbacks(
            lambda r: event.source.respond(__("Factoid deleted")),
            lambda f: self._factoid_command_fail(event.source, f)
        )
예제 #6
0
class CommandManager(object):
    """This is the command manager. It's in charge of tracking commands that
    plugins wish to offer, and providing ways for plugins to offer methods
    of providing authentication and permissions.
    """

    __metaclass__ = Singleton

    #: Storage for all the registered commands. ::
    #:
    #:     commands = {
    #:         "command": {
    #:             "f": func(),
    #:             "permission": "plugin.command",
    #:             "owner": object
    #:         }
    #:     }
    commands = {}

    #: Storage for command aliases.
    #:
    #:     aliases = {
    #:         "alias": "command"
    #:     }
    aliases = {}

    @property
    @deprecated("Use the singular auth_handler instead")
    def auth_handlers(self):
        if self.auth_handler:
            return [self.auth_handler]
        return []

    #: Storage for all the registered auth handler.
    #:
    #: Auth handlers are in charge of asserting whether users are logged in
    #: or not, and identifying who they are logged in as.
    auth_handler = None

    #: Storage for the permissions handler. There may only ever be one of
    #: these.
    #:
    #: Permissions handlers are in charge of asserting whether a user has
    #: permission for a specified action. They work together with auth
    #: handlers to determine this.
    perm_handler = None

    #: Storage for the factory manager, to avoid function call overhead.
    factory_manager = None

    def __init__(self):
        self.logger = getLogger("Commands")
        self.event_manager = EventManager()

    def set_factory_manager(self, factory_manager):
        """Set the factory manager.

        This should only ever be called by the factory manager itself.

        :param factory_manager: The factory manager
        :type factory_manager: Manager
        """
        self.factory_manager = factory_manager

    def register_command(self,
                         command,
                         handler,
                         owner,
                         permission=None,
                         aliases=None,
                         default=False):
        """Register a command, provided it hasn't been registered already.

        The params should go like this.

        :param command: The command to register
        :param handler: The command handler
        :param owner: The plugin or object registering the command
        :param permission: The permission needed to run the command
        :param aliases: A list of aliases for the command being registered.
        :param default: Whether the command should be run when there is no
            permissions manager installed.

        :type command: str
        :type handler: function
        :type owner: PluginObject
        :type permission: str, None
        :type aliases: list, None
        :type default: bool

        :returns: Whether the command was registered or not
        :rtype: Boolean
        """

        if aliases is None:
            aliases = []

        if command in self.commands:
            self.logger.warn(
                _("Object '%s' tried to register command '%s' but"
                  "it's already been registered by object '%s'.") %
                (owner, command, self.commands[command]["owner"]))
            return False

        self.logger.debug(_("Registering command: %s (%s)") % (command, owner))
        commandobj = {
            "f": handler,
            "permission": permission,
            "owner": owner,
            "default": default
        }

        self.commands[command] = commandobj

        for alias in aliases:
            if alias in self.aliases:
                self.logger.warn(
                    _("Failed to register command alias '%s' as "
                      "it already belongs to another command.") % alias)
                continue

            self.logger.debug(
                _("Registering alias: %s -> %s (%s)") %
                (alias, command, owner))
            self.aliases[alias] = command

        return True

    def unregister_commands_for_owner(self, owner):
        """Unregister all commands that have been registered by a certain
        object.

        This method checks instances, not types!

        :param owner: The owner to check for
        :type owner: object
        """
        current = self.commands.items()
        for key, value in current:
            if owner is value["owner"]:
                del self.commands[key]
                self.logger.debug(_("Unregistered command: %s") % key)

                aliases = self.aliases.items()
                for k, v in aliases:
                    if v == key:
                        del self.aliases[k]
                        self.logger.debug(_("Unregistered alias: %s") % k)

    def process_input(self,
                      in_str,
                      caller,
                      source,
                      protocol,
                      control_char=None,
                      our_name=None):
        """Process a set of inputs, to check if there's a command there and
        action it.

        This is designed to be used from a protocol.

        :param in_str: The entire message to parse
        :param caller: The User that sent the message
        :param source: The User or Channel that the message was sent to
        :param protocol: The Protocol object the User belongs to
        :param control_char: The control characters (prefix)
        :param our_name: The name of the bot on Protocol

        :type in_str: str
        :type caller: User
        :type source: User, Channel
        :type protocol: Protocol
        :type control_char: str
        :type our_name: str

        :return: Tuple containing CommandState representing the state of
            the command, and either None or an Exception.
        :rtype: tuple(CommandState, None or Exception)
        """

        if control_char is None:
            if hasattr(protocol, "control_chars"):
                control_char = protocol.control_chars
            else:
                self.logger.debug("Protocol %s doesn't have a control "
                                  "character sequence!" % protocol.name)
                return CommandState.Error, NoControlCharacterException(
                    "Protocol %s doesn't have a control character sequence." %
                    protocol.name)

        if our_name is None:
            if hasattr(protocol, "nickname"):
                our_name = protocol.nickname

        if our_name is not None:
            control_char = control_char.replace("{NAME}", our_name)
            control_char = control_char.replace("{NICK}", our_name)

        if len(in_str) < len(control_char):
            self.logger.trace("Control character sequence is longer than the "
                              "input string, so this cannot be a command.")
            return CommandState.NotACommand, None

        if in_str.lower().startswith(control_char.lower()):  # It's a command!
            # Remove the command char(s) from the start
            replaced = in_str[len(control_char):]

            split = replaced.split(None, 1)
            if not split:
                return False
            command = split[0]
            args = ""
            if len(split) > 1:
                args = split[1]

            printable = "<%s:%s> %s" % (caller, source, in_str)

            event = events.PreCommand(protocol, command, args, caller, source,
                                      printable, in_str)
            self.event_manager.run_callback("PreCommand", event)

            if event.printable:
                self.logger.info("%s | %s" % (protocol.name, event.printable))

            result = self.run_command(event.command, event.source,
                                      event.target, protocol, event.args)

            return result

        self.logger.debug("Command not found.")
        return CommandState.NotACommand, None

    def run_command(self, command, caller, source, protocol, args):
        """Run a command, provided it's been registered.

        :param command: The command, a string
        :param caller: Who ran the command
        :param source: Where they ran the command
        :param protocol: The protocol they're part of
        :param args: A list of arguments for the command

        :type command: str
        :type caller: User
        :type source: User
        :type protocol: Protocol
        :type args: list

        :return: Tuple containing CommandState representing the state of
            the command, and either None or an Exception.
        :rtype: tuple(CommandState, None or Exception)
        """

        if command not in self.commands:
            if command not in self.aliases:  # Get alias, if it exists
                event = events.UnknownCommand(self, protocol, command, args,
                                              caller, source)

                self.event_manager.run_callback("UnknownCommand", event)

                if event.cancelled:
                    return CommandState.UnknownOverridden, None

                return CommandState.Unknown, None
            command = self.aliases[command]
        # Parse args
        raw_args = args
        try:
            lex = shlex.shlex(args, posix=True)
            lex.whitespace_split = True
            lex.quotes = '"'
            lex.commenters = ""
            parsed_args = list(lex)
        except ValueError:
            parsed_args = None
        try:
            if self.commands[command]["permission"]:
                if not self.perm_handler:
                    if not self.commands[command]["default"]:
                        return CommandState.NoPermission, None

                    try:
                        self.commands[command]["f"](protocol, caller, source,
                                                    command, raw_args,
                                                    parsed_args)
                    except RateLimitExceededError:
                        # TODO: Proper decorator
                        return CommandState.RateLimited, None
                    except Exception as e:
                        self.logger.exception("Error running command")
                        return CommandState.Error, e
                else:
                    if self.perm_handler.check(
                            self.commands[command]["permission"], caller,
                            source, protocol):
                        try:
                            self.commands[command]["f"](protocol, caller,
                                                        source, command,
                                                        raw_args, parsed_args)
                        except RateLimitExceededError:
                            # TODO: Proper decorator
                            return CommandState.RateLimited, None
                        except Exception as e:
                            self.logger.exception("Error running command")
                            return CommandState.Error, e
                    else:
                        return CommandState.NoPermission, None
            else:
                self.commands[command]["f"](protocol, caller, source, command,
                                            raw_args, parsed_args)
        except RateLimitExceededError:
            # TODO: Proper decorator
            return CommandState.RateLimited, None
        except Exception as e:
            self.logger.exception("Error running command")
            return CommandState.Error, e
        else:
            return CommandState.Success, None

    @deprecated("Use set_auth_handler instead")
    def add_auth_handler(self, handler):
        return self.set_auth_handler(handler)

    def set_auth_handler(self, handler):
        """Add an auth handler, provided it hasn't already been added.

        :param handler: The handler to add
        :type handler: object

        :returns: Whether the handler was added or not
        :rtype: Boolean
        """
        if self.auth_handler is None:
            self.auth_handler = handler
            return True

        return False

    def set_permissions_handler(self, handler):
        """Set the permissions handler, provided one hasn't already been set.

        :param handler: The handler to set
        :type handler: object

        :returns: Whether the handler was set or not
        :rtype: Boolean
        """
        if self.perm_handler:
            self.logger.warn(
                _("Two plugins are trying to provide permissions "
                  "handlers. Only the first will be used!"))
            return False
        self.perm_handler = handler
        return True
예제 #7
0
class Protocol(SingleChannelProtocol):

    TYPE = "mumble"

    VERSION_MAJOR = 1
    VERSION_MINOR = 2
    VERSION_PATCH = 4

    VERSION_DATA = (VERSION_MAJOR << 16)\
        | (VERSION_MINOR << 8) \
        | VERSION_PATCH

    # From the Mumble protocol documentation
    PREFIX_FORMAT = ">HI"
    PREFIX_LENGTH = 6

    # This specific order of IDs is extracted from
    # https://github.com/mumble-voip/mumble/blob/master/src/Message.h
    ID_MESSAGE = [
        Mumble_pb2.Version,
        Mumble_pb2.UDPTunnel,
        Mumble_pb2.Authenticate,
        Mumble_pb2.Ping,
        Mumble_pb2.Reject,
        Mumble_pb2.ServerSync,
        Mumble_pb2.ChannelRemove,
        Mumble_pb2.ChannelState,
        Mumble_pb2.UserRemove,
        Mumble_pb2.UserState,
        Mumble_pb2.BanList,
        Mumble_pb2.TextMessage,
        Mumble_pb2.PermissionDenied,
        Mumble_pb2.ACL,
        Mumble_pb2.QueryUsers,
        Mumble_pb2.CryptSetup,
        Mumble_pb2.ContextActionModify,
        Mumble_pb2.ContextAction,
        Mumble_pb2.UserList,
        Mumble_pb2.VoiceTarget,
        Mumble_pb2.PermissionQuery,
        Mumble_pb2.CodecVersion,
        Mumble_pb2.UserStats,
        Mumble_pb2.RequestBlob,
        Mumble_pb2.ServerConfig
    ]

    # Reversing the IDs, so we are able to backreference.
    MESSAGE_ID = dict([(v, k) for k, v in enumerate(ID_MESSAGE)])

    PING_REPEAT_TIME = 5

    channels = {}
    users = {}
    _acls = {}

    @property
    def num_channels(self):
        return len(self.channels)

    control_chars = "."

    pinging = True

    ourselves = None

    def __init__(self, name, factory, config):
        self.name = name
        self.factory = factory
        self.config = config

        self.received = ""
        self.log = getLogger(self.name)
        self.log.info("Setting up..")

        self.command_manager = CommandManager()
        self.event_manager = EventManager()

        self.username = config["identity"]["username"]
        self.password = config["identity"]["password"]
        self.networking = config["network"]
        self.tokens = config["identity"]["tokens"]

        self.control_chars = config["control_chars"]

        audio_conf = config.get("audio", {})
        self.should_mute_self = audio_conf.get("should_mute_self", True)
        self.should_deafen_self = audio_conf.get("should_deafen_self", True)

        event = general_events.PreConnectEvent(self, config)
        self.event_manager.run_callback("PreConnect", event)

        context = self._get_client_context()
        if context is None:
            # Could not create a context (problem loading cert file)
            self.factory.manager.remove_protocol(self.name)
            return

        reactor.connectSSL(
            self.networking["address"],
            self.networking["port"],
            self.factory,
            context,
            120
        )

        event = general_events.PostConnectEvent(self, config)
        self.event_manager.run_callback("PostConnect", event)

    def _get_client_context(self):
        # Check if a cert file is specified in config
        if ("certificate" in self.config["identity"] and
                self.config["identity"]["certificate"]):
            # Attempt to load it
            try:
                self.log.debug(_("Attempting to load certificate file"))
                from OpenSSL import crypto, SSL
                cert_file = self.config["identity"]["certificate"]
                # Check if cert file exists, and if not, create it
                if not os.path.exists(cert_file):
                    self.log.info(_("Certificate file does not exist - "
                                    "generating..."))
                    # Creates a key similarly to the official mumble client
                    pkey = crypto.PKey()
                    pkey.generate_key(crypto.TYPE_RSA, 2048)
                    cert = crypto.X509()
                    cert.set_version(2)
                    cert.set_serial_number(1000)
                    cert.gmtime_adj_notBefore(0)
                    cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 20)
                    cert.set_pubkey(pkey)
                    cert.get_subject().CN = self.username
                    cert.set_issuer(cert.get_subject())
                    cert.add_extensions([
                        crypto.X509Extension("basicConstraints", True,
                                             "CA:FALSE"),
                        crypto.X509Extension("extendedKeyUsage", False,
                                             "clientAuth"),
                        # The official Mumble client does this, but it errors
                        # here, and I'm not sure it's required for certs where
                        # CA is FALSE (RFC 3280, 4.2.1.2)
                        # crypto.X509Extension("subjectKeyIdentifier", False,
                        #                      "hash"),
                        crypto.X509Extension("nsComment", False,
                                             "Generated by Ultros"),
                    ])
                    cert.sign(pkey, "sha1")
                    p12 = crypto.PKCS12()
                    p12.set_privatekey(pkey)
                    p12.set_certificate(cert)
                    cert_file_dir = os.path.dirname(cert_file)
                    if not os.path.exists(cert_file_dir):
                        self.log.debug("Creating directories for cert file")
                        os.makedirs(cert_file_dir)
                    with open(cert_file, "wb") as cert_file_handle:
                        cert_file_handle.write(p12.export())

                # Load the cert file
                with open(cert_file, "rb") as cert_file_handle:
                    certificate = crypto.load_pkcs12(cert_file_handle.read())

                # Context factory class, using the loaded cert
                class CtxFactory(ssl.ClientContextFactory):
                    def getContext(self):
                        self.method = SSL.SSLv23_METHOD
                        ctx = ssl.ClientContextFactory.getContext(self)
                        ctx.use_certificate(certificate.get_certificate())
                        ctx.use_privatekey(certificate.get_privatekey())
                        return ctx

                self.log.info(_("Loaded specified certificate file"))
                return CtxFactory()
            except ImportError:
                self.log.error(_("Could not import OpenSSL - cannot connect "
                                 "with certificate file"))
            except IOError:
                self.log.error(_("Could not load cert file"))
                self.log.debug("Exception info:", exc_info=1)
            except Exception:
                self.log.exception(_("Unknown error while loading certificate "
                                     "file"))
            return None
        else:
            # Default CtxFactory for no certificate
            self.log.info(_("No certificate specified - connecting without "
                            "certificate"))
            return ssl.ClientContextFactory()

    def shutdown(self):
        self.msg(_("Disconnecting: Protocol shutdown"))
        self.transport.loseConnection()

    def connectionMade(self):
        self.log.info(_("Connected to server."))

        # In the mumble protocol you must first send your current version
        # and immediately after that the authentication data.
        #
        # The mumble server will respond with a version message right after
        # this one.
        version = Mumble_pb2.Version()

        version.version = Protocol.VERSION_DATA
        version.release = "%d.%d.%d" % (Protocol.VERSION_MAJOR,
                                        Protocol.VERSION_MINOR,
                                        Protocol.VERSION_PATCH)
        version.os = platform.system()
        version.os_version = "Mumble %s Twisted Protocol" % version.release

        # Here we authenticate
        auth = Mumble_pb2.Authenticate()
        auth.username = self.username
        if self.password:
            auth.password = self.password
        for token in self.tokens:
            auth.tokens.append(token)

        # And now we send both packets one after another
        self.sendProtobuf(version)
        self.sendProtobuf(auth)

        event = general_events.PreSetupEvent(self, self.config)
        self.event_manager.run_callback("PreSetup", event)

        # Then we initialize our ping handler
        self.init_ping()

        # Mute/deafen ourselves if wanted (saves processing UDP packets if not
        # needed)
        message = Mumble_pb2.UserState()
        message.self_mute = self.should_mute_self
        message.self_deaf = self.should_deafen_self

        self.sendProtobuf(message)

        self.factory.clientConnected()

    def dataReceived(self, recv):
        # Append our received data
        self.received = self.received + recv

        # If we have enough bytes to read the header, we do that
        while len(self.received) >= Protocol.PREFIX_LENGTH:
            msg_type, length = \
                struct.unpack(Protocol.PREFIX_FORMAT,
                              self.received[:Protocol.PREFIX_LENGTH])

            full_length = Protocol.PREFIX_LENGTH + length

            self.log.trace("Length: %d" % length)
            self.log.trace("Message type: %d" % msg_type)

            # Check if this this a valid message ID
            if msg_type not in Protocol.MESSAGE_ID.values():
                self.log.error(_("Message ID not available."))
                self.transport.loseConnection()
                return

            # We need to check if we have enough bytes to fully read the
            # message
            if len(self.received) < full_length:
                self.log.trace(_("Need to fill data"))
                return

            # Read and handle the specific message
            if msg_type == 1:
                # Non-Protobuf messages
                # 1 is taken from the position of UDPTunnel in ID_MESSAGE
                self.recv_UDP(self.received[Protocol.PREFIX_LENGTH:
                                            Protocol.PREFIX_LENGTH + length])
            else:
                # Regular (Protobuf) messages
                msg = Protocol.ID_MESSAGE[msg_type]()
                msg.ParseFromString(
                    self.received[Protocol.PREFIX_LENGTH:
                                  Protocol.PREFIX_LENGTH + length])

                # Handle the message
                try:
                    self.recvProtobuf(msg_type, msg)
                except Exception:
                    self.log.exception(_("Exception while handling data."))

            self.received = self.received[full_length:]

    def sendProtobuf(self, message):
        # We find the message ID
        msg_type = Protocol.MESSAGE_ID[message.__class__]
        # Serialize the message
        msg_data = message.SerializeToString()
        length = len(msg_data)

        # Compile the data with the header
        data = struct.pack(Protocol.PREFIX_FORMAT, msg_type, length) + msg_data

        # Send the data
        self.transport.write(data)

    def recvProtobuf(self, msg_type, message):
        if isinstance(message, Mumble_pb2.Version):
            # version, release, os, os_version
            self.log.info(_("Connected to Murmur v%s") % message.release)
            event = general_events.PostSetupEvent(self, self.config)
            self.event_manager.run_callback("PostSetup", event)
        elif isinstance(message, Mumble_pb2.Reject):
            # version, release, os, os_version
            self.log.info(_("Could not connect to server: %s - %s") %
                          (message.type, message.reason))

            self.transport.loseConnection()
            self.pinging = False
        elif isinstance(message, Mumble_pb2.CodecVersion):
            # alpha, beta, prefer_alpha, opus
            alpha = message.alpha
            beta = message.beta
            prefer_alpha = message.prefer_alpha
            opus = message.opus

            event = mumble_events.CodecVersion(self, alpha, beta, prefer_alpha,
                                               opus)
            self.event_manager.run_callback("Mumble/CodecVersion", event)
        elif isinstance(message, Mumble_pb2.CryptSetup):
            # key, client_nonce, server_nonce
            key = message.key
            c_n = message.client_nonce
            s_n = message.server_nonce

            event = mumble_events.CryptoSetup(self, key, c_n, s_n)
            self.event_manager.run_callback("Mumble/CryptoSetup", event)
        elif isinstance(message, Mumble_pb2.ChannelState):
            # channel_id, name, position, [parent]
            self.handle_msg_channelstate(message)
        elif isinstance(message, Mumble_pb2.PermissionQuery):
            # channel_id, permissions, flush
            channel = self.channels[message.channel_id]
            permissions = message.permissions
            flush = message.flush
            self.set_permissions(channel, permissions, flush)
            self.log.trace("PermissionQuery received: channel: '%s', "
                           "permissions: '%s', flush:'%s'" %
                           (channel,
                            Perms.get_permissions_names(permissions),
                            flush))
            event = mumble_events.PermissionsQuery(self, channel, permissions,
                                                   flush)
            self.event_manager.run_callback("Mumble/PermissionsQuery", event)
        elif isinstance(message, Mumble_pb2.UserState):
            # session, name,
            # [user_id, suppress, hash, actor, self_mute, self_deaf]
            self.handle_msg_userstate(message)
        elif isinstance(message, Mumble_pb2.ServerSync):
            # session, max_bandwidth, welcome_text, permissions
            session = message.session
            # TODO: Store this?
            max_bandwidth = message.max_bandwidth
            permissions = message.permissions
            # TODO: Check this permissions relevancy - root chan? We don't know
            # what channel we're in yet, so it must be
            self.set_permissions(0, permissions)
            welcome_text = html_to_text(message.welcome_text, True)
            self.log.info(_("===   Welcome message   ==="))
            self.log.trace("ServerSync received: max_bandwidth: '%s', "
                           "permissions: '%s', welcome text: [below]" %
                           (max_bandwidth,
                            Perms.get_permissions_names(permissions)))
            for line in welcome_text.split("\n"):
                self.log.info(line)
            self.log.info(_("=== End welcome message ==="))

            event = mumble_events.ServerSync(self, session, max_bandwidth,
                                             welcome_text, permissions)
            self.event_manager.run_callback("Mumble/ServerSync", event)
        elif isinstance(message, Mumble_pb2.ServerConfig):
            # max_bandwidth, welcome_text, allow_html, message_length,
            # image_message_length
            # TODO: Store these
            max_bandwidth = message.max_bandwidth
            welcome_text = message.welcome_text
            self.allow_html = message.allow_html
            message_length = message.message_length
            image_message_length = message.image_message_length

            event = mumble_events.ServerConfig(self, max_bandwidth,
                                               welcome_text, self.allow_html,
                                               message_length,
                                               image_message_length)
            self.event_manager.run_callback("Mumble/ServerConfig", event)
        elif isinstance(message, Mumble_pb2.Ping):
            # timestamp, good, late, lost, resync, udp_packets, tcp_packets,
            # udp_ping_avg, udp_ping_var, tcp_ping_avg, tcp_ping_var
            timestamp = message.timestamp
            good = message.good
            late = message.late
            lost = message.lost
            resync = message.resync
            udp = message.udp_packets
            tcp = message.tcp_packets
            udp_a = message.udp_ping_avg
            udp_v = message.udp_ping_var
            tcp_a = message.tcp_ping_avg
            tcp_v = message.tcp_ping_var

            event = mumble_events.Ping(self, timestamp, good, late, lost,
                                       resync, tcp, udp, tcp_a, udp_a, tcp_v,
                                       udp_v)

            self.event_manager.run_callback("Mumble/Ping", event)
        elif isinstance(message, Mumble_pb2.UserRemove):
            # session, actor, reason, ban
            session = message.session
            actor = message.actor
            reason = message.reason
            ban = message.ban

            if message.session in self.users:
                user = self.users[message.session]
                user.is_tracked = False
                self.log.info(_("User left: %s") %
                              user)
                user.channel.remove_user(user)
                del self.users[message.session]
            else:
                user = None

            if actor in self.users:
                event = mumble_events.UserRemove(self, session, actor, user,
                                                 reason, ban,
                                                 self.users[actor])
                self.event_manager.run_callback("Mumble/UserRemove", event)

            s_event = general_events.UserDisconnected(self, user)
            self.event_manager.run_callback("UserDisconnected", s_event)
        elif isinstance(message, Mumble_pb2.TextMessage):
            # actor, channel_id, message
            self.handle_msg_textmessage(message)
        else:
            self.log.trace(_("Unknown message type: %s") % message.__class__)
            self.log.trace(_("Received message '%s' (%d):\n%s")
                           % (message.__class__, msg_type, str(message)))

            event = mumble_events.Unknown(self, type(message), message)
            self.event_manager.run_callback("Mumble/Unknown", event)

    def recv_UDP(self, data):
        """
        Handle a UDP message (whether it be from actual UDP or via TCP tunnel)
        :param data: UDP
        """
        # TODO: Use UDP rather than TCP tunnel (see protocol docs section 5)
        # We don't actually need to parse this atm
        return
        _first_byte = ord(data[0])
        msg_type = (_first_byte & 0xE0) >> 5
        target = _first_byte & 0x1F
        pos = 1
        session, pos = decode_varint(data, pos)
        sequence, pos = decode_varint(data, pos)
        self.log.trace(
            "UDP Message: Type=%s, target=%s, session=%s, sequence=%s",
            msg_type,
            target,
            self.get_user(session),
            sequence
        )
        # TEMP: Forward the packet back to the server

        msg_data = data

        length = len(msg_data)

        # Compile the data with the header
        data = struct.pack(Protocol.PREFIX_FORMAT, 1, length) + msg_data

        # Send the data
        # self.transport.write(data)

    def init_ping(self):
        # Call ping every PING_REPEAT_TIME seconds.
        reactor.callLater(Protocol.PING_REPEAT_TIME, self.ping_handler)

    def ping_handler(self):
        if not self.pinging:
            return
        self.log.trace("Sending ping")

        # Ping has only optional data, no required
        ping = Mumble_pb2.Ping()
        self.sendProtobuf(ping)

        self.init_ping()

    def handle_msg_channelstate(self, message):
        if message.channel_id not in self.channels:
            parent = None
            if message.HasField('parent'):
                parent = message.parent
            links = []
            if message.links:
                links = list(message.links)
                for link in links:
                    self.log.debug(_("Channel link: %s to %s") %
                                   (self.channels[link],
                                    self.channels[message.channel_id]))
            self.channels[message.channel_id] = Channel(self,
                                                        message.channel_id,
                                                        message.name,
                                                        parent,
                                                        message.position,
                                                        links)
            self.log.info(_("New channel: %s") % message.name)
        if message.links_add:
            for link in message.links_add:
                self.channels[message.channel_id].add_link(link)
                self.log.info(_("Channel link added: %s to %s") %
                              (self.channels[link],
                               self.channels[message.channel_id]))

                # TOTALLY MORE READABLE
                # GOOD JOB PEP8
                event = mumble_events.ChannelLinked(self, self.channels[link],
                                                    self.channels
                                                    [message.channel_id])
                self.event_manager.run_callback("Mumble/ChannelLinked", event)
        if message.links_remove:
            for link in message.links_remove:
                self.channels[message.channel_id].remove_link(link)
                self.log.info(_("Channel link removed: %s from %s") %
                              (self.channels[link],
                               self.channels[message.channel_id]))

                # Jesus f**k.
                event = mumble_events.ChannelUnlinked(self, self.channels
                                                      [link], self.channels
                                                      [message.channel_id])
                self.event_manager.run_callback("Mumble/ChannelUnlinked",
                                                event)

    def handle_msg_userstate(self, message):
        if message.name and message.session not in self.users:
            # Note: I'm not sure if message.name should ever be empty and
            # not in self.users - rakiru
            self.users[message.session] = User(self,
                                               message.session,
                                               message.name,
                                               self.channels[
                                                   message.channel_id],
                                               message.mute,
                                               message.deaf,
                                               message.suppress,
                                               message.self_mute,
                                               message.self_deaf,
                                               message.priority_speaker,
                                               message.recording)
            self.log.info(_("User joined: %s") % message.name)
            # We can't just flow into the next section to deal with this, as
            # that would count as a channel change, and it doesn't always work
            # as expected anyway.
            self.channels[message.channel_id].add_user(
                self.users[message.session])
            # Store our User object
            if message.name == self.username:
                self.ourselves = self.users[message.session]
                # User connection messages come after all channels have been
                # given, so now is a safe time to attempt to join a channel.
                try:
                    conf = self.config["channel"]
                    if "id" in conf and conf["id"]:
                        if conf["id"] in self.channels:
                            self.join_channel(self.channels[conf["id"]])
                        else:
                            self.log.warning(_("No channel with id '%s'") %
                                             conf["id"])
                    elif "name" in conf and conf["name"]:
                        chan = self.get_channel(conf["name"])
                        if chan is not None:
                            self.join_channel(chan)
                        else:
                            self.log.warning(_("No channel with name '%s'") %
                                             conf["name"])
                    else:
                        self.log.warning(_("No channel found in config"))
                except Exception:
                    self.log.warning(_("Config is missing 'channel' section"))
            else:
                event = mumble_events.UserJoined(self,
                                                 self.users[message.session])
                self.event_manager.run_callback("Mumble/UserJoined", event)
        else:
            # Note: More than one state change can happen at once
            user = self.users[message.session]
            if message.HasField('channel_id'):
                actor = self.users[message.actor]
                self.log.info(_("User moved channel: %s from %s to %s by %s") %
                              (user,
                               user.channel,
                               self.channels[message.channel_id],
                               actor))
                old = self.channels[user.channel.channel_id]
                user.channel.remove_user(user)
                self.channels[message.channel_id].add_user(user)
                user.channel = self.channels[message.channel_id]

                event = mumble_events.UserMoved(self, user, user.channel, old)
                self.event_manager.run_callback("Mumble/UserMoved", event)
            if message.HasField('mute'):
                actor = self.users[message.actor]
                if message.mute:
                    self.log.info(_("User was muted: %s by %s")
                                  % (user, actor))
                else:
                    self.log.info(_("User was unmuted: %s by %s")
                                  % (user, actor))
                user.mute = message.mute

                event = mumble_events.UserMuteToggle(self, user, user.mute,
                                                     actor)
                self.event_manager.run_callback("Mumble/UserMuteToggle", event)
            if message.HasField('deaf'):
                actor = self.users[message.actor]
                if message.deaf:
                    self.log.info(_("User was deafened: %s by %s") % (user,
                                                                      actor))
                else:
                    self.log.info(_("User was undeafened: %s by %s") % (user,
                                                                        actor))
                user.deaf = message.deaf

                event = mumble_events.UserDeafToggle(self, user, user.deaf,
                                                     actor)
                self.event_manager.run_callback("Mumble/UserDeafToggle", event)
            if message.HasField('suppress'):
                if message.suppress:
                    self.log.info(_("User was suppressed: %s") % user)
                else:
                    self.log.info(_("User was unsuppressed: %s") % user)
                user.suppress = message.suppress

                event = mumble_events.UserSuppressionToggle(self, user,
                                                            user.suppress)
                self.event_manager.run_callback("Mumble/UserSuppressionToggle",
                                                event)
            if message.HasField('self_mute'):
                if message.self_mute:
                    self.log.info(_("User muted themselves: %s") % user)
                else:
                    self.log.info(_("User unmuted themselves: %s") % user)
                user.self_mute = message.self_mute

                event = mumble_events.UserSelfMuteToggle(self, user,
                                                         user.self_mute)
                self.event_manager.run_callback("Mumble/UserSelfMuteToggle",
                                                event)
            if message.HasField('self_deaf'):
                if message.self_deaf:
                    self.log.info(_("User deafened themselves: %s") % user)
                else:
                    self.log.info(_("User undeafened themselves: %s") % user)
                user.self_deaf = message.self_deaf

                event = mumble_events.UserSelfDeafToggle(self, user,
                                                         user.self_deaf)
                self.event_manager.run_callback("Mumble/UserSelfDeafToggle",
                                                event)
            if message.HasField('priority_speaker'):
                actor = self.users[message.actor]
                if message.priority_speaker:
                    self.log.info(_("User was given priority speaker: %s by "
                                    "%s")
                                  % (user, actor))
                else:
                    self.log.info(_("User was revoked priority speaker: %s by "
                                    "%s")
                                  % (user, actor))
                state = user.priority_speaker = message.priority_speaker

                event = mumble_events.UserPrioritySpeakerToggle(self, user,
                                                                state, actor)
                self.event_manager.run_callback("Mumble/UserPrioritySpeaker" +
                                                "Toggle", event)
            if message.HasField('recording'):
                if message.recording:
                    self.log.info(_("User started recording: %s") % user)
                else:
                    self.log.info(_("User stopped recording: %s") % user)
                user.recording = message.recording

                event = mumble_events.UserRecordingToggle(self, user,
                                                          user.recording)
                self.event_manager.run_callback("Mumble/UserRecordingToggle",
                                                event)

    def handle_msg_textmessage(self, message):
        if message.actor in self.users:
            user_obj = self.users[message.actor]
            # TODO: Replace this with proper formatting stuff when implemented
            # Perhaps a new command-handler/event parameter for raw and parsed
            msg = html_to_text(message.message, True)

            if message.channel_id:
                cid = message.channel_id[0]
                channel_obj = self.channels[cid]
            else:
                # Private message - set the channel_obj (source) to user who
                # sent the message, as is done with IRC (otherwise it would be
                # None).
                channel_obj = user_obj

            event = general_events.PreMessageReceived(
                self, user_obj, channel_obj, msg, "message"
            )
            self.event_manager.run_callback("PreMessageReceived", event)
            if event.printable:
                for line in event.message.split("\n"):
                    self.log.info("<%s> %s" % (user_obj, line))

            if not event.cancelled:
                result = self.command_manager.process_input(
                    event.message, user_obj, channel_obj, self,
                    self.control_chars, self.nickname
                )

                for case, default in Switch(result[0]):
                    if case(CommandState.RateLimited):
                        self.log.debug("Command rate-limited")
                        user_obj.respond("That command has been rate-limited, "
                                         "please try again later.")
                        return  # It was a command
                    if case(CommandState.NotACommand):
                        self.log.debug("Not a command")
                        break
                    if case(CommandState.UnknownOverridden):
                        self.log.debug("Unknown command overridden")
                        return  # It was a command
                    if case(CommandState.Unknown):
                        self.log.debug("Unknown command")
                        break
                    if case(CommandState.Success):
                        self.log.debug("Command ran successfully")
                        return  # It was a command
                    if case(CommandState.NoPermission):
                        self.log.debug("No permission to run command")
                        return  # It was a command
                    if case(CommandState.Error):
                        user_obj.respond("Error running command: %s"
                                         % result[1])
                        return  # It was a command
                    if default:
                        self.log.debug("Unknown command state: %s" % result[0])
                        break

                second_event = general_events.MessageReceived(
                    self, user_obj, channel_obj, msg, "message"
                )

                self.event_manager.run_callback(
                    "MessageReceived", second_event
                )

            # TODO: Remove this before proper release. An admin plugin with the
            #       same functionality should be created.
            # if msg.startswith('!'):
            #     cmd = msg[1:].lower().split(" ")[0]
            #     if cmd == "users":
            #         self.print_users()
            #     elif cmd == "channels":
            #         self.print_channels()
            #     elif cmd == "msgme":
            #         self.msg_user("msg_user() test using id", message.actor)
            #         self.msg_user("msg_user() test using User object",
            #                       self.users[message.actor])
            #     elif cmd == "join":
            #         channame = msg[6:]
            #         chan = None
            #         for _id, channel in self.channels.iteritems():
            #             if channel.name.lower() == channame.lower():
            #                 chan = _id
            #                 break
            #         if chan is None:
            #             self.msg_user("Could not find channel",
            #                           message.actor)
            # NOTE: The weird indent is because of the stupid line length limit
            #         else:
            #             self.msg_user("Joining channel", message.actor)
            #             self.join_channel(chan)

    def send_msg(self, target, message, target_type=None, use_event=True):
        if isinstance(target, int) or isinstance(target, str):
            if target_type == "user":
                target = self.get_user(target)
                if not target:
                    return False
            else:  # Prioritize channels
                target = self.get_channel(target)
                if not target:
                    return False

        if target is None:
            target = self.get_channel()

        if isinstance(target, User):
            self.msg_user(message, target, use_event)
            return True
        elif isinstance(target, Channel):
            self.msg_channel(message, target, use_event)
            return True

        return False

    def send_action(self, target, message, target_type=None, use_event=True):
        if isinstance(target, int) or isinstance(target, str):
            if target_type == "user":
                target = self.get_user(target)
                if not target:
                    return False
            else:  # Prioritize channels
                target = self.get_channel(target)
                if not target:
                    return False

        if target is None:
            target = self.get_channel()

        # TODO: Add italics once formatter is added

        message = u"*%s*" % message
        event = general_events.ActionSent(self, target, message)

        self.event_manager.run_callback("ActionSent", event)

        if isinstance(target, User) and not event.cancelled:
            self.msg_user(message, target, use_event)
            return True
        elif isinstance(target, Channel) and not event.cancelled:
            self.msg_channel(message, target, use_event)
            return True
        return False

    def channel_kick(self, user, channel=None, reason=None, force=False):
        # TODO: Event?
        self.log.debug("Attempting to kick '%s' for '%s'" % (user, reason))
        if not isinstance(user, User):
            user = self.get_user(user)
            if user is None:
                return False
        if not force:
            if not self.ourselves.can_kick(user, channel):
                self.log.trace("Tried to kick, but don't have permission")
                return False
        msg = Mumble_pb2.UserRemove()
        msg.session = user.session
        msg.actor = self.ourselves.session
        if reason is not None:
            msg.reason = reason

        self.sendProtobuf(msg)

    def channel_ban(self, user, channel=None, reason=None, force=False):
        # TODO: Event?
        self.log.debug("Attempting to ban '%s' for '%s'" % (user, reason))
        if not isinstance(user, User):
            user = self.get_user(user)
            if user is None:
                return False
        if not force:
            if not self.ourselves.can_ban(user, channel):
                self.log.trace("Tried to ban, but don't have permission")
                return False
        msg = Mumble_pb2.UserRemove()
        msg.session = user.session
        msg.actor = self.ourselves.session
        if reason is not None:
            msg.reason = reason
        msg.ban = True

        self.sendProtobuf(msg)

    def global_ban(self, user, reason=None, force=False):
        # TODO: Event?
        return False

    def global_kick(self, user, reason=None, force=False):
        # TODO: Event?
        return False

    def msg(self, message, target="channel", target_id=None):
        if target_id is None and target == "channel":
            target_id = self.ourselves.channel.channel_id

        self.log.trace(_("Sending text message: %s") % message)

        message = cgi.escape(message)

        msg = Mumble_pb2.TextMessage()  # session, channel_id, tree_id, message
        msg.message = message
        if target == "channel":
            msg.channel_id.append(target_id)
        else:
            msg.session.append(target_id)

        self.sendProtobuf(msg)

    def msg_channel(self, message, channel, use_event=True):
        if isinstance(channel, Channel):
            channel = channel.channel_id

        if use_event:
            event = general_events.MessageSent(self, "message",
                                               self.channels[channel], message)
            self.event_manager.run_callback("MessageSent", event)

            message = event.message

        self.log.info("-> *%s* %s" % (self.channels[channel], message))

        self.msg(message, "channel", channel)

    def msg_user(self, message, user, use_event=True):
        if isinstance(user, User):
            user = user.session

        if use_event:
            event = general_events.MessageSent(self, "message",
                                               self.users[user], message)
            self.event_manager.run_callback("MessageSent", event)

            message = event.message

        self.log.info("-> (%s) %s" % (self.users[user], message))

        self.msg(message, "user", user)

    def join_channel(self, channel, password=None):
        if isinstance(channel, str) or isinstance(channel, unicode):
            channel = self.get_channel(channel)
        if channel is None:
            return False
        if isinstance(channel, Channel):
            channel = channel.channel_id
        msg = Mumble_pb2.UserState()
        msg.channel_id = channel
        self.sendProtobuf(msg)
        return True

    def leave_channel(self, channel=None, reason=None):
        return False

    def get_channel(self, name_or_id=None):
        if name_or_id is None:
            return self.ourselves.channel  # Yay

        if isinstance(name_or_id, str) or isinstance(name_or_id, unicode):
            name = name_or_id.lower()
            for cid, channel in self.channels.iteritems():
                if channel.name.lower() == name:
                    return channel
            return None
        else:
            # Assume ID - it's a hash lookup anyway
            try:
                return self.channels[name_or_id]
            except KeyError:
                return None

    def get_user(self, name_or_session):
        if isinstance(name_or_session, str):
            name = name_or_session.lower()
            for session, user in self.users.iteritems():
                if user.nickname.lower() == name:
                    return user
            return None
        else:
            # Assume session - it's a hash lookup anyway
            try:
                return self.users[name_or_session]
            except KeyError:
                return None

    # region Permissions

    def set_permissions(self, channel, permissions, flush=False):
        # TODO: Investigate flush properly
        self._acls[channel] = permissions

    def has_permission(self, channel, *perms):
        # TODO: Figure out how perms actually work, and how to store them, etc.
        # Note: Do not use these yet.
        if not isinstance(channel, Channel):
            channel = self.get_channel(channel)
        if channel is None or channel not in self._acls:
            return False
        return Perms.has_permission(self._acls[channel], *perms)

    # endregion

    def print_users(self):
        # TODO: Remove this debug function once user handling is complete
        def print_indented(s, times=1):
            print ("\t" * times), s
        for user in self.users.itervalues():
            print user
            cn = user.channel.__str__()
            print_indented(_("Channel: %s") % cn.encode('ascii', 'replace'))
            print_indented(_("Mute: %s") % user.mute)
            print_indented(_("Deaf: %s") % user.deaf)
            print_indented(_("Suppressed: %s") % user.suppress)
            print_indented(_("Self mute: %s") % user.self_mute)
            print_indented(_("Self deaf: %s") % user.self_deaf)
            print_indented(_("Priority speaker: %s") % user.priority_speaker)
            print_indented(_("Recording: %s") % user.recording)

    def print_channels(self):
        # TODO: Remove this debug function once channel handling is complete
        def get_children_channels(channel_id):
            children = []
            for cid, channel in self.channels.iteritems():
                if channel.parent == channel_id:
                    children.append(cid)
            return children

        def print_channel(channels, channel_id, depth=0):
            print "----" * depth,\
                self.channels[channel_id].__str__().encode('ascii', 'replace')
            # Print users, if any
            if len(self.channels[channel_id].users) > 0:
                print "    " * (depth + 1), _("Users {")
                for user in self.channels[channel_id].users:
                    print "    " * (depth + 2), user
                print "    " * (depth + 1), "}"
            # Print sub-channels
            for chan in channels[channel_id]:
                print_channel(channels, chan, depth + 1)

        chans = {}  # Assumes root channel is 0 - not sure if this is ever not
        for cid, chan in self.channels.iteritems():
            chans[cid] = get_children_channels(cid)
        print_channel(chans, 0)
예제 #8
0
class Manager(object):
    """
    Manager for keeping track of multiple factories - one per protocol.

    This is so that the bot can connect to multiple services at once, and have
    them communicate with each other.
    """

    __metaclass__ = Singleton

    #: Instance of the storage manager
    storage = None

    #: Storage for all of our factories.
    factories = {}

    #: Storage for all of the protocol configs.
    configs = {}

    #: The main configuration is stored here.
    main_config = None

    #: Whether the manager is already running or not
    running = False

    #: Console handler
    console_magic = None

    def __init__(self):
        self.commands = CommandManager()
        self.event_manager = EventManager()
        self.logger = getLogger("Manager")
        self.plugman = PluginManager(self)
        self.yapsy_logger = getLogger("yapsy")

        self.metrics = None

    @property
    def all_plugins(self):
        return self.plugman.info_objects

    @property
    def loaded_plugins(self):
        return self.plugman.plugin_objects

    def setup(self):
        signal.signal(signal.SIGINT, self.signal_callback)

        self.yapsy_logger.debug_ = self.yapsy_logger.debug
        self.yapsy_logger.debug = self.yapsy_logger.trace

        self.storage = StorageManager()
        self.main_config = self.storage.get_file(self, "config", YAML,
                                                 "settings.yml")

        self.commands.set_factory_manager(self)

        self.load_config()  # Load the configuration

        try:
            self.metrics = Metrics(self.main_config, self)
        except Exception:
            self.logger.exception(_("Error setting up metrics."))

        self.plugman.scan()
        self.load_plugins()  # Load the configured plugins
        self.load_protocols()  # Load and set up the protocols

        if not len(self.factories):
            self.logger.info(
                _("It seems like no protocols are loaded. "
                  "Shutting down.."))
            return

        self.console_magic = ConsoleMagic()

    def run(self):
        if not self.running:
            event = ReactorStartedEvent(self)

            reactor.callLater(0, self.event_manager.run_callback,
                              "ReactorStarted", event)

            self.running = True
            reactor.run()
        else:
            raise RuntimeError(_("Manager is already running!"))

    def signal_callback(self, signum, frame):
        try:
            try:
                self.unload()
                self.console_magic.unwrap()
            except Exception:
                self.logger.exception(_("Error while unloading!"))
                try:
                    reactor.stop()
                except Exception:
                    try:
                        reactor.crash()
                    except Exception:
                        pass
        except Exception:
            exit(0)

    # Load stuff

    def load_config(self):
        """
        Load the main configuration file.

        :return: Whether the config was loaded or not
        :rtype: bool
        """

        try:
            self.logger.info(_("Loading global configuration.."))
            if not self.main_config.exists:
                self.logger.error(
                    _("Main configuration not found! Please correct this and try"
                      " again."))
                return False
        except IOError:
            self.logger.error(
                _("Unable to load main configuration at config/settings.yml"))
            self.logger.error(_("Please check that this file exists."))
            return False
        except Exception:
            self.logger.exception(
                _("Unable to load main configuration at config/settings.yml"))
            return False
        return True

    def load_plugins(self):
        """
        Attempt to load all of the plugins.
        """

        self.logger.info(_("Loading plugins.."))

        self.logger.trace(
            _("Configured plugins: %s") %
            ", ".join(self.main_config["plugins"]))

        self.plugman.load_plugins(self.main_config.get("plugins", []))

        event = PluginsLoadedEvent(self, self.plugman.plugin_objects)
        self.event_manager.run_callback("PluginsLoaded", event)

    @deprecated("Use the plugin manager directly")
    def load_plugin(self, name, unload=False):
        """
        Load a single plugin by name.

        This will return one of the system.enums.PluginState values.

        :param name: The plugin to load.
        :type name: str

        :param unload: Whether to unload the plugin, if it's already loaded.
        :type unload: bool
        """

        result = self.plugman.load_plugin(name)

        if result is PluginState.AlreadyLoaded:
            if unload:
                result_two = self.plugman.unload_plugin(name)

                if result_two is not PluginState.Unloaded:
                    return result_two

                result = self.plugman.load_plugin(name)

        return result

    @deprecated("Use the plugin manager directly")
    def collect_plugins(self):
        """
        Collect all possible plugin candidates.
        """

        self.plugman.scan()

    def load_protocols(self):
        """
        Load and set up all of the configured protocols.
        """

        self.logger.info(_("Setting up protocols.."))

        for protocol in self.main_config["protocols"]:
            if protocol.lower().startswith("plugin-"):
                self.logger.error("Invalid protocol name: %s" % protocol)
                self.logger.error(
                    "Protocol names beginning with \"plugin-\" are reserved "
                    "for plugin use.")
                continue

            self.logger.info(_("Setting up protocol: %s") % protocol)
            conf_location = "protocols/%s.yml" % protocol
            result = self.load_protocol(protocol, conf_location)

            if result is not PROTOCOL_LOADED:
                if result is PROTOCOL_ALREADY_LOADED:
                    self.logger.warn(_("Protocol is already loaded."))
                elif result is PROTOCOL_CONFIG_NOT_EXISTS:
                    self.logger.warn(
                        _("Unable to find protocol "
                          "configuration."))
                elif result is PROTOCOL_LOAD_ERROR:
                    self.logger.warn(
                        _("Error detected while loading "
                          "protocol."))
                elif result is PROTOCOL_SETUP_ERROR:
                    self.logger.warn(
                        _("Error detected while setting up "
                          "protocol."))

    def load_protocol(self, name, conf_location):
        """
        Attempt to load a protocol by name. This can return one of the
        following, from system.constants:

        * PROTOCOL_ALREADY_LOADED
        * PROTOCOL_CONFIG_NOT_EXISTS
        * PROTOCOL_LOAD_ERROR
        * PROTOCOL_LOADED
        * PROTOCOL_SETUP_ERROR

        :param name: The name of the protocol
        :type name: str

        :param conf_location: The location of the config file, relative
            to the config/ directory, or a Config object
        :type conf_location: str, Config
        """

        if name in self.factories:
            return PROTOCOL_ALREADY_LOADED

        config = conf_location
        if not isinstance(conf_location, Config):
            # TODO: Prevent upward directory traversal properly
            conf_location = conf_location.replace("..", "")
            try:
                config = self.storage.get_file(self, "config", YAML,
                                               conf_location)
                if not config.exists:
                    return PROTOCOL_CONFIG_NOT_EXISTS
            except Exception:
                self.logger.exception(
                    _("Unable to load configuration for the '%s' protocol.") %
                    name)
                return PROTOCOL_LOAD_ERROR
        try:
            self.factories[name] = Factory(name, config, self)
            self.factories[name].setup()
            return PROTOCOL_LOADED
        except Exception:
            if name in self.factories:
                del self.factories[name]
            self.logger.exception(
                _("Unable to create factory for the '%s' protocol!") % name)
            return PROTOCOL_SETUP_ERROR

    # Reload stuff

    @deprecated("Use the plugin manager directly")
    def reload_plugin(self, name):
        """
        Attempt to reload a plugin by name.

        This will return one of the system.enums.PluginState values.

        :param name: The name of the plugin
        :type name: str
        """
        return self.plugman.reload_plugin(name)

    def reload_protocol(self, name):
        factory = self.get_factory(name)

        if name is not None:
            factory.shutdown()
            factory.setup()
            return True

    # Unload stuff

    @deprecated("Use the plugin manager directly")
    def unload_plugin(self, name):
        """
        Attempt to unload a plugin by name.

        This will return one of the system.enums.PluginState values.

        :param name: The name of the plugin
        :type name: str
        """

        return self.plugman.unload_plugin(name)

    def unload_protocol(self, name):  # Removes with a shutdown
        """
        Attempt to unload a protocol by name. This will also shut it down.

        :param name: The name of the protocol
        :type name: str

        :return: Whether the protocol was unloaded
        :rtype: bool
        """

        if name in self.factories:
            proto = self.factories[name]
            try:
                proto.shutdown()
            except Exception:
                self.logger.exception(
                    _("Error shutting down protocol %s") % name)
            finally:
                try:
                    self.storage.release_file(self, "config",
                                              "protocols/%s.yml" % name)
                    self.storage.release_files(proto)
                    self.storage.release_files(proto.protocol)
                except Exception:
                    self.logger.exception("Error releasing files for protocol "
                                          "%s" % name)
            del self.factories[name]
            return True
        return False

    def unload(self):
        """
        Shut down and unload everything.
        """

        self.console_magic.unwrap()

        # Shut down!
        for name in self.factories.keys():
            self.logger.info(_("Unloading protocol: %s") % name)
            self.unload_protocol(name)

        self.plugman.unload_plugins()

        if reactor.running:
            try:
                reactor.stop()
            except Exception:
                self.logger.exception("Error stopping reactor")

    # Grab stuff

    def get_protocol(self, name):
        """
        Get the instance of a protocol, by name.

        :param name: The name of the protocol
        :type name: str

        :return: The protocol, or None if it doesn't exist.
        """

        if name in self.factories:
            return self.factories[name].protocol
        return None

    def get_factory(self, name):
        """
        Get the instance of a protocol's factory, by name.

        :param name: The name of the protocol
        :type name: str

        :return: The factory, or None if it doesn't exist.
        """

        if name in self.factories:
            return self.factories[name]
        return None

    @deprecated("Use the plugin manager directly")
    def get_plugin(self, name):
        """
        Get the insatnce of a plugin, by name.
        :param name: The name of the plugin
        :type name: str

        :return: The plugin, or None if it isn't loaded.
        """

        return self.plugman.get_plugin(name)

    def remove_protocol(self, protocol):  # Removes without shutdown
        """
        Remove a protocol without shutting it down. You shouldn't use this.

        :param protocol: The name of the protocol
        :type protocol: str

        :return: Whether the protocol was removed.
        :rtype: bool
        """

        if protocol in self.factories:
            del self.factories[protocol]
            return True
        return False
예제 #9
0
class CommandManager(object):
    """This is the command manager. It's in charge of tracking commands that
    plugins wish to offer, and providing ways for plugins to offer methods
    of providing authentication and permissions.
    """

    __metaclass__ = Singleton

    #: Storage for all the registered commands. ::
    #:
    #:     commands = {
    #:         "command": {
    #:             "f": func(),
    #:             "permission": "plugin.command",
    #:             "owner": object
    #:         }
    #:     }
    commands = {}

    #: Storage for command aliases.
    #:
    #:     aliases = {
    #:         "alias": "command"
    #:     }
    aliases = {}

    @property
    @deprecated("Use the singular auth_handler instead")
    def auth_handlers(self):
        if self.auth_handler:
            return [self.auth_handler]
        return []

    #: Storage for all the registered auth handler.
    #:
    #: Auth handlers are in charge of asserting whether users are logged in
    #: or not, and identifying who they are logged in as.
    auth_handler = None

    #: Storage for the permissions handler. There may only ever be one of
    #: these.
    #:
    #: Permissions handlers are in charge of asserting whether a user has
    #: permission for a specified action. They work together with auth
    #: handlers to determine this.
    perm_handler = None

    #: Storage for the factory manager, to avoid function call overhead.
    factory_manager = None

    def __init__(self):
        self.logger = getLogger("Commands")
        self.event_manager = EventManager()

    def set_factory_manager(self, factory_manager):
        """Set the factory manager.

        This should only ever be called by the factory manager itself.

        :param factory_manager: The factory manager
        :type factory_manager: Manager
        """
        self.factory_manager = factory_manager

    def register_command(self, command, handler, owner, permission=None,
                         aliases=None, default=False):
        """Register a command, provided it hasn't been registered already.

        The params should go like this.

        :param command: The command to register
        :param handler: The command handler
        :param owner: The plugin or object registering the command
        :param permission: The permission needed to run the command
        :param aliases: A list of aliases for the command being registered.
        :param default: Whether the command should be run when there is no
            permissions manager installed.

        :type command: str
        :type handler: function
        :type owner: PluginObject
        :type permission: str, None
        :type aliases: list, None
        :type default: bool

        :returns: Whether the command was registered or not
        :rtype: Boolean
        """

        if aliases is None:
            aliases = []

        if command in self.commands:
            self.logger.warn(_("Object '%s' tried to register command '%s' but"
                               "it's already been registered by object '%s'.")
                             % (owner,
                                command,
                                self.commands[command]["owner"])
                             )
            return False

        self.logger.debug(_("Registering command: %s (%s)")
                          % (command, owner))
        commandobj = {
            "f": handler,
            "permission": permission,
            "owner": owner,
            "default": default
        }

        self.commands[command] = commandobj

        for alias in aliases:
            if alias in self.aliases:
                self.logger.warn(_("Failed to register command alias '%s' as "
                                   "it already belongs to another command.")
                                 % alias)
                continue

            self.logger.debug(_("Registering alias: %s -> %s (%s)")
                              % (alias, command, owner))
            self.aliases[alias] = command

        return True

    def unregister_commands_for_owner(self, owner):
        """Unregister all commands that have been registered by a certain
        object.

        This method checks instances, not types!

        :param owner: The owner to check for
        :type owner: object
        """
        current = self.commands.items()
        for key, value in current:
            if owner is value["owner"]:
                del self.commands[key]
                self.logger.debug(_("Unregistered command: %s") % key)

                aliases = self.aliases.items()
                for k, v in aliases:
                    if v == key:
                        del self.aliases[k]
                        self.logger.debug(_("Unregistered alias: %s") % k)

    def process_input(self, in_str, caller, source, protocol,
                      control_char=None, our_name=None):
        """Process a set of inputs, to check if there's a command there and
        action it.

        This is designed to be used from a protocol.

        :param in_str: The entire message to parse
        :param caller: The User that sent the message
        :param source: The User or Channel that the message was sent to
        :param protocol: The Protocol object the User belongs to
        :param control_char: The control characters (prefix)
        :param our_name: The name of the bot on Protocol

        :type in_str: str
        :type caller: User
        :type source: User, Channel
        :type protocol: Protocol
        :type control_char: str
        :type our_name: str

        :return: Tuple containing CommandState representing the state of
            the command, and either None or an Exception.
        :rtype: tuple(CommandState, None or Exception)
        """

        if control_char is None:
            if hasattr(protocol, "control_chars"):
                control_char = protocol.control_chars
            else:
                self.logger.debug("Protocol %s doesn't have a control "
                                  "character sequence!" % protocol.name)
                return CommandState.Error, NoControlCharacterException(
                    "Protocol %s doesn't have a control character sequence." %
                    protocol.name
                )

        if our_name is None:
            if hasattr(protocol, "nickname"):
                our_name = protocol.nickname

        if our_name is not None:
            control_char = control_char.replace("{NAME}", our_name)
            control_char = control_char.replace("{NICK}", our_name)

        if len(in_str) < len(control_char):
            self.logger.trace("Control character sequence is longer than the "
                              "input string, so this cannot be a command.")
            return CommandState.NotACommand, None

        if in_str.lower().startswith(control_char.lower()):  # It's a command!
            # Remove the command char(s) from the start
            replaced = in_str[len(control_char):]

            split = replaced.split(None, 1)
            if not split:
                return False
            command = split[0]
            args = ""
            if len(split) > 1:
                args = split[1]

            printable = "<%s:%s> %s" % (caller, source, in_str)

            event = events.PreCommand(protocol, command, args, caller,
                                      source, printable, in_str)
            self.event_manager.run_callback("PreCommand", event)

            if event.printable:
                self.logger.info("%s | %s" % (protocol.name,
                                              event.printable)
                                 )

            result = self.run_command(event.command, event.source,
                                      event.target, protocol, event.args)

            return result

        self.logger.debug("Command not found.")
        return CommandState.NotACommand, None

    def run_command(self, command, caller, source, protocol, args):
        """Run a command, provided it's been registered.

        :param command: The command, a string
        :param caller: Who ran the command
        :param source: Where they ran the command
        :param protocol: The protocol they're part of
        :param args: A list of arguments for the command

        :type command: str
        :type caller: User
        :type source: User
        :type protocol: Protocol
        :type args: list

        :return: Tuple containing CommandState representing the state of
            the command, and either None or an Exception.
        :rtype: tuple(CommandState, None or Exception)
        """

        if command not in self.commands:
            if command not in self.aliases:  # Get alias, if it exists
                event = events.UnknownCommand(self, protocol, command, args,
                                              caller, source)

                self.event_manager.run_callback("UnknownCommand", event)

                if event.cancelled:
                    return CommandState.UnknownOverridden, None

                return CommandState.Unknown, None
            command = self.aliases[command]
        # Parse args
        raw_args = args
        try:
            lex = shlex.shlex(args, posix=True)
            lex.whitespace_split = True
            lex.quotes = '"'
            lex.commenters = ""
            parsed_args = list(lex)
        except ValueError:
            parsed_args = None
        try:
            if self.commands[command]["permission"]:
                if not self.perm_handler:
                    if not self.commands[command]["default"]:
                        return CommandState.NoPermission, None

                    try:
                        self.commands[command]["f"](protocol, caller,
                                                    source, command,
                                                    raw_args,
                                                    parsed_args)
                    except RateLimitExceededError:
                        # TODO: Proper decorator
                        return CommandState.RateLimited, None
                    except Exception as e:
                        self.logger.exception("Error running command")
                        return CommandState.Error, e
                else:
                    if self.perm_handler.check(self.commands
                                               [command]["permission"],
                                               caller, source, protocol):
                        try:
                            self.commands[command]["f"](protocol, caller,
                                                        source, command,
                                                        raw_args,
                                                        parsed_args)
                        except RateLimitExceededError:
                            # TODO: Proper decorator
                            return CommandState.RateLimited, None
                        except Exception as e:
                            self.logger.exception("Error running command")
                            return CommandState.Error, e
                    else:
                        return CommandState.NoPermission, None
            else:
                self.commands[command]["f"](protocol, caller, source, command,
                                            raw_args, parsed_args)
        except RateLimitExceededError:
            # TODO: Proper decorator
            return CommandState.RateLimited, None
        except Exception as e:
            self.logger.exception("Error running command")
            return CommandState.Error, e
        else:
            return CommandState.Success, None

    @deprecated("Use set_auth_handler instead")
    def add_auth_handler(self, handler):
        return self.set_auth_handler(handler)

    def set_auth_handler(self, handler):
        """Add an auth handler, provided it hasn't already been added.

        :param handler: The handler to add
        :type handler: object

        :returns: Whether the handler was added or not
        :rtype: Boolean
        """
        if self.auth_handler is None:
            self.auth_handler = handler
            return True

        return False

    def set_permissions_handler(self, handler):
        """Set the permissions handler, provided one hasn't already been set.

        :param handler: The handler to set
        :type handler: object

        :returns: Whether the handler was set or not
        :rtype: Boolean
        """
        if self.perm_handler:
            self.logger.warn(_("Two plugins are trying to provide permissions "
                               "handlers. Only the first will be used!"))
            return False
        self.perm_handler = handler
        return True