class LazySusan(object): update_checked = False @staticmethod def _get_config(section): config = ConfigParser() if "APPDATA" in os.environ: # Windows os_config_path = os.environ["APPDATA"] elif "XDG_CONFIG_HOME" in os.environ: # Modern Linux os_config_path = os.environ["XDG_CONFIG_HOME"] elif "HOME" in os.environ: # Legacy Linux os_config_path = os.path.join(os.environ["HOME"], ".config") else: os_config_path = None locations = ["lazysusan.ini"] if os_config_path is not None: locations.insert(0, os.path.join(os_config_path, "lazysusan.ini")) if not config.read(locations): raise LazySusanException("No lazysusan.ini found.") if not config.has_section(section) and section != "DEFAULT": raise LazySusanException("No section `{0}` found in lazysusan.ini.".format(section)) return dict(config.items(section)) def __init__(self, config_section, plugin_dir, enable_logging): if not self.update_checked: update_check(__name__, __version__) self.update_checked = True self.start_time = datetime.utcnow() if plugin_dir: if os.path.isdir(plugin_dir): sys.path.append(plugin_dir) else: print ("`{0}` is not a directory.".format(plugin_dir)) config = self._get_config(config_section) self._delayed_events = [] self._loaded_plugins = {} self.api = Bot(config["auth_id"], config["user_id"], rate_limit=0.575) self.api.debug = enable_logging self.api.on("add_dj", self.handle_add_dj) self.api.on("booted_user", self.handle_booted_user) self.api.on("deregistered", self.handle_user_leave) self.api.on("new_moderator", self.handle_add_moderator) self.api.on("post_message", self.run_delayed_events) self.api.on("pmmed", self.handle_pm) self.api.on("ready", self.handle_ready) self.api.on("registered", self.handle_user_join) self.api.on("rem_dj", self.handle_remove_dj) self.api.on("rem_moderator", self.handle_remove_moderator) self.api.on("roomChanged", self.handle_room_change) self.api.on("speak", self.handle_room_message) self.bot_id = config["user_id"] self.commands = { "/about": self.cmd_about, "/commands": self.cmd_commands, "/help": self.cmd_help, "/join": self.cmd_join, "/leave": self.cmd_leave, "/pgload": self.cmd_plugin_load, "/pgreload": self.cmd_plugin_reload, "/pgunload": self.cmd_plugin_unload, "/plugins": self.cmd_plugins, "/uptime": self.cmd_uptime, } self.config = config self.dj_ids = set() self.listener_ids = set() self.max_djs = None self.moderator_ids = set() self.username = None # Load plugins after everything has been initialized for plugin in config["plugins"].split("\n"): self.load_plugin(plugin) self.api.connect(config["room_id"]) self.api.ws.on_error = handle_error def _load_command_plugin(self, plugin): to_add = {} for command, func_name in plugin.COMMANDS.items(): if command in self.commands: other = self.commands[command] if isinstance(other.im_self, CommandPlugin): print ( "`{0}` conflicts with `{1}` for command `{2}`.".format(plugin.NAME, other.im_self.NAME, command) ) else: print ("`{0}` cannot use the reserved command `{1}`.".format(plugin.NAME, command)) print ("Not loading plugin `{0}`.".format(plugin.NAME)) return False to_add[command] = getattr(plugin, func_name) self.commands.update(to_add) return True def _unload_command_plugin(self, plugin): for command in plugin.COMMANDS: del self.commands[command] @no_arg_command def cmd_about(self, data): """Display information about this bot.""" reply = "I am powered by LazySusan version {0}. " "https://github.com/bboe/LazySusan".format(__version__) self.reply(reply, data) @no_arg_command def cmd_commands(self, data): """List the available commands.""" admin_cmds = [] admin_or_moderator_cmds = [] moderator_cmds = [] no_priv_cmds = [] for command, func in self.commands.items(): if func.func_dict.get("admin_required"): admin_cmds.append(command) elif func.func_dict.get("admin_or_moderator_required"): admin_or_moderator_cmds.append(command) elif func.func_dict.get("moderator_required"): moderator_cmds.append(command) else: no_priv_cmds.append(command) reply = "Available commands: " reply += ", ".join(sorted(no_priv_cmds)) self.reply(reply, data) user_id = get_sender_id(data) if moderator_cmds and self.is_moderator(user_id): reply = "Moderator commands: " reply += ", ".join(sorted(moderator_cmds)) self.api.pm(reply, user_id) if admin_or_moderator_cmds and (self.is_moderator(user_id) or self.is_admin(user_id)): reply = "Priviliged commands: " reply += ", ".join(sorted(admin_or_moderator_cmds)) self.api.pm(reply, user_id) if admin_cmds and self.is_admin(user_id): reply = "Admin commands: " reply += ", ".join(sorted(admin_cmds)) self.api.pm(reply, user_id) def _connect(self, room_id, when_connected=True): if self.api.roomId == room_id or (self.api.roomId and not when_connected): return print ("Joining {0}".format(room_id)) self.api.roomRegister(room_id) def cmd_help(self, message, data): """With no arguments, display this message. Otherwise, display the help for the given command. Type /commands to see the list of commands.""" def docstr(item): lines = [] for line in item.__doc__.split("\n"): line = line.strip() if line: lines.append(line) return " ".join(lines) if not message: reply = docstr(self.cmd_help) elif " " not in message: if message in self.commands: tmp = self.commands[message].func_dict if ( tmp.get("admin_required") and not self.is_admin(data) or tmp.get("moderator_required") and not self.is_moderator(data) ): return reply = docstr(self.commands[message]) else: reply = "`{0}` is not a valid command.".format(message) else: return self.reply(reply, data) @admin_required def cmd_join(self, message, data): """Join the room by room_id. With no arguments, join the room specified in lazysusan.ini.""" if " " in message: return room_id = message if message else self.config["room_id"] if room_id == self.api.roomId: self.reply("I am already in that room.", data) else: self._connect(room_id) @admin_required @no_arg_command def cmd_leave(self, data): """Leave the current room and remain connected to the chat server.""" def callback(cb_data): user_id = get_sender_id(data) if cb_data["success"]: # Schedule an event to possibly rejoin after 1 minute self.schedule(60, self._connect, self.config["room_id"], False) self.api.pm( "I have left the room. If I remain roomless after " "~1 minute, I will rejoin the default room.", user_id, ) else: self.api.pm("Leaving the room failed.", user_id) print ("Leaving {0}".format(self.api.roomId)) self.api.roomDeregister(callback) @admin_required @single_arg_command def cmd_plugin_load(self, message, data): """Load the specified plugin.""" if message in self._loaded_plugins: reply = "Plugin `{0}` is already loaded.".format(message) elif self.load_plugin(message, attempt_reload=True): reply = "Plugin `{0}` loaded.".format(message) else: reply = "Plugin `{0}` could not be loaded.".format(message) self.reply(reply, data) @admin_required @single_arg_command def cmd_plugin_reload(self, message, data): """Reoad the specified plugin.""" if message not in self._loaded_plugins: reply = "Plugin `{0}` is not loaded.".format(message) elif not (self.unload_plugin(message) and self.load_plugin(message, attempt_reload=True)): reply = "Plugin `{0}` could not be reloaded.".format(message) else: reply = "Plugin `{0}` reloaded.".format(message) self.reply(reply, data) @admin_required @single_arg_command def cmd_plugin_unload(self, message, data): """Unload the specified plugin.""" if message not in self._loaded_plugins: reply = "Plugin `{0}` is not loaded.".format(message) elif self.unload_plugin(message): reply = "Plugin `{0}` unloaded.".format(message) else: reply = "Plugin `{0}` could not be unloaded.".format(message) self.reply(reply, data) @admin_required @no_arg_command def cmd_plugins(self, data): """Display the list of loaded plugins.""" reply = "Loaded plugins: " reply += ", ".join(sorted(self._loaded_plugins.keys())) self.reply(reply, data) @no_arg_command def cmd_uptime(self, data): """Display how long since LazySusan was started.""" msg = "LazySusan was started {0}".format(pretty_date(self.start_time)) self.reply(msg, data) def is_admin(self, item): """item can be either the user_id, or a dictionary from a message.""" if isinstance(item, dict): item = get_sender_id(item) return item in self.config["admin_ids"] def is_moderator(self, item): """item can be either the user_id, or a dictionary from a message.""" if isinstance(item, dict): item = get_sender_id(item) return item in self.moderator_ids def handle_add_dj(self, data): for user in data["user"]: self.dj_ids.add(user["userid"]) def handle_booted_user(self, data): if data["userid"] == self.bot_id: # Try to rejoin the default room after 30 seconds. self.api.roomId = None self.schedule(30, self._connect, self.config["room_id"], False) def handle_add_moderator(self, data): self.moderator_ids.add(data["userid"]) def handle_pm(self, data): self.process_message(data) def handle_ready(self, _): self.api.userInfo(self.set_username) @display_exceptions def handle_remove_dj(self, data): for user in data["user"]: self.dj_ids.remove(user["userid"]) @display_exceptions def handle_remove_moderator(self, data): self.moderator_ids.remove(data["userid"]) def handle_room_change(self, data): if not data["success"]: if data["errno"] == 3: print ("You are banned from that room. Retrying in 3 minutes.") self.schedule(180, self._connect, self.config["room_id"], False) return print ("Error changing rooms.") # Try to rejoin the default room self.api.roomId = None self._connect(self.config["room_id"]) return self.dj_ids = set(data["room"]["metadata"]["djs"]) self.listener_ids = set(x["userid"] for x in data["users"]) self.max_djs = data["room"]["metadata"]["max_djs"] self.moderator_ids = set(data["room"]["metadata"]["moderator_id"]) @display_exceptions def handle_room_message(self, data): if self.username and self.username != data["name"]: self.process_message(data) def handle_user_join(self, data): for user in data["user"]: self.listener_ids.add(user["userid"]) @display_exceptions def handle_user_leave(self, data): for user in data["user"]: self.listener_ids.remove(user["userid"]) def load_plugin(self, plugin_name, attempt_reload=False): parts = plugin_name.split(".") if len(parts) > 1: module_name = ".".join(parts[:-1]) class_name = parts[-1] else: # Use the titlecase format of the module name as the class name module_name = parts[0] class_name = parts[0].title() # First try to load plugins from the passed in plugins_dir and then # from the lazysusan.plugins package. module = None for package in (None, "lazysusan.plugins"): if package: module_name = "{0}.{1}".format(package, module_name) if attempt_reload and module_name in sys.modules: module = reload(sys.modules[module_name]) else: try: module = __import__(module_name, fromlist=[class_name]) except ImportError: pass if module: break if not module: print ("Cannot find plugin `{0}`.".format(plugin_name)) return False try: plugin = getattr(module, class_name)(self) except AttributeError: print ("Cannot find plugin `{0}`.".format(plugin_name)) return False plugin.__class__.NAME = plugin_name if isinstance(plugin, CommandPlugin): if not self._load_command_plugin(plugin): return self._loaded_plugins[plugin_name] = plugin print ("Loaded plugin `{0}`.".format(plugin_name)) return True def process_message(self, data): parts = data["text"].split() if not parts: return command = parts[0] if len(parts) == 1: message = "" else: message = " ".join(parts[1:]) # Normalize with single spaces handler = self.commands.get(command) if not handler: return handler(message, data) def reply(self, message, data): if data["command"] == "speak": self.api.speak(message) elif data["command"] == "pmmed": self.api.pm(message, data["senderid"]) else: raise Exception("Unrecognized command type `{0}`".format(data["command"])) def run_delayed_events(self, _): now = time.time() process = True while process and self._delayed_events: item = self._delayed_events[0] # Peek at the top if item[0] < now: heapq.heappop(self._delayed_events) # Actually remove item[1](*item[2], **item[3]) else: process = False def schedule(self, min_delay, callback, *args, **kwargs): """Schedule an event to occur at least min_delay seconds in the future. The passed in callback function will be called with all remaining arguments. Scheduled events are checked and processed after every received message from turntable. In an inactive room the longest duration between received messages is 12 seconds.""" schedule_time = time.time() + min_delay heapq.heappush(self._delayed_events, (schedule_time, callback, args, kwargs)) def set_username(self, data): self.username = data["name"] def start(self): self.api.start() def unload_plugin(self, plugin_name): if plugin_name not in self._loaded_plugins: return False plugin = self._loaded_plugins[plugin_name] if isinstance(plugin, CommandPlugin): self._unload_command_plugin(plugin) del self._loaded_plugins[plugin_name] del plugin print ("Unloaded plugin `{0}`.".format(plugin_name)) return True
class LazySusan(object): update_checked = False @staticmethod def _get_config(section): config = ConfigParser() if 'APPDATA' in os.environ: # Windows os_config_path = os.environ['APPDATA'] elif 'XDG_CONFIG_HOME' in os.environ: # Modern Linux os_config_path = os.environ['XDG_CONFIG_HOME'] elif 'HOME' in os.environ: # Legacy Linux os_config_path = os.path.join(os.environ['HOME'], '.config') else: os_config_path = None locations = ['lazysusan.ini'] if os_config_path is not None: locations.insert(0, os.path.join(os_config_path, 'lazysusan.ini')) if not config.read(locations): raise LazySusanException('No lazysusan.ini found.') if not config.has_section(section) and section != 'DEFAULT': raise LazySusanException('No section `{0}` found in lazysusan.ini.' .format(section)) return dict(config.items(section)) def __init__(self, config_section, plugin_dir, enable_logging): if not self.update_checked: update_check(__name__, __version__) self.update_checked = True self.start_time = datetime.utcnow() if plugin_dir: if os.path.isdir(plugin_dir): sys.path.append(plugin_dir) else: print('`{0}` is not a directory.'.format(plugin_dir)) config = self._get_config(config_section) self._delayed_events = [] self._loaded_plugins = {} self.api = Bot(config['auth_id'], config['user_id'], rate_limit=0.575) self.api.debug = enable_logging self.api.on('add_dj', self.handle_add_dj) self.api.on('booted_user', self.handle_booted_user) self.api.on('deregistered', self.handle_user_leave) self.api.on('new_moderator', self.handle_add_moderator) self.api.on('post_message', self.run_delayed_events) self.api.on('pmmed', self.handle_pm) self.api.on('ready', self.handle_ready) self.api.on('registered', self.handle_user_join) self.api.on('rem_dj', self.handle_remove_dj) self.api.on('rem_moderator', self.handle_remove_moderator) self.api.on('roomChanged', self.handle_room_change) self.api.on('speak', self.handle_room_message) self.bot_id = config['user_id'] self.commands = {'/about': self.cmd_about, '/commands': self.cmd_commands, '/help': self.cmd_help, '/join': self.cmd_join, '/leave': self.cmd_leave, '/pgload': self.cmd_plugin_load, '/pgreload': self.cmd_plugin_reload, '/pgunload': self.cmd_plugin_unload, '/plugins': self.cmd_plugins, '/uptime': self.cmd_uptime} self.config = config self.dj_ids = set() self.listener_ids = set() self.max_djs = None self.moderator_ids = set() self.username = None # Load plugins after everything has been initialized for plugin in config['plugins'].split('\n'): self.load_plugin(plugin) self.api.connect(config['room_id']) self.api.ws.on_error = handle_error def _load_command_plugin(self, plugin): to_add = {} for command, func_name in plugin.COMMANDS.items(): if command in self.commands: other = self.commands[command] if isinstance(other.im_self, CommandPlugin): print('`{0}` conflicts with `{1}` for command `{2}`.' .format(plugin.NAME, other.im_self.NAME, command)) else: print('`{0}` cannot use the reserved command `{1}`.' .format(plugin.NAME, command)) print('Not loading plugin `{0}`.'.format(plugin.NAME)) return False to_add[command] = getattr(plugin, func_name) self.commands.update(to_add) return True def _unload_command_plugin(self, plugin): for command in plugin.COMMANDS: del self.commands[command] @no_arg_command def cmd_about(self, data): """Display information about this bot.""" reply = ('''I am powered by LazySusan version {0}. ' 'https://github.com/bboe/LazySusan'.format(__version__) 'Questions/Comments can be directed to ##TTT on IRC.Freenode.net http://webchat.freenode.net/?randomnick=1&channels=##ttt&uio=d4''' ) self.reply(reply, data) @no_arg_command def cmd_commands(self, data): """List the available commands.""" admin_cmds = [] admin_or_moderator_cmds = [] moderator_cmds = [] no_priv_cmds = [] for command, func in self.commands.items(): if func.func_dict.get('admin_required'): admin_cmds.append(command) elif func.func_dict.get('admin_or_moderator_required'): admin_or_moderator_cmds.append(command) elif func.func_dict.get('moderator_required'): moderator_cmds.append(command) else: no_priv_cmds.append(command) reply = 'Available commands: ' reply += ', '.join(sorted(no_priv_cmds)) self.reply(reply, data) user_id = get_sender_id(data) if moderator_cmds and self.is_moderator(user_id): reply = 'Moderator commands: ' reply += ', '.join(sorted(moderator_cmds)) self.api.pm(reply, user_id) if admin_or_moderator_cmds and (self.is_moderator(user_id) or self.is_admin(user_id)): reply = 'Priviliged commands: ' reply += ', '.join(sorted(admin_or_moderator_cmds)) self.api.pm(reply, user_id) if admin_cmds and self.is_admin(user_id): reply = 'Admin commands: ' reply += ', '.join(sorted(admin_cmds)) self.api.pm(reply, user_id) def _connect(self, room_id, when_connected=True): if self.api.roomId == room_id or (self.api.roomId and not when_connected): return print('Joining {0}'.format(room_id)) self.api.roomRegister(room_id) def cmd_help(self, message, data): """With no arguments, display this message. Otherwise, display the help for the given command. Type /commands to see the list of commands.""" def docstr(item): lines = [] for line in item.__doc__.split('\n'): line = line.strip() if line: lines.append(line) return ' '.join(lines) if not message: reply = docstr(self.cmd_help) elif ' ' not in message: if message in self.commands: tmp = self.commands[message].func_dict if tmp.get('admin_required') and not self.is_admin(data) or \ tmp.get('moderator_required') and \ not self.is_moderator(data): return reply = docstr(self.commands[message]) else: reply = '`{0}` is not a valid command.'.format(message) else: return self.reply(reply, data) @admin_required def cmd_join(self, message, data): """Join the room by room_id. With no arguments, join the room specified in lazysusan.ini.""" if ' ' in message: return room_id = message if message else self.config['room_id'] if room_id == self.api.roomId: self.reply('I am already in that room.', data) else: self._connect(room_id) @admin_required @no_arg_command def cmd_leave(self, data): """Leave the current room and remain connected to the chat server.""" def callback(cb_data): user_id = get_sender_id(data) if cb_data['success']: # Schedule an event to possibly rejoin after 1 minute self.schedule(60, self._connect, self.config['room_id'], False) self.api.pm('I have left the room. If I remain roomless after ' '~1 minute, I will rejoin the default room.', user_id) else: self.api.pm('Leaving the room failed.', user_id) print('Leaving {0}'.format(self.api.roomId)) self.api.roomDeregister(callback) @admin_required @single_arg_command def cmd_plugin_load(self, message, data): """Load the specified plugin.""" if message in self._loaded_plugins: reply = 'Plugin `{0}` is already loaded.'.format(message) elif self.load_plugin(message, attempt_reload=True): reply = 'Plugin `{0}` loaded.'.format(message) else: reply = 'Plugin `{0}` could not be loaded.'.format(message) self.reply(reply, data) @admin_required @single_arg_command def cmd_plugin_reload(self, message, data): """Reoad the specified plugin.""" if message not in self._loaded_plugins: reply = 'Plugin `{0}` is not loaded.'.format(message) elif not (self.unload_plugin(message) and self.load_plugin(message, attempt_reload=True)): reply = 'Plugin `{0}` could not be reloaded.'.format(message) else: reply = 'Plugin `{0}` reloaded.'.format(message) self.reply(reply, data) @admin_required @single_arg_command def cmd_plugin_unload(self, message, data): """Unload the specified plugin.""" if message not in self._loaded_plugins: reply = 'Plugin `{0}` is not loaded.'.format(message) elif self.unload_plugin(message): reply = 'Plugin `{0}` unloaded.'.format(message) else: reply = 'Plugin `{0}` could not be unloaded.'.format(message) self.reply(reply, data) @admin_required @no_arg_command def cmd_plugins(self, data): """Display the list of loaded plugins.""" reply = 'Loaded plugins: ' reply += ', '.join(sorted(self._loaded_plugins.keys())) self.reply(reply, data) @no_arg_command def cmd_uptime(self, data): """Display how long since LazySusan was started.""" msg = 'LazySusan was started {0}'.format(pretty_date(self.start_time)) self.reply(msg, data) def is_admin(self, item): """item can be either the user_id, or a dictionary from a message.""" if isinstance(item, dict): item = get_sender_id(item) return item in self.config['admin_ids'] def is_moderator(self, item): """item can be either the user_id, or a dictionary from a message.""" if isinstance(item, dict): item = get_sender_id(item) return item in self.moderator_ids def handle_add_dj(self, data): for user in data['user']: self.dj_ids.add(user['userid']) def handle_booted_user(self, data): if data['userid'] == self.bot_id: # Try to rejoin the default room after 30 seconds. self.api.roomId = None self.schedule(30, self._connect, self.config['room_id'], False) def handle_add_moderator(self, data): self.moderator_ids.add(data['userid']) def handle_pm(self, data): self.process_message(data) def handle_ready(self, _): self.api.userInfo(self.set_username) @display_exceptions def handle_remove_dj(self, data): for user in data['user']: self.dj_ids.remove(user['userid']) @display_exceptions def handle_remove_moderator(self, data): self.moderator_ids.remove(data['userid']) def handle_room_change(self, data): if not data['success']: if data['errno'] == 3: print('You are banned from that room. Retrying in 3 minutes.') self.schedule(180, self._connect, self.config['room_id'], False) return print('Error changing rooms.') # Try to rejoin the default room self.api.roomId = None self._connect(self.config['room_id']) return self.dj_ids = set(data['room']['metadata']['djs']) self.listener_ids = set(x['userid'] for x in data['users']) self.max_djs = data['room']['metadata']['max_djs'] self.moderator_ids = set(data['room']['metadata']['moderator_id']) @display_exceptions def handle_room_message(self, data): if self.username and self.username != data['name']: self.process_message(data) def handle_user_join(self, data): for user in data['user']: self.listener_ids.add(user['userid']) @display_exceptions def handle_user_leave(self, data): for user in data['user']: self.listener_ids.remove(user['userid']) def load_plugin(self, plugin_name, attempt_reload=False): parts = plugin_name.split('.') if len(parts) > 1: module_name = '.'.join(parts[:-1]) class_name = parts[-1] else: # Use the titlecase format of the module name as the class name module_name = parts[0] class_name = parts[0].title() # First try to load plugins from the passed in plugins_dir and then # from the lazysusan.plugins package. module = None for package in (None, 'lazysusan.plugins'): if package: module_name = '{0}.{1}'.format(package, module_name) if attempt_reload and module_name in sys.modules: module = reload(sys.modules[module_name]) else: try: module = __import__(module_name, fromlist=[class_name]) except ImportError: pass if module: break if not module: print('Cannot find plugin `{0}`.'.format(plugin_name)) return False try: plugin = getattr(module, class_name)(self) except AttributeError: print('Cannot find plugin `{0}`.'.format(plugin_name)) return False plugin.__class__.NAME = plugin_name if isinstance(plugin, CommandPlugin): if not self._load_command_plugin(plugin): return self._loaded_plugins[plugin_name] = plugin print('Loaded plugin `{0}`.'.format(plugin_name)) return True def process_message(self, data): parts = data['text'].split() if not parts: return command = parts[0] if len(parts) == 1: message = '' else: message = ' '.join(parts[1:]) # Normalize with single spaces handler = self.commands.get(command) if not handler: return handler(message, data) def reply(self, message, data): if data['command'] == 'speak': self.api.speak(message) elif data['command'] == 'pmmed': self.api.pm(message, data['senderid']) else: raise Exception('Unrecognized command type `{0}`' .format(data['command'])) def run_delayed_events(self, _): now = time.time() process = True while process and self._delayed_events: item = self._delayed_events[0] # Peek at the top if item[0] < now: heapq.heappop(self._delayed_events) # Actually remove item[1](*item[2], **item[3]) else: process = False def schedule(self, min_delay, callback, *args, **kwargs): """Schedule an event to occur at least min_delay seconds in the future. The passed in callback function will be called with all remaining arguments. Scheduled events are checked and processed after every received message from turntable. In an inactive room the longest duration between received messages is 12 seconds.""" schedule_time = time.time() + min_delay heapq.heappush(self._delayed_events, (schedule_time, callback, args, kwargs)) def set_username(self, data): self.username = data['name'] def start(self): self.api.start() def unload_plugin(self, plugin_name): if plugin_name not in self._loaded_plugins: return False plugin = self._loaded_plugins[plugin_name] if isinstance(plugin, CommandPlugin): self._unload_command_plugin(plugin) del self._loaded_plugins[plugin_name] del plugin print('Unloaded plugin `{0}`.'.format(plugin_name)) return True