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