Example #1
0
class FactoidsPlugin(plugin.PluginObject):

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

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

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

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

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

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

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

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

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

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

    # region Util functions

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

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

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

    # endregion

    # region API functions to access factoids

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

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

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

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

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

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

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

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

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

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

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

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

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

    # endregion

    # region Command handlers for interacting with factoids

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

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

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

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

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

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

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

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

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

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

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

    # endregion

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

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

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

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

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

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

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

    # ## Getting "commands"

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

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

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

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

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

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

    # ## Channel "commands"

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

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

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

    # ## Global "commands"

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

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

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

    # ## Protocol-specific "commands"

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

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

    def _message_handler_delete_protocol(self, command, factoid, args, event,
                                         split):
        """
        Handle @?- factoid "command"
        :type event: MessageReceived
        """
        if factoid is None:
            event.source.respond(__("Usage: @?- <factoid>"))
            return
        d = self.delete_factoid(event.source,
                                event.target,
                                event.caller,
                                self.PROTOCOL,
                                factoid)
        d.addCallbacks(
            lambda r: event.source.respond(__("Factoid deleted")),
            lambda f: self._factoid_command_fail(event.source, f)
        )
Example #2
0
class Manager(object):
    """
    Manager for keeping track of multiple factories - one per protocol.

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

    __metaclass__ = Singleton

    #: Instance of the storage manager
    storage = None

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

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

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

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

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

        self.metrics = None

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

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

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

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

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

        self.commands.set_factory_manager(self)

        self.load_config()  # Load the configuration

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

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

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

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

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

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

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

    # Load stuff

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

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

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

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

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

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

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

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

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

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

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

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

        result = self.plugman.load_plugin(name)

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

                if result_two is not PluginState.Unloaded:
                    return result_two

                result = self.plugman.load_plugin(name)

        return result

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

        self.plugman.scan()

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

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

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

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

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

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

        * PROTOCOL_ALREADY_LOADED
        * PROTOCOL_CONFIG_NOT_EXISTS
        * PROTOCOL_LOAD_ERROR
        * PROTOCOL_LOADED
        * PROTOCOL_SETUP_ERROR

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

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

        if name in self.factories:
            return PROTOCOL_ALREADY_LOADED

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

    # Reload stuff

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

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

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

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

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

    # Unload stuff

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

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

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

        return self.plugman.unload_plugin(name)

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

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

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

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

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

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

        self.plugman.unload_plugins()

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

    # Grab stuff

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

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

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

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

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

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

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

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

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

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

        return self.plugman.get_plugin(name)

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

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

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

        if protocol in self.factories:
            del self.factories[protocol]
            return True
        return False
Example #3
0
class Manager(object):
    """
    Manager for keeping track of multiple factories - one per protocol.

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

    __metaclass__ = Singleton

    #: Instance of the storage manager
    storage = None

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

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

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

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

    #: Console handler
    console_magic = None

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

        self.metrics = None

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

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

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

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

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

        self.commands.set_factory_manager(self)

        self.load_config()  # Load the configuration

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

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

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

        self.console_magic = ConsoleMagic()

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

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

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

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

    # Load stuff

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

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

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

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

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

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

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

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

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

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

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

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

        result = self.plugman.load_plugin(name)

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

                if result_two is not PluginState.Unloaded:
                    return result_two

                result = self.plugman.load_plugin(name)

        return result

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

        self.plugman.scan()

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

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

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

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

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

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

        * PROTOCOL_ALREADY_LOADED
        * PROTOCOL_CONFIG_NOT_EXISTS
        * PROTOCOL_LOAD_ERROR
        * PROTOCOL_LOADED
        * PROTOCOL_SETUP_ERROR

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

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

        if name in self.factories:
            return PROTOCOL_ALREADY_LOADED

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

    # Reload stuff

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

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

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

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

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

    # Unload stuff

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

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

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

        return self.plugman.unload_plugin(name)

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

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

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

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

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

        self.console_magic.unwrap()

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

        self.plugman.unload_plugins()

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

    # Grab stuff

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

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

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

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

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

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

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

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

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

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

        return self.plugman.get_plugin(name)

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

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

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

        if protocol in self.factories:
            del self.factories[protocol]
            return True
        return False