def load_plugins(self): PluginManager.load_plugins() self.non_plugins = [] self.plugins = {} self.plugin_helps = {'help': HELP_DOC} for plugin in list_plugins(): if plugin.__doc__ and plugin.COMMAND: self.plugin_helps[ plugin.COMMAND.lower()] = plugin.__doc__.strip('\n') if plugin.COMMAND: self.plugins[plugin.COMMAND.lower()] = plugin else: self.non_plugins.append(plugin)
class Environment(object): """An environment in which to run a program""" def __init__(self, spec): self.spec = spec self.basename = spec['basename'] self.files = {} self.plugin_mgr = PluginManager() # apply defaults self.spec['dir'] = self.merge_dicts(self.spec['dir'], { 'create': False, 'relative': False, 'cleanup': False, 'mode': 448 # 0700 }) # set up environment directory self.dir = os.path.expanduser(self.spec['dir']['path']) if self.spec['dir']['create']: try: os.mkdir(self.dir, self.spec['dir']['mode']) except OSError as e: if e.errno != errno.EEXIST: raise # setup files self.init_files() def __enter__(self): return self def __exit__(self, type, value, traceback): self.clean_up() def __getitem__(self, key): return self.files[key] def init_files(self): """Initialise files""" # initialise files, loading any config files first to ensure that any basename etc changes have applied configs = list(filter(lambda i: 'type' in self.spec['files'][i] and self.spec['files'][i]['type'] == 'config', self.spec['files'])) others = list(filter(lambda i: i not in configs, self.spec['files'])) for name in (configs + others): fspec = self.spec['files'][name] # apply defaults d = { 'type': 'raw', # raw file 'read': False, # don't read 'create': False, # don't create 'cleanup': False, # don't clean up 'mode': 448, # 0700 'rel_to': 'dir' # relative to environment directory } fspec = self.merge_dicts(fspec, d) # if we didn't get a name, use the name of the dictionary if 'name' not in fspec: fspec['name'] = name # substitute basename if self.basename: fspec['name'] = fspec['name'].format(basename=self.basename) # if this file doesn't have a path specified, use the name if 'path' not in fspec: # otherwise we use the 'path' field provided fspec['path'] = fspec['name'] # if environment var exists in this fspec then use it to override the path if 'var' in fspec and fspec['var'] in os.environ: fspec['path'] = os.environ[fspec['var']] else: # otherwise update the path based on the 'rel_to' field if fspec['rel_to'] == 'pkg' and 'pkg' in fspec: # relative to a given package, and package is specified fspec['path'] = pkg_resources.resource_filename(fspec['pkg'], fspec['path']) elif fspec['rel_to'] == 'pwd' or fspec['rel_to'] == 'cwd': # relative to the current working directory fspec['path'] = os.path.join(os.getcwd(), fspec['path']) elif fspec['rel_to'] == 'abs': # absolute path has been provided, don't update it pass else: # relative to the environment directory (the default) fspec['path'] = os.path.join(self.dir, fspec['path']) # store updated spec self.spec['files'][name] = fspec # create file if 'create' in fspec and fspec['create']: if fspec['type'] == 'plugin_dir': # create the plugin directory if it doesn't exist if not os.path.exists(fspec['path']): os.mkdir(fspec['path']) elif not os.path.exists(fspec['path']): # otherwise if it's a normal file of some kind and doesn't exist, create it fd = os.open(fspec['path'], os.O_WRONLY | os.O_CREAT, fspec['mode']) f = os.fdopen(fd, 'w') f.close() # load file if 'read' in fspec and fspec['read']: if fspec['type'] == 'config': # load as a config file self.files[name] = self.load_config(fspec) # if there was a basename variable specified in the config, grab the contents of it if 'basename_variable' in self.files[name] and self.files[name]['basename_variable'] in os.environ: bn = os.environ[self.files[name]['basename_variable']].replace("/", '') if len(bn) > 0: if len(bn) > MAX_BASENAME: bn = bn[-MAX_BASENAME:] self.basename = bn elif fspec['type'] == 'json': # load as a json file self.files[name] = self.load_json(fspec) elif fspec['type'] == 'raw': # load as raw file self.files[name] = self.load_raw(fspec) elif fspec['type'] == 'plugin_dir': # this is a plugin directory, ignore it here as we'll load it below pass else: # no idea, just load it as raw self.files[name] = self.load_raw(fspec) else: self.files[name] = fspec['path'] # load plugins if fspec['type'] == 'plugin_dir': self.load_plugins(fspec) def load_config(self, spec): """Load a JSON configuration file""" # load default config config = {} if 'default' in spec: # find where the default configuration is if spec['default']['rel_to'] == 'pkg': path = pkg_resources.resource_filename(spec['default']['pkg'], spec['default']['path']) elif spec['default']['rel_to'] == 'pwd': path = os.path.join(os.getcwd(), spec['default']['path']) elif spec['default']['rel_to'] == 'abs': path = spec['default']['path'] # load it try: config = self.parse_json(open(path).read()) except ValueError as e: raise IOError("Error parsing default configuration" + e.message) # load local config try: local_config = self.parse_json(open(spec['path']).read()) config = self.merge_dicts(local_config, config) except ValueError as e: raise ValueError("Error parsing local configuration: " + e.message) except IOError: pass return config def load_json(self, spec): """Load a JSON file""" return self.parse_json(file(spec['path']).read()) def load_raw(self, spec): """Load a raw text file""" return file(spec['path']).read() def load_plugins(self, spec): """Load plugins""" self.plugin_mgr.load_plugins(spec['path']) def parse_json(self, config): """Parse a JSON file""" lines = filter(lambda x: len(x) != 0 and x.strip()[0] != '#', config.split('\n')) return json.loads('\n'.join(lines)) def read_file(self, name): """Read a file within the environment""" # get a file spec spec = self.file_spec(name) # read file return file(spec['path']).read() def write_file(self, name, data): """Read a file within the environment""" # open file spec = self.file_spec(name) f = open(spec['path'], 'w+') # truncate the file if we're not appending if not 'append' in spec or 'append' in spec and not spec['append']: f.truncate() # write file f.write(data) f.close() def path_to(self, filename): """Return the path to a file within the environment""" return os.path.join(self.dir, filename) def file_spec(self, name): """Return a file spec for the specified name""" if name in self.spec['files']: spec = self.spec['files'][name] else: spec = {'name': name, 'type': 'raw', 'path':self.path_to(name)} return spec def clean_up(self): """Clean up the environment""" # remove any files that need to be cleaned up for name in self.spec['files']: fspec = self.spec['files'][name] if 'cleanup' in fspec and fspec['cleanup']: os.unlink(self.path_to(name)) # remove the directory if necessary if self.spec['dir']['cleanup']: os.rmdir(self.dir) def merge_dicts(self, d1, d2): """Merge two dictionaries""" # recursive merge where items in d2 override those in d1 for k1,v1 in d1.items(): if isinstance(v1, dict) and k1 in d2.keys() and isinstance(d2[k1], dict): self.merge_dicts(v1, d2[k1]) else: d2[k1] = v1 return d2 @property def plugins(self): return self.plugin_mgr.plugins
class Bot(Daemon): def __init__(self, config): super().__init__() self.config = config self.plugin_manager = PluginManager(self, os.path.join(self.config['home_path'], self.config['plugin_path'])) self.plugin_manager.load_plugins() self.builtin_commands = { 'quit': self._quit, 'join': self._join, 'part': self._part, 'loaded': self._plugin_loaded, 'unload': self._plugin_unload, 'load': self._plugin_load, 'reload': self._plugin_reload, 'ping': self._ping, 'help': self._command_help } self.events = { 'send_privmsg': self._send_privmsg_event, 'privmsg': self._privmsg_event, 'stop': self._stop_event, 'pong': self._pong_event, 'users': self._qnull_event, 'part': self._part_event, 'recv': self._qnull_event, 'send': self._qnull_event, 'server_ping': self._qnull_event, 'join': self._qnull_event } self.channels = {} def init_loop(self): logger.info("Bot started.") def cleanup_loop(self): self.event_handler(("stop", ())) def do(self): #TODO: Event driven framework? time.sleep(0.01) def event_handler(self, event): event_type, data = event # TODO: Pool? try: for _, plugin in self.plugin_manager.plugins.items(): try: attr = getattr(plugin, "{}_hook".format(event_type)) except AttributeError: attr = None if attr is not None: attr_thread = threading.Thread(target=attr, args=event) attr_thread.start() except RuntimeError as e: logger.debug("Skipped plugin hooks. {}".format(e)) self.events.get(event_type, self._null_event)(event) # Built-in commands @staticmethod def _null_command(data): logger.debug("Unknown command: {}".format(data)) def _quit(self, data): message = data[0] sender = data[1] destination = data[2] if re.search(self.config['admin_pass'], message): self.send_event("stop", message) self.stop() else: self.send_event("send_response", "F**k You!", sender) def _ping(self, data): self.send_event("ping", data) def _join(self, data): self.send_event("join", data) def _part(self, data): self.send_event("part", data) def _plugin_loaded(self, data): if data[2][0] == '#': destination = data[2] else: destination = data[1] self.send_event("send_response", "Loaded plugins: {}".format([i for i in self.plugin_manager.plugins.keys()]), destination) def _plugin_reload(self, data): try: plugin_name = data[0].split(" ")[2] except IndexError: plugin_name = None if plugin_name in self.plugin_manager.plugins: self._plugin_unload(data) self._plugin_load(data) def _plugin_unload(self, data): try: plugin_name = data[0].split(" ")[2] except IndexError: return if data[2][0] == '#': destination = data[2] else: destination = data[1] if plugin_name == "all": self.plugin_manager.unload_plugins() self.send_event("send_response", "All plugins unloaded.", destination) elif plugin_name in self.plugin_manager.plugins: self.plugin_manager.unload_plugin(plugin_name) self.send_event("send_response", "Plugin {} unloaded.".format(plugin_name), destination) def _plugin_load(self, data): try: plugin_name = data[0].split(" ")[2] except IndexError: return if data[2][0] == '#': destination = data[2] else: destination = data[1] if plugin_name == "all": self.plugin_manager.load_plugins() self.send_event("send_response", "All plugins loaded: {}".format([i for i in self.plugin_manager.plugins.keys()]), destination) else: self.plugin_manager.load_plugin(plugin_name) self.send_event("send_response", "Plugin {} loaded.".format(plugin_name), destination) def _command_help(self, data): if data[2][0] == '#': destination = data[2] else: destination = data[1] cmds = [i for i in self.builtin_commands] self.send_event("send_response", cmds, destination) cmds = [i for i in self.plugin_manager.commands] self.send_event("send_response", cmds, destination) # Events def _privmsg_event(self, event): event_type, data = event message = data[0] sender = data[1] destination = data[2] tokens = message.split(" ") prefix, command, message = None, None, None if len(tokens) == 1: command = tokens[0] elif len(tokens) == 2: command, message = tokens elif len(tokens) >= 3: prefix = tokens[0] if not re.search("{}[:,]?".format(self.config['name']), prefix): command, *message = tokens else: command, *message = tokens[1:] if destination[0] == '#' and prefix is None: command, message = message, "" prefix = self.config['name'] # TODO: Proper authentication if command is not None: if sender in self.config['authorized_users']: if prefix is None and destination != self.config['name']: return if command in self.builtin_commands: logger.debug("CMD | {}: {}".format(command, message)) self.builtin_commands.get(command, self._null_command)(data) elif command in self.plugin_manager.commands: logger.debug("CMD | {}: {}".format(command, message)) # TODO: threading plugin_thread = threading.Thread(target=self.plugin_manager.commands.get(command, self._null_command), args=data) logger.log(level=5, msg=plugin_thread) plugin_thread.start() else: self._null_command(command) def _stop_event(self, event): self.stop() def _pong_event(self, event): event_type, data = event try: if data[2][0] == '#': destination = data[2] else: destination = data[1] except IndexError: destination = data[1] logger.info("Ping time: {}".format(data[0])) self.send_event("send_response", "Ping time: {}".format(data[0]), destination) def _part_event(self, event): event_type, data = event # self.channels.pop(data[0]) def _send_privmsg_event(self, event): pass