Exemplo n.º 1
0
    def __init__(self):
        self.commands = CommandManager()
        self.event_manager = EventManager()
        self.logger = getLogger("Manager")
        self.plugman = PluginManager(self)

        self.metrics = None
Exemplo n.º 2
0
    def __init__(self):
        self.commands = CommandManager()
        self.event_manager = EventManager()
        self.logger = getLogger("Manager")
        self.plugman = PluginManager(self)

        self.metrics = None
Exemplo n.º 3
0
    def __str__(self):
        manager = CommandManager()

        if manager.aliases:
            return "Aliases: %s" % ", ".join(
                ["%s: %s" % (x[0], x[1]) for x in manager.aliases.items()])
        return "Aliases: None"
Exemplo n.º 4
0
    def __init__(self, info, loader):
        from system.factory_manager import FactoryManager
        from system.plugins.manager import PluginManager

        self.commands = CommandManager()
        self.events = EventManager()
        self.factory_manager = FactoryManager()
        self.info = info
        self.logger = getLogger(info.name)
        self.module = self.info.module
        self.plugins = PluginManager()
        self.storage = StorageManager()
        self._loader = loader.name
Exemplo n.º 5
0
    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)

        self.userstats_request_rate = config.get("userstats_request_rate", 60)
Exemplo n.º 6
0
    def add_variables(self, info, factory_manager):
        """
        Adds essential variables at load time and sets up logging

        Do not override this function! If you *do* override this and don't call
        super, your plugin WILL NOT WORK.

        :param info: The plugin info file
        :type info: Info instance

        :param factory_manager: The factory manager
        :type factory_manager: system.factory_manager.FactoryManager
        """

        from system.plugins.manager import PluginManager

        self.commands = CommandManager()
        self.events = EventManager()
        self.factory_manager = factory_manager
        self.info = info
        self.module = self.info.module
        self.plugins = PluginManager()
        self.storage = StorageManager()
Exemplo n.º 7
0
    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)

        self.userstats_request_rate = config.get("userstats_request_rate", 60)
Exemplo n.º 8
0
class FactoryManager(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 = StorageManager()

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

    #: Storage for each factory module, so we can reload them
    factory_modules = {}

    #: 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

    #: Configuration for logging stuff
    logging_config = {}

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

        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_logging(self, args):
        """
        Set up logging.
        """

        self.logging_config = self.storage.get_file(self, "config", YAML,
                                                    "logging.yml")

        try:
            if not self.logging_config.exists:
                logger.configure(None)

                self.logger.warn("No logging config found - using defaults")
                return False
        except IOError:
            logger.configure(None)

            self.logger.error(
                "Unable to load logging configuration at config/logging.yml "
                "- using defaults")
            self.logger.error("Please check that this file exists.")
            return False
        except Exception:
            self.logger.exception(
                "Unable to load logging configuration at config/logging.yml "
                "- using defaults")
            return False

        logger.configure(self.logging_config, args)

        return True

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

        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 ProtocolState.Loaded:
                if result is ProtocolState.AlreadyLoaded:
                    self.logger.warn(_("Protocol is already loaded."))
                elif result is ProtocolState.ConfigNotExists:
                    self.logger.warn(
                        _("Unable to find protocol "
                          "configuration."))
                elif result is ProtocolState.LoadError:
                    self.logger.warn(
                        _("Error detected while loading "
                          "protocol."))
                elif result is ProtocolState.SetupError:
                    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 ProtocolState.AlreadyLoaded

        config = conf_location
        if not isinstance(conf_location, Config):
            if not valid_path(self.storage.conf_path + conf_location):
                self.logger.error("Invalid config location: {}{}".format(
                    self.storage.conf_path, conf_location))

                return

            try:
                config = self.storage.get_file(self, "config", YAML,
                                               conf_location)
                if not config.exists:
                    return ProtocolState.ConfigNotExists
            except Exception:
                self.logger.exception(
                    _("Unable to load configuration for the '%s' protocol.") %
                    name)
                return ProtocolState.LoadError
        try:
            protocol_type = config["main"]["protocol-type"]

            if protocol_type in self.factory_modules:
                factory_module = reload(self.factory_modules[protocol_type])
            else:
                factory_module = importlib.import_module(
                    "system.protocols.{}.factory".format(protocol_type))

            self.factory_modules[protocol_type] = factory_module

            self.factories[name] = factory_module.Factory(name, config, self)
            r = self.factories[name].connect()

            if not r:
                if name in self.factories:
                    del self.factories[name]

                self.logger.error(
                    "Factory setup for the '{}' protocol failed.".format(name))
                return ProtocolState.SetupError

            return ProtocolState.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 ProtocolState.SetupError

    # 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):
        if self.unload_protocol(name):
            return self.load_protocol(name, "protocols/{}.yml".format(name))

        return False

    # 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.
        :rtype: system.protocols.generic.factory.BaseFactory
        """

        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
Exemplo n.º 9
0
    def __init__(self):
        self.manager = CommandManager()
        self.manager.logger.setLevel(logging.CRITICAL)  # Shut up, logger

        self.factory_manager = Mock(name="factory_manager")
        self.plugin = Mock(name="plugin")
Exemplo n.º 10
0
class test_commands:

    def __init__(self):
        self.manager = CommandManager()
        self.manager.logger.setLevel(logging.CRITICAL)  # Shut up, logger

        self.factory_manager = Mock(name="factory_manager")
        self.plugin = Mock(name="plugin")

    @nosetools.nottest
    def teardown(self):
        # Clean up
        self.manager.commands = {}
        self.manager.aliases = {}
        self.manager.auth_handler = None
        self.manager.perm_handler = None

        self.plugin.reset_mock()
        self.plugin.handler.reset_mock()

        self.factory_manager.reset_mock()

    @nose.with_setup(teardown=teardown)
    def test_singleton(self):
        """CMNDS | Test Singleton metaclass"""
        nosetools.assert_true(self.manager is CommandManager())

    @nose.with_setup(teardown=teardown)
    def test_set_factory_manager(self):
        """CMNDS | Test setting factory manager"""
        self.manager.set_factory_manager(self.factory_manager)
        nosetools.assert_true(self.factory_manager)

    @nose.with_setup(teardown=teardown)
    def test_add_command(self):
        """CMNDS | Test adding commands"""
        r = self.manager.register_command("test", self.plugin.handler,
                                          self.plugin, "test.test",
                                          ["test2"], True)

        nosetools.assert_true(r)
        nosetools.assert_true("test" in self.manager.commands)

        command = self.manager.commands.get("test", None)

        if command:
            nosetools.assert_true("f" in command)
            nosetools.assert_true(command.get("f") is self.plugin.handler)

            nosetools.assert_true("permission" in command)
            nosetools.assert_true(command.get("permission") == "test.test")

            nosetools.assert_true("owner" in command)
            nosetools.assert_true(command.get("owner") is self.plugin)

            nosetools.assert_true("default" in command)
            nosetools.assert_true(command.get("default"))

        nosetools.assert_true("test2" in self.manager.aliases)

        alias = self.manager.aliases.get("test2", None)

        if alias:
            nosetools.assert_true(alias == "test")

        r = self.manager.register_command("test", self.plugin.handler,
                                          self.plugin, "test.test",
                                          ["test2"], True)

        nosetools.assert_false(r)

    @nose.with_setup(teardown=teardown)
    def test_unregister_commands(self):
        """CMNDS | Test unregistering commands"""
        self.manager.register_command("test1", self.plugin.handler,
                                      self.plugin, aliases=["test11"])
        self.manager.register_command("test2", self.plugin.handler,
                                      self.plugin, aliases=["test22"])
        self.manager.register_command("test3", self.plugin.handler,
                                      self.plugin, aliases=["test33"])

        nosetools.assert_equals(len(self.manager.commands), 3)
        nosetools.assert_equals(len(self.manager.aliases), 3)

        self.manager.unregister_commands_for_owner(self.plugin)

        nosetools.assert_equals(len(self.manager.commands), 0)
        nosetools.assert_equals(len(self.manager.aliases), 0)

    @nose.with_setup(teardown=teardown)
    def test_run_commands_defaults(self):
        """CMNDS | Test running commands directly | Defaults"""

        self.manager.register_command("test4", self.plugin.handler,
                                      self.plugin, aliases=["test5"],
                                      default=True)

        caller = Mock(name="caller")
        source = Mock(name="source")
        protocol = Mock(name="protocol")

        # Testing defaults

        r = self.manager.run_command("test4", caller, source, protocol, "")
        nosetools.assert_equals(r, (CommandState.Success, None))

        r = self.manager.run_command("test5", caller, source, protocol, "")
        nosetools.assert_equals(r, (CommandState.Success, None))

        nosetools.assert_equals(self.plugin.handler.call_count, 2)

    @nose.with_setup(teardown=teardown)
    def test_run_commands_aliases(self):
        """CMNDS | Test running commands directly | Aliases"""

        self.manager.register_command("test4", self.plugin.handler,
                                      self.plugin, aliases=["test5"],
                                      default=True)

        caller = Mock(name="caller")
        source = Mock(name="source")
        protocol = Mock(name="protocol")

        # Testing defaults

        r = self.manager.run_command("test4", caller, source, protocol, "")
        nosetools.assert_equals(r, (CommandState.Success, None))

        self.plugin.handler.assert_called_with(protocol, caller, source,
                                               "test4", "", [])

        # Reset mock
        self.plugin.handler.reset_mock()

        r = self.manager.run_command("test5", caller, source, protocol, "")
        nosetools.assert_equals(r, (CommandState.Success, None))

        self.plugin.handler.assert_called_with(protocol, caller, source,
                                               "test4", "", [])

        # Reset mock
        self.plugin.handler.reset_mock()

        self.manager.register_command("test5", self.plugin.handler,
                                      self.plugin, default=True)

        r = self.manager.run_command("test5", caller, source, protocol, "")
        nosetools.assert_equals(r, (CommandState.Success, None))

        self.plugin.handler.assert_called_with(protocol, caller, source,
                                               "test5", "", [])

    @nose.with_setup(teardown=teardown)
    def test_run_commands_auth(self):
        """CMNDS | Test running commands directly | Auth"""

        caller = Mock(name="caller")
        source = Mock(name="source")
        protocol = Mock(name="protocol")

        auth = Mock(name="auth")
        perms = Mock(name="perms")

        self.manager.set_auth_handler(auth)
        self.manager.set_permissions_handler(perms)

        # COMMAND WITH PERMISSION #
        auth.authorized.return_value = True
        perms.check.return_value = True

        self.manager.register_command("test5", self.plugin.handler,
                                      self.plugin, "test.test",
                                      aliases=["test6"])

        r = self.manager.run_command("test5", caller, source, protocol, "")

        nosetools.assert_equals(r, (CommandState.Success, None))
        nosetools.assert_equals(self.plugin.handler.call_count, 1)

        auth.authorized.reset_mock()
        perms.check.reset_mock()
        self.plugin.handler.reset_mock()

        # ALIAS WITH PERMISSION #

        r = self.manager.run_command("test6", caller, source, protocol, "")

        nosetools.assert_equals(r, (CommandState.Success, None))
        nosetools.assert_equals(self.plugin.handler.call_count, 1)

        auth.authorized.reset_mock()
        perms.check.reset_mock()

        auth.authorized.return_value = True
        perms.check.return_value = False
        self.plugin.handler.reset_mock()

        # COMMAND WITHOUT PERMISSION #

        r = self.manager.run_command("test5", caller, source, protocol, "")

        nosetools.assert_equals(r, (CommandState.NoPermission, None))
        nosetools.assert_equals(self.plugin.handler.call_count, 0)

        perms.check.return_value = True

        auth.authorized.reset_mock()
        perms.check.reset_mock()
        self.plugin.handler.reset_mock()

        # COMMAND WITH EXCEPTION #

        self.plugin.handler = Mock(side_effect=Exception('Boom!'))
        self.manager.unregister_commands_for_owner(self.plugin)

        self.manager.register_command("test5", self.plugin.handler,
                                      self.plugin, "test.test")

        r = self.manager.run_command("test5", caller, source, protocol, "")

        nosetools.assert_equals(r[0], CommandState.Error)
        nosetools.assert_true(isinstance(r[1], Exception))
        nosetools.assert_equals(self.plugin.handler.call_count, 1)

        auth.authorized.reset_mock()
        perms.check.reset_mock()
        self.plugin.handler.reset_mock()

        # UNKNOWN COMMAND #

        r = self.manager.run_command("test7", caller, source, protocol, "")
        nosetools.assert_equals(r, (CommandState.Unknown, None))
        nosetools.assert_equals(self.plugin.handler.call_count, 0)
Exemplo n.º 11
0
class FactoryManager(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 = StorageManager()

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

    #: Storage for each factory module, so we can reload them
    factory_modules = {}

    #: 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

    #: Configuration for logging stuff
    logging_config = {}

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

        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_logging(self, args):
        """
        Set up logging.
        """

        self.logging_config = self.storage.get_file(
            self, "config", YAML, "logging.yml"
        )

        try:
            if not self.logging_config.exists:
                logger.configure(None)

                self.logger.warn(
                    "No logging config found - using defaults"
                )
                return False
        except IOError:
            logger.configure(None)

            self.logger.error(
                "Unable to load logging configuration at config/logging.yml "
                "- using defaults"
            )
            self.logger.error("Please check that this file exists.")
            return False
        except Exception:
            self.logger.exception(
                "Unable to load logging configuration at config/logging.yml "
                "- using defaults"
            )
            return False

        logger.configure(self.logging_config, args)

        return True

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

        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()
        deferred = self.load_plugins()  # Load the configured plugins
        deferred.addCallback(self.deferred_callback)

    def deferred_callback(self, _=None):
        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.unload()

    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!"))

    @inlineCallbacks
    def signal_callback(self, signum, frame):
        try:
            try:
                __ = yield 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

    @inlineCallbacks
    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"]))

        result = yield 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 ProtocolState.Loaded:
                if result is ProtocolState.AlreadyLoaded:
                    self.logger.warn(_("Protocol is already loaded."))
                elif result is ProtocolState.ConfigNotExists:
                    self.logger.warn(_("Unable to find protocol "
                                       "configuration."))
                elif result is ProtocolState.LoadError:
                    self.logger.warn(_("Error detected while loading "
                                       "protocol."))
                elif result is ProtocolState.SetupError:
                    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 ProtocolState.AlreadyLoaded

        config = conf_location
        if not isinstance(conf_location, Config):
            if not valid_path(self.storage.conf_path + conf_location):
                self.logger.error(
                    "Invalid config location: {}{}".format(
                        self.storage.conf_path, conf_location
                    )
                )

                return

            try:
                config = self.storage.get_file(self, "config", YAML,
                                               conf_location)
                if not config.exists:
                    return ProtocolState.ConfigNotExists
            except Exception:
                self.logger.exception(
                    _("Unable to load configuration for the '%s' protocol.")
                    % name)
                return ProtocolState.LoadError
        try:
            protocol_type = config["main"]["protocol-type"]

            if protocol_type in self.factory_modules:
                factory_module = reload(self.factory_modules[protocol_type])
            else:
                factory_module = importlib.import_module(
                    "system.protocols.{}.factory".format(protocol_type)
                )

            self.factory_modules[protocol_type] = factory_module

            self.factories[name] = factory_module.Factory(name, config, self)
            r = self.factories[name].connect()

            if not r:
                if name in self.factories:
                    del self.factories[name]

                self.logger.error(
                    "Factory setup for the '{}' protocol failed.".format(
                        name
                    )
                )
                return ProtocolState.SetupError

            return ProtocolState.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 ProtocolState.SetupError

    # 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):
        if self.unload_protocol(name):
            return self.load_protocol(name, "protocols/{}.yml".format(name))

        return False

    # 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

    @inlineCallbacks
    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)

        __ = yield 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.
        :rtype: system.protocols.generic.factory.BaseFactory
        """

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

    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
Exemplo n.º 12
0
    def __str__(self):
        manager = CommandManager()

        if manager.commands:
            return "Commands: %s" % ", ".join(manager.commands.keys())
        return "Commands: None"
Exemplo n.º 13
0
 def test_singleton(self):
     """CMNDS | Test Singleton metaclass"""
     nosetools.assert_true(self.manager is CommandManager())
Exemplo n.º 14
0
    def __init__(self):
        self.manager = CommandManager()
        self.manager.logger.setLevel(logging.CRITICAL)  # Shut up, logger

        self.factory_manager = Mock(name="factory_manager")
        self.plugin = Mock(name="plugin")
Exemplo n.º 15
0
class test_commands:
    def __init__(self):
        self.manager = CommandManager()
        self.manager.logger.setLevel(logging.CRITICAL)  # Shut up, logger

        self.factory_manager = Mock(name="factory_manager")
        self.plugin = Mock(name="plugin")

    @nosetools.nottest
    def teardown(self):
        # Clean up
        self.manager.commands = {}
        self.manager.aliases = {}
        self.manager.auth_handler = None
        self.manager.perm_handler = None

        self.plugin.reset_mock()
        self.plugin.handler.reset_mock()

        self.factory_manager.reset_mock()

    @nose.with_setup(teardown=teardown)
    def test_singleton(self):
        """CMNDS | Test Singleton metaclass"""
        nosetools.assert_true(self.manager is CommandManager())

    @nose.with_setup(teardown=teardown)
    def test_set_factory_manager(self):
        """CMNDS | Test setting factory manager"""
        self.manager.set_factory_manager(self.factory_manager)
        nosetools.assert_true(self.factory_manager)

    @nose.with_setup(teardown=teardown)
    def test_add_command(self):
        """CMNDS | Test adding commands"""
        r = self.manager.register_command("test", self.plugin.handler,
                                          self.plugin, "test.test", ["test2"],
                                          True)

        nosetools.assert_true(r)
        nosetools.assert_true("test" in self.manager.commands)

        command = self.manager.commands.get("test", None)

        if command:
            nosetools.assert_true("f" in command)
            nosetools.assert_true(command.get("f") is self.plugin.handler)

            nosetools.assert_true("permission" in command)
            nosetools.assert_true(command.get("permission") == "test.test")

            nosetools.assert_true("owner" in command)
            nosetools.assert_true(command.get("owner") is self.plugin)

            nosetools.assert_true("default" in command)
            nosetools.assert_true(command.get("default"))

        nosetools.assert_true("test2" in self.manager.aliases)

        alias = self.manager.aliases.get("test2", None)

        if alias:
            nosetools.assert_true(alias == "test")

        r = self.manager.register_command("test", self.plugin.handler,
                                          self.plugin, "test.test", ["test2"],
                                          True)

        nosetools.assert_false(r)

    @nose.with_setup(teardown=teardown)
    def test_unregister_commands(self):
        """CMNDS | Test unregistering commands"""
        self.manager.register_command("test1",
                                      self.plugin.handler,
                                      self.plugin,
                                      aliases=["test11"])
        self.manager.register_command("test2",
                                      self.plugin.handler,
                                      self.plugin,
                                      aliases=["test22"])
        self.manager.register_command("test3",
                                      self.plugin.handler,
                                      self.plugin,
                                      aliases=["test33"])

        nosetools.assert_equals(len(self.manager.commands), 3)
        nosetools.assert_equals(len(self.manager.aliases), 3)

        self.manager.unregister_commands_for_owner(self.plugin)

        nosetools.assert_equals(len(self.manager.commands), 0)
        nosetools.assert_equals(len(self.manager.aliases), 0)

    @nose.with_setup(teardown=teardown)
    def test_run_commands_defaults(self):
        """CMNDS | Test running commands directly | Defaults"""

        self.manager.register_command("test4",
                                      self.plugin.handler,
                                      self.plugin,
                                      aliases=["test5"],
                                      default=True)

        caller = Mock(name="caller")
        source = Mock(name="source")
        protocol = Mock(name="protocol")

        # Testing defaults

        r = self.manager.run_command("test4", caller, source, protocol, "")
        nosetools.assert_equals(r, (CommandState.Success, None))

        r = self.manager.run_command("test5", caller, source, protocol, "")
        nosetools.assert_equals(r, (CommandState.Success, None))

        nosetools.assert_equals(self.plugin.handler.call_count, 2)

    @nose.with_setup(teardown=teardown)
    def test_run_commands_aliases(self):
        """CMNDS | Test running commands directly | Aliases"""

        self.manager.register_command("test4",
                                      self.plugin.handler,
                                      self.plugin,
                                      aliases=["test5"],
                                      default=True)

        caller = Mock(name="caller")
        source = Mock(name="source")
        protocol = Mock(name="protocol")

        # Testing defaults

        r = self.manager.run_command("test4", caller, source, protocol, "")
        nosetools.assert_equals(r, (CommandState.Success, None))

        self.plugin.handler.assert_called_with(protocol, caller, source,
                                               "test4", "", [])

        # Reset mock
        self.plugin.handler.reset_mock()

        r = self.manager.run_command("test5", caller, source, protocol, "")
        nosetools.assert_equals(r, (CommandState.Success, None))

        self.plugin.handler.assert_called_with(protocol, caller, source,
                                               "test4", "", [])

        # Reset mock
        self.plugin.handler.reset_mock()

        self.manager.register_command("test5",
                                      self.plugin.handler,
                                      self.plugin,
                                      default=True)

        r = self.manager.run_command("test5", caller, source, protocol, "")
        nosetools.assert_equals(r, (CommandState.Success, None))

        self.plugin.handler.assert_called_with(protocol, caller, source,
                                               "test5", "", [])

    @nose.with_setup(teardown=teardown)
    def test_run_commands_auth(self):
        """CMNDS | Test running commands directly | Auth"""

        caller = Mock(name="caller")
        source = Mock(name="source")
        protocol = Mock(name="protocol")

        auth = Mock(name="auth")
        perms = Mock(name="perms")

        self.manager.set_auth_handler(auth)
        self.manager.set_permissions_handler(perms)

        # COMMAND WITH PERMISSION #
        auth.authorized.return_value = True
        perms.check.return_value = True

        self.manager.register_command("test5",
                                      self.plugin.handler,
                                      self.plugin,
                                      "test.test",
                                      aliases=["test6"])

        r = self.manager.run_command("test5", caller, source, protocol, "")

        nosetools.assert_equals(r, (CommandState.Success, None))
        nosetools.assert_equals(self.plugin.handler.call_count, 1)

        auth.authorized.reset_mock()
        perms.check.reset_mock()
        self.plugin.handler.reset_mock()

        # ALIAS WITH PERMISSION #

        r = self.manager.run_command("test6", caller, source, protocol, "")

        nosetools.assert_equals(r, (CommandState.Success, None))
        nosetools.assert_equals(self.plugin.handler.call_count, 1)

        auth.authorized.reset_mock()
        perms.check.reset_mock()

        auth.authorized.return_value = True
        perms.check.return_value = False
        self.plugin.handler.reset_mock()

        # COMMAND WITHOUT PERMISSION #

        r = self.manager.run_command("test5", caller, source, protocol, "")

        nosetools.assert_equals(r, (CommandState.NoPermission, None))
        nosetools.assert_equals(self.plugin.handler.call_count, 0)

        perms.check.return_value = True

        auth.authorized.reset_mock()
        perms.check.reset_mock()
        self.plugin.handler.reset_mock()

        # COMMAND WITH EXCEPTION #

        self.plugin.handler = Mock(side_effect=Exception('Boom!'))
        self.manager.unregister_commands_for_owner(self.plugin)

        self.manager.register_command("test5", self.plugin.handler,
                                      self.plugin, "test.test")

        r = self.manager.run_command("test5", caller, source, protocol, "")

        nosetools.assert_equals(r[0], CommandState.Error)
        nosetools.assert_true(isinstance(r[1], Exception))
        nosetools.assert_equals(self.plugin.handler.call_count, 1)

        auth.authorized.reset_mock()
        perms.check.reset_mock()
        self.plugin.handler.reset_mock()

        # UNKNOWN COMMAND #

        r = self.manager.run_command("test7", caller, source, protocol, "")
        nosetools.assert_equals(r, (CommandState.Unknown, None))
        nosetools.assert_equals(self.plugin.handler.call_count, 0)
Exemplo n.º 16
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,
        Mumble_pb2.SuggestConfig
    ]

    # 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

    use_cgi = True

    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)

        self.userstats_request_rate = config.get("userstats_request_rate", 60)

    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.stop_userstats_requests()
        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 looping handlers
        self.init_ping()
        self.start_userstats_requests()

        # 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 connectionLost(self, reason=None):
        self.pinging = False
        self.stop_userstats_requests()

    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)
        elif isinstance(message, Mumble_pb2.UserStats):
            self.handle_msg_userstats(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 start_userstats_requests(self):
        self._userstats_requests_task = task.LoopingCall(
            self.userstats_request_handler)
        self._userstats_requests_task.start(self.userstats_request_rate, False)

    def stop_userstats_requests(self):
        if self._userstats_requests_task and \
                self._userstats_requests_task.running:
            self._userstats_requests_task.stop()

    def userstats_request_handler(self):
        try:
            for user in self.users.itervalues():
                self.request_userstats(user, True)
        except Exception:
            self.log.exception("Error in UserStats request loop")

    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
            # Note: RE: above note - Mumble desktop client source suggests not.
            user = 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.users[message.session] = user

            # TODO: plugin_identity and plugin_context
            # TODO: Handle comments and avatars properly
            if message.HasField("comment_hash"):
                user.comment_hash = message.comment_hash
            if message.HasField("comment"):
                user.comment = message.comment
            if message.HasField("texture_hash"):
                user.avatar_hash = message.texture_hash
            if message.HasField("texture"):
                user.avatar = message.texture

            if message.HasField("user_id"):
                user_id = message.user_id
                if user_id == 4294967295:
                    # This should never happen, but things will break if it
                    # does. See comment below for explanation of 4294967295.
                    user_id = -1
                user.user_id = user_id

            if message.HasField("hash"):
                user.certificate_hash = message.hash

            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(user)

            # Store our User object
            if message.name == self.username:
                self.ourselves = user
                # 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"] is not None:
                        cid = conf["id"]
                        if not isinstance(cid, int):
                            try:
                                cid = int(cid)
                            except ValueError:
                                self.log.error(
                                    "Channel ID in config must be a number.")
                            else:
                                self.log.warning(
                                    "Channel ID in config should be a number.")
                        if cid in self.channels:
                            self.join_channel(self.channels[cid])
                        else:
                            self.log.warning(
                                _("No channel with id '%s'") % cid)
                    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, user)
                self.event_manager.run_callback("Mumble/UserJoined", event)

            # Request initial UserStats
            self.request_userstats(user, False)
        else:
            # Note: More than one state change can happen at once
            user = self.users[message.session]
            if message.HasField("actor"):
                actor = self.users[message.actor]
            else:
                actor = None
            if message.HasField('channel_id'):
                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'):
                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'):
                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'):
                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)
            # TODO: Events
            # - Comments/avatars may want higher level events. For example, we
            # - may want to automatically request the full comment/avatar if we
            # - get a hash, and only provide an API for the full thing.
            if message.HasField("comment_hash"):
                user.comment_hash = message.comment_hash
            if message.HasField("comment"):
                user.comment = message.comment
            if message.HasField("texture_hash"):
                user.avatar_hash = message.texture_hash
            if message.HasField("texture"):
                user.avatar = message.texture

            if message.HasField("user_id"):
                user_id = message.user_id
                if user_id == 4294967295:
                    # Mumble uses -1 internally, but the field on the message
                    # is a uint32, so we get that instead. We'll use -1 too.
                    user_id = -1
                old_user_id = user.user_id
                user.user_id = user_id
                if user_id >= 0:
                    self.log.info("User was registered: {} by {}", user,
                                  user_id, actor)
                    event = mumble_events.UserRegistered(
                        self, user, user_id, actor)
                    event_type = "Mumble/UserRegistered"
                else:
                    self.log.info("User was unregistered: {} by {}", user,
                                  user_id, actor)
                    event = mumble_events.UserUnregistered(
                        self, user, old_user_id, actor)
                    event_type = "Mumble/UserUnregistered"
                self.event_manager.run_callback(event_type, 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)

    def handle_msg_userstats(self, message):
        user = self.users[message.session]

        # Not sure if this would ever go over UDP, but if it does, then it's
        # possible to arrive after the user has disconnected.
        if user is None:
            self.log.warning(
                "Received UserStats message for non-existent user")
            return

        # You'd think the stats_only flag would avoid us having to do all these
        # HasField checks, but the server doesn't appear to ever send it.
        # There are some fields that appear to exist or not in a group, but
        # I'm not certain if that's always the case, and 3rd party
        # implementations may do it differently anyway.

        if len(message.certificates):
            user.certificates = list(message.certificates)
        if message.HasField("version"):
            user.version = Version(message.version.version,
                                   message.version.release, message.version.os,
                                   message.version.os_version)
        if len(message.celt_versions):
            user.celt_versions = list(message.celt_versions)
        if message.HasField("address"):
            user.address = message.address
        if message.HasField("strong_certificate"):
            user.strong_certificate = message.strong_certificate
        if message.HasField("opus"):
            user.opus = message.opus

        if message.HasField("from_client"):
            stats = user.packet_stats_from_client
            if message.from_client.HasField("good"):
                stats.good = message.from_client.good
            if message.from_client.HasField("late"):
                stats.late = message.from_client.late
            if message.from_client.HasField("lost"):
                stats.lost = message.from_client.lost
            if message.from_client.HasField("resync"):
                stats.resync = message.from_client.resync

        if message.HasField("from_server"):
            stats = user.packet_stats_from_server
            if message.from_server.HasField("good"):
                stats.good = message.from_server.good
            if message.from_server.HasField("late"):
                stats.late = message.from_server.late
            if message.from_server.HasField("lost"):
                stats.lost = message.from_server.lost
            if message.from_server.HasField("resync"):
                stats.resync = message.from_server.resync

        if message.HasField("udp_packets"):
            user.udp_packets_sent = message.udp_packets
        if message.HasField("tcp_packets"):
            user.tcp_packets_sent = message.tcp_packets

        if message.HasField("udp_ping_avg"):
            user.udp_ping_avg = message.udp_ping_avg
        if message.HasField("udp_ping_var"):
            user.udp_ping_var = message.udp_ping_var
        if message.HasField("tcp_ping_avg"):
            user.tcp_ping_avg = message.tcp_ping_avg
        if message.HasField("tcp_ping_var"):
            user.tcp_ping_var = message.tcp_ping_var

        if message.HasField("onlinesecs"):
            user.online_time = message.onlinesecs
        if message.HasField("idlesecs"):
            user.idle_time = message.idlesecs

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

    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)

        if self.use_cgi:
            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 request_userstats(self, user, stats_only=False):
        self.log.debug("Requesting UserStats for {}, stats_only={}", user,
                       stats_only)
        user_stats = Mumble_pb2.UserStats()
        user_stats.session = user.session
        user_stats.stats_only = stats_only
        self.sendProtobuf(user_stats)

    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)
Exemplo n.º 17
0
class Protocol(SingleChannelProtocol):

    TYPE = "mumble"
    CAPABILITIES = (
        Capabilities.MULTIPLE_CHANNELS,
        Capabilities.MULTILINE_MESSAGE,
        Capabilities.MESSAGE_UNJOINED_CHANNELS,
        Capabilities.VOICE,
    )

    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,
        Mumble_pb2.SuggestConfig
    ]

    # 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 = {}

    # Server config - defaults from official Mumble client
    max_bandwidth = -1
    welcome_text = None
    allow_html = True
    message_length = 5000
    image_message_length = 131072

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

    control_chars = "."

    pinging = True

    ourselves = None

    use_cgi = True

    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)

        self.userstats_request_rate = config.get("userstats_request_rate", 60)

    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.stop_userstats_requests()
        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 looping handlers
        self.init_ping()
        self.start_userstats_requests()

        # 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 connectionLost(self, reason=None):
        self.pinging = False
        self.stop_userstats_requests()

    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
            self.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)
            self.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]" %
                           (self.max_bandwidth,
                            Perms.get_permissions_names(permissions)))
            for line in self.welcome_text.split("\n"):
                self.log.info(line)
            self.log.info(_("=== End welcome message ==="))

            event = mumble_events.ServerSync(self, session, self.max_bandwidth,
                                             self.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
            if message.HasField("max_bandwidth"):
                self.max_bandwidth = message.max_bandwidth
            if message.HasField("welcome_text"):
                self.welcome_text = message.welcome_text
            if message.HasField("allow_html"):
                self.allow_html = message.allow_html
            if message.HasField("message_length"):
                self.message_length = message.message_length
            if message.HasField("image_message_length"):
                self.image_message_length = message.image_message_length

            # TODO: FIXME: Not all of these are necessarily set by this packet,
            # but the event acts as if they are.
            event = mumble_events.ServerConfig(self, self.max_bandwidth,
                                               self.welcome_text,
                                               self.allow_html,
                                               self.message_length,
                                               self.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)
        elif isinstance(message, Mumble_pb2.UserStats):
            self.handle_msg_userstats(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 start_userstats_requests(self):
        self._userstats_requests_task = task.LoopingCall(
            self.userstats_request_handler
        )
        self._userstats_requests_task.start(self.userstats_request_rate, False)

    def stop_userstats_requests(self):
        if self._userstats_requests_task and \
                self._userstats_requests_task.running:
            self._userstats_requests_task.stop()

    def userstats_request_handler(self):
        try:
            for user in self.users.itervalues():
                self.request_userstats(user, True)
        except Exception:
            self.log.exception("Error in UserStats request loop")

    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
            # Note: RE: above note - Mumble desktop client source suggests not.
            user = 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.users[message.session] = user

            # TODO: plugin_identity and plugin_context
            # TODO: Handle comments and avatars properly
            if message.HasField("comment_hash"):
                user.comment_hash = message.comment_hash
            if message.HasField("comment"):
                user.comment = message.comment
            if message.HasField("texture_hash"):
                user.avatar_hash = message.texture_hash
            if message.HasField("texture"):
                user.avatar = message.texture

            if message.HasField("user_id"):
                user_id = message.user_id
                if user_id == 4294967295:
                    # This should never happen, but things will break if it
                    # does. See comment below for explanation of 4294967295.
                    user_id = -1
                user.user_id = user_id

            if message.HasField("hash"):
                user.certificate_hash = message.hash

            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(user)

            # Store our User object
            if message.name == self.username:
                self.ourselves = user
                # 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"] is not None:
                        cid = conf["id"]
                        if not isinstance(cid, int):
                            try:
                                cid = int(cid)
                            except ValueError:
                                self.log.error(
                                    "Channel ID in config must be a number."
                                )
                            else:
                                self.log.warning(
                                    "Channel ID in config should be a number."
                                )
                        if cid in self.channels:
                            self.join_channel(self.channels[cid])
                        else:
                            self.log.warning(_("No channel with id '%s'") %
                                             cid)
                    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, user)
                self.event_manager.run_callback("Mumble/UserJoined", event)

            # Request initial UserStats
            self.request_userstats(user, False)
        else:
            # Note: More than one state change can happen at once
            user = self.users[message.session]
            if message.HasField("actor"):
                actor = self.users[message.actor]
            else:
                actor = None
            if message.HasField('channel_id'):
                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'):
                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'):
                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'):
                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)
            # TODO: Events
            # - Comments/avatars may want higher level events. For example, we
            # - may want to automatically request the full comment/avatar if we
            # - get a hash, and only provide an API for the full thing.
            if message.HasField("comment_hash"):
                user.comment_hash = message.comment_hash
            if message.HasField("comment"):
                user.comment = message.comment
            if message.HasField("texture_hash"):
                user.avatar_hash = message.texture_hash
            if message.HasField("texture"):
                user.avatar = message.texture

            if message.HasField("user_id"):
                user_id = message.user_id
                if user_id == 4294967295:
                    # Mumble uses -1 internally, but the field on the message
                    # is a uint32, so we get that instead. We'll use -1 too.
                    user_id = -1
                old_user_id = user.user_id
                user.user_id = user_id
                if user_id >= 0:
                    self.log.info("User was registered: {} by {}",
                                  user, user_id, actor)
                    event = mumble_events.UserRegistered(self, user,
                                                         user_id, actor)
                    event_type = "Mumble/UserRegistered"
                else:
                    self.log.info("User was unregistered: {} by {}",
                                  user, user_id, actor)
                    event = mumble_events.UserUnregistered(self, user,
                                                           old_user_id, actor)
                    event_type = "Mumble/UserUnregistered"
                self.event_manager.run_callback(event_type, 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
                )

    def handle_msg_userstats(self, message):
        user = self.users[message.session]

        # Not sure if this would ever go over UDP, but if it does, then it's
        # possible to arrive after the user has disconnected.
        if user is None:
            self.log.warning(
                "Received UserStats message for non-existent user"
            )
            return

        # You'd think the stats_only flag would avoid us having to do all these
        # HasField checks, but the server doesn't appear to ever send it.
        # There are some fields that appear to exist or not in a group, but
        # I'm not certain if that's always the case, and 3rd party
        # implementations may do it differently anyway.

        if len(message.certificates):
            user.certificates = list(message.certificates)
        if message.HasField("version"):
            user.version = Version(
                message.version.version,
                message.version.release,
                message.version.os,
                message.version.os_version
            )
        if len(message.celt_versions):
            user.celt_versions = list(message.celt_versions)
        if message.HasField("address"):
            user.address = message.address
        if message.HasField("strong_certificate"):
            user.strong_certificate = message.strong_certificate
        if message.HasField("opus"):
            user.opus = message.opus

        if message.HasField("from_client"):
            stats = user.packet_stats_from_client
            if message.from_client.HasField("good"):
                stats.good = message.from_client.good
            if message.from_client.HasField("late"):
                stats.late = message.from_client.late
            if message.from_client.HasField("lost"):
                stats.lost = message.from_client.lost
            if message.from_client.HasField("resync"):
                stats.resync = message.from_client.resync

        if message.HasField("from_server"):
            stats = user.packet_stats_from_server
            if message.from_server.HasField("good"):
                stats.good = message.from_server.good
            if message.from_server.HasField("late"):
                stats.late = message.from_server.late
            if message.from_server.HasField("lost"):
                stats.lost = message.from_server.lost
            if message.from_server.HasField("resync"):
                stats.resync = message.from_server.resync

        if message.HasField("udp_packets"):
            user.udp_packets_sent = message.udp_packets
        if message.HasField("tcp_packets"):
            user.tcp_packets_sent = message.tcp_packets

        if message.HasField("udp_ping_avg"):
            user.udp_ping_avg = message.udp_ping_avg
        if message.HasField("udp_ping_var"):
            user.udp_ping_var = message.udp_ping_var
        if message.HasField("tcp_ping_avg"):
            user.tcp_ping_avg = message.tcp_ping_avg
        if message.HasField("tcp_ping_var"):
            user.tcp_ping_var = message.tcp_ping_var

        if message.HasField("onlinesecs"):
            user.online_time = message.onlinesecs
        if message.HasField("idlesecs"):
            user.idle_time = message.idlesecs

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

    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)

        if self.use_cgi:
            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 request_userstats(self, user, stats_only=False):
        self.log.debug(
            "Requesting UserStats for {}, stats_only={}", user, stats_only
        )
        user_stats = Mumble_pb2.UserStats()
        user_stats.session = user.session
        user_stats.stats_only = stats_only
        self.sendProtobuf(user_stats)

    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)