class Bot(Plugin): """The IRC bot. Handles plugins, command dispatch, hook dispatch, etc. Persistent across losing and regaining connection. """ #: Default configuration values CONFIG_DEFAULTS = { 'nickname': 'csyorkbot', 'password': None, 'username': '******', 'realname': 'cs-york bot', 'sourceURL': 'http://github.com/csyork/csbot/', 'lineRate': '1', 'irc_host': 'irc.freenode.net', 'irc_port': '6667', 'command_prefix': '!', 'channels': ' '.join([ '#cs-york-dev', ]), 'plugins': ' '.join([ 'example', ]), 'mongodb_uri': 'mongodb://localhost:27017', 'mongodb_prefix': 'csbot__', } #: Environment variable fallbacks CONFIG_ENVVARS = { 'password': ['IRC_PASS'], 'mongodb_uri': ['MONGOLAB_URI', 'MONGODB_URI'], } #: The top-level package for all bot plugins PLUGIN_PACKAGE = 'csbot.plugins' def __init__(self, configpath): super(Bot, self).__init__(self) # Load the configuration file self.config_path = configpath self.config_root = configparser.ConfigParser(interpolation=None, allow_no_value=True) with open(self.config_path, 'r') as cfg: self.config_root.read_file(cfg) # Make mongodb connection self.log.info('connecting to mongodb: ' + self.config_get('mongodb_uri')) self.mongodb = pymongo.Connection(self.config_get('mongodb_uri')) # Plugin management self.plugins = PluginManager(self.PLUGIN_PACKAGE, Plugin, self.config_get('plugins').split(), [self], [self]) self.commands = {} # Event runner self.events = events.ImmediateEventRunner( lambda e: self.plugins.broadcast('fire_hooks', (e,))) @classmethod def plugin_name(cls): """Special plugin name that can't clash with real plugin classes. """ return '@' + super(Bot, cls).plugin_name() def setup(self): """Load plugins defined in configuration. """ super(Bot, self).setup() self.plugins.broadcast('setup', static=False) def teardown(self): """Unload plugins and save data. """ super(Bot, self).teardown() self.plugins.broadcast('teardown', static=False) def post_event(self, event): self.events.post_event(event) def register_command(self, cmd, metadata, f, tag=None): # Bail out if the command already exists if cmd in self.commands: self.log.warn('tried to overwrite command: {}'.format(cmd)) return False self.commands[cmd] = (f, metadata, tag) self.log.info('registered command: ({}, {})'.format(cmd, tag)) return True def unregister_command(self, cmd, tag=None): if cmd in self.commands: f, m, t = self.commands[cmd] if t == tag: del self.commands[cmd] self.log.info('unregistered command: ({}, {})' .format(cmd, tag)) else: self.log.error(('tried to remove command {} ' + 'with wrong tag {}').format(cmd, tag)) def unregister_commands(self, tag): delcmds = [c for c, (_, _, t) in self.commands.iteritems() if t == tag] for cmd in delcmds: f, _, tag = self.commands[cmd] del self.commands[cmd] self.log.info('unregistered command: ({}, {})'.format(cmd, tag)) @Plugin.hook('core.self.connected') def signedOn(self, event): map(event.protocol.join, self.config_get('channels').split()) @Plugin.hook('core.message.privmsg') def privmsg(self, event): """Handle commands inside PRIVMSGs.""" # See if this is a command command = CommandEvent.parse_command( event, self.config_get('command_prefix')) if command is not None: self.post_event(command) @Plugin.hook('core.command') def fire_command(self, event): """Dispatch a command event to its callback. """ # Ignore unknown commands if event['command'] not in self.commands: return f, _, _ = self.commands[event['command']] f(event) @Plugin.command('help', help=('help [command]: show help for command, or ' 'show available commands')) def show_commands(self, e): args = e.arguments() if len(args) > 0: cmd = args[0] if cmd in self.commands: f, meta, tag = self.commands[cmd] e.protocol.msg(e['reply_to'], meta.get('help', cmd + ': no help string')) else: e.protocol.msg(e['reply_to'], cmd + ': no such command') else: e.protocol.msg(e['reply_to'], ', '.join(sorted(self.commands))) @Plugin.command('plugins') def show_plugins(self, event): event.protocol.msg(event['reply_to'], 'loaded plugins: ' + ', '.join(self.plugins))