class Campy(object): def __init__(self): self.client = Campfire(settings.CAMPFIRE_SUBDOMAIN, settings.CAMPFIRE_API_KEY, ssl=True) self.rooms = [] self.since_message_id = None self.plugins = [] for plugin in settings.REGISTERED_PLUGINS: path = plugin.split('.') klass = path.pop() plugin_obj = getattr(__import__('.'.join(path), globals(), locals(), [klass], -1), klass) self.plugins.append(plugin_obj()) for room in settings.CAMPFIRE_ROOMS: print "Joining %s" % room room = self.client.find_room_by_name(room) if room: self.rooms.append(room) room.join() def listen(self): def callback(message): for plugin in self.plugins: try: speaker = self.client.user(message['user_id']) if not message['body']: return if re.match('%s: help' % settings.CAMPFIRE_BOT_NAME, message['body']): try: plugin.send_help(self.client, self.client.room(message['room_id']), message, speaker) except NotImplementedError: pass else: plugin.handle_message(self.client, self.client.room(message['room_id']), message, speaker) except HTTPNotFoundException: pass def errback(message): print message # Setup all rooms first for room in self.rooms: room.listen(callback, errback, False) # Then start listening to those rooms reactor.run() def die(self): for room in self.rooms: if settings.SAY_GOODBYE: room.speak("Goodbye!") if settings.LEAVE_ON_EXIT: room.leave()
class Campy(object): def __init__(self, **kwargs): # This is the directory we were launched in, before we move to the # Campy home directory self.launchdir = os.getcwd() # In some cases, it might be useful to provide the subdomain and # an API key in the initializer (perhaps passed in as a command- # line argument?) self.subdomain = kwargs.get('subdomain', None) self.apiKey = kwargs.get('api_key', None) # When we get a message from Campfire, we're supplied with the room_id # as one of the parameters of the message. As such, we'll have a room # dictionary so we can quickly look up the appropriate room object self.rooms = {} # This is a list of all our plugins, where each is an instance of the # type campy.CampyPlugin, and the keys are the regular expressions they # are meant to match. from campy.plugins import sh, say, config, alias self.plugins = { 'sh' : sh.ShPlugin(self), 'say' : say.SayPlugin(self), 'config': config.ConfigPlugin(self), 'alias' : alias.AliasPlugin(self) } # This is the name that the campy bot will respond to. It can be # provided in the constructor, or it can be provided in the settings. # The value provided in the constructor overrides what's specified in # the settings, and it defaults to 'campy' self.name = kwargs.get('name', None) # This is the regular expression that matches our bot's name, and the # corresponding message to parse self.nameRE = None # This is our actual campfire client. It will be made when we read in # our settings. However, for now, it is None. That value is used as # a semaphore indicating whether or not this is the first read-in or # not. self.client = None self.goodbye = 'Goodbye!' self.leaveOnExit = True # Try to set up our home directory self.home = kwargs.get('home', os.path.expanduser('~/.campy')) if not os.path.exists(self.home): try: os.mkdir(self.home) except Exception as e: log.exception('Cannot make directory %s' % self.home) # Set the configuration files self.configs = [ os.path.join(self.home, 'campy.conf'), os.path.join(self.launchdir, 'campy.conf') ] self.read() def findPlugin(self, n): '''Try to locate the module with this particular name''' try: log.debug('Trying to import %s' % n) plugins = [] # If we've already loaded this particular module... try: import campy m = __import__('campy.plugins.%s' % n) m = getattr(campy, 'plugins') m = getattr(m, n) except ImportError: m = __import__(n) for name, klass in inspect.getmembers(m): try: if issubclass(klass, CampyPlugin): plugins.append(klass) except: continue return plugins except: log.exception('Could not find plugin %s' % n) return [] def updatePath(self): for path in os.listdir(self.home): if not path.startswith('.'): p = os.path.join(self.home, path) if p not in sys.path: log.debug('Appending %s to path' % p) sys.path.append(p) def reloadPlugin(self, n): '''Try to reload a given module''' try: reload(n) except Exception: log.exception('Could not reload %s' % n) def update(self, update, destination=None): if not destination: destination = self.data for key, value in update.items(): if key in destination and isinstance(destination[key], dict): self.update(value, destination[key]) else: destination[key] = value return destination def reload(self, **kwargs): '''Given a new set of configurations, make sure we're up to date''' self.updatePath() log.info(repr(kwargs)) subdomain = kwargs.get('subdomain', self.subdomain) apiKey = kwargs.get('api_key', self.apiKey) if not subdomain and not apiKey: log.critical('Need a subdomain and API key') exit(1) self.client = Campfire(subdomain, apiKey) # Now register the bot's name self.name = kwargs.get('name', self.name) or 'campy' self.nameRE = re.compile(r'\s*%s\s+(.+)\s*$' % re.escape(self.name), re.I) # Now join all the appropriate rooms for room in kwargs.get('rooms', []): if not self.joinRoom(room): log.warn('Could not find room %s' % room) else: log.debug('Joined %s' % room) def read(self, overrides={}): '''Re-read the settings, and do all the appropriate imports''' # We're going to build up a single dictionary, making sure to merge # and override all options in subsequent config files. Then, and # only then will we start to go through it. log.debug('Reading configuration files...') self.data = {} for fname in self.configs: try: with file(fname) as f: self.data.update(yaml.load(f)) except Exception as e: log.error('Could not read %s => %s' % (fname, repr(e))) self.update(overrides) # There has to at least be a campy section if 'campy' not in self.data: log.critical('No campy configuration found in config files!') exit(1) # Now go through the remaining sections for section, values in self.data.items(): if section == 'campy': if not values: values = {} self.reload(**values) continue if section == 'logger': import logging.config logging.config.dictConfig(values) continue # If it wasn't the campy section, then we should try to # import the module named in that section, and then to # instantiate each of the plugins that inherit from # plugins.CampyPlugin try: for m in self.findPlugin(section): # Alright! We found a plugin. Now, let's see # if the plugin has been instantiated, or if # it hasn't yet been encountered plugin = self.plugins.get(m.shortname) if plugin: if not values: values = {} plugin.reload(**values) else: log.debug('Storing plugin at %s' % m.shortname) values = values or {} self.plugins[m.shortname] = m(self, **values) except ImportError: log.exception('Unable to import module %s' % section) log.debug('Read configuration files...') def save(self, filename=None): # Try to save the current configuration to a file. try: # To be safe when writing, the best thing to do is to first # save a temporary copy, and then if successful, replace the # original with the temporary path = filename or os.path.join(self.home, 'campy.conf') with file(path + '.bak', 'w+') as f: f.write(yaml.dump(self.data)) # And now move the new file to replace the old file os.rename(path + '.bak', path) log.debug('Wrote to %s' % path) return 'Wrote configuration to %s' % path except Exception as e: log.exception('Campy save exception') return 'Campy save exception => %s' % repr(e) def callback(self, obj): '''Go through all the plugins and give it the appropriate message''' # The object provided looks something like this: # 'body' : Text we heard # 'user_id' : Who said it # 'created_at' : When it happened # 'room_id' : The room where it was said # 'starred' : A string representation ('true' or 'false') # 'type' : Message type (e.g. 'TextMessage') # 'id' : Message ID match = self.nameRE.match(obj.get('body', '')) if match: message = match.group(1) obj['body'] = message else: log.debug('Message "%s" did not match "%s "' % (obj.get('body', ''), self.name)) return # The room that received the message: room = self.rooms.get(obj.get('room_id', ''), None) if room: log.info('I just heard : %s' % repr(obj)) else: log.warn('I do not know about room %s' % obj.get('room_id', '')) return user = self.client.user(obj['user_id']) self.handle_message(self.client, room, obj, user) def handle_message(self, campfire, room, message, speaker): # Handle the join room command match = re.match(r'\s*join\s+(.+)\s*$', message['body']) if match: room.speak(self.joinRoom(match.group(1).strip())) return # Handle the leave room command match = re.match(r'\s*leave\s+(.+)\s*$', message['body']) if match: room.speak(self.leaveRoom(match.group(1).strip())) return # Handle the reload command match = re.match(r'\s*reload\s*$', message['body']) if match: room.speak('Reloading...') self.reload() return # See if it's a built-in command if re.match(r'\s*help\s*$', message['body']): self.handle_help(campfire, room, message, speaker) return # Handle all help requests... match = re.match(r'\s*help\s*(.+?)\s*$', message['body']) if match: name = match.group(1) plugin = self.plugins.get(name, None) if plugin: plugin.send_help(self.client, room, message, speaker) return # Alright, if we've made it here, nothing had help for this command room.speak('No help available for %s' % match.group(1)) return # Otherwise, try to find the plugin that they were trying to talk to match = re.match(r'\s*(\S+)(\s*.*$|$)', message['body']) if match: name = match.group(1) plugin = self.plugins.get(name, None) if plugin: try: plugin.handle_message(self.client, room, message, speaker) return except Exception as e: room.paste('Exception in %s => %s' % (name, repr(e))) log.exception('Exception in %s' % name) else: room.speak('No plugin responds to %s' % message['body']) else: log.warn('No match.') def handle_help(self, campfire, room, message, speaker): room.paste('''Give this campfire bot commands by saying "campy <command>". To find out more about a command, type "campy help <command>". The currently-loaded plugins are: %s''' % ', '.join(self.plugins.keys())) def errback(self, failure): try: failure.raiseException() except: log.exception('Errback') def joinRoom(self, name): room = self.client.find_room_by_name(name) if room: if room.id in self.rooms: log.debug('Already in room %s' % name) return 'Already in room %s' % name else: self.rooms[room.id] = room room.join() room.listen(self.callback, self.errback, start_reactor=False) log.debug('Joined room %s' % name) return 'Joined room %s' % name else: log.debug('Could not find room %s' % name) return 'Couldn\'t find room %s' % name def leaveRoom(self, name): room = self.client.find_room_by_name(name) if room: if len(self.rooms) > 1: if room.id in self.rooms: del self.rooms[room.id] return 'Left room %s' % name else: return 'I\'m not in room %s' % name else: return '%s is the last room I\'m in, and I\'m not leaving!' % name else: return 'Couldn\'t find room %s' % name def start(self): log.info('Starting campy...') reactor.run() def stop(self): for room in self.rooms.values(): room.speak(self.goodbye) if self.leaveOnExit: room.leave()
class Campy(object): def __init__(self, **kwargs): # This is the directory we were launched in, before we move to the # Campy home directory self.launchdir = os.getcwd() # In some cases, it might be useful to provide the subdomain and # an API key in the initializer (perhaps passed in as a command- # line argument?) self.subdomain = kwargs.get('subdomain', None) self.apiKey = kwargs.get('api_key', None) # When we get a message from Campfire, we're supplied with the room_id # as one of the parameters of the message. As such, we'll have a room # dictionary so we can quickly look up the appropriate room object self.rooms = {} # This is a list of all our plugins, where each is an instance of the # type campy.CampyPlugin, and the keys are the regular expressions they # are meant to match. from campy.plugins import sh, say, config, alias self.plugins = { 'sh': sh.ShPlugin(self), 'say': say.SayPlugin(self), 'config': config.ConfigPlugin(self), 'alias': alias.AliasPlugin(self) } # This is the name that the campy bot will respond to. It can be # provided in the constructor, or it can be provided in the settings. # The value provided in the constructor overrides what's specified in # the settings, and it defaults to 'campy' self.name = kwargs.get('name', None) # This is the regular expression that matches our bot's name, and the # corresponding message to parse self.nameRE = None # This is our actual campfire client. It will be made when we read in # our settings. However, for now, it is None. That value is used as # a semaphore indicating whether or not this is the first read-in or # not. self.client = None self.goodbye = 'Goodbye!' self.leaveOnExit = True # Try to set up our home directory self.home = kwargs.get('home', os.path.expanduser('~/.campy')) if not os.path.exists(self.home): try: os.mkdir(self.home) except Exception as e: log.exception('Cannot make directory %s' % self.home) # Set the configuration files self.configs = [ os.path.join(self.home, 'campy.conf'), os.path.join(self.launchdir, 'campy.conf') ] self.read() def findPlugin(self, n): '''Try to locate the module with this particular name''' try: log.debug('Trying to import %s' % n) plugins = [] # If we've already loaded this particular module... try: import campy m = __import__('campy.plugins.%s' % n) m = getattr(campy, 'plugins') m = getattr(m, n) except ImportError: m = __import__(n) for name, klass in inspect.getmembers(m): try: if issubclass(klass, CampyPlugin): plugins.append(klass) except: continue return plugins except: log.exception('Could not find plugin %s' % n) return [] def updatePath(self): for path in os.listdir(self.home): if not path.startswith('.'): p = os.path.join(self.home, path) if p not in sys.path: log.debug('Appending %s to path' % p) sys.path.append(p) def reloadPlugin(self, n): '''Try to reload a given module''' try: reload(n) except Exception: log.exception('Could not reload %s' % n) def update(self, update, destination=None): if not destination: destination = self.data for key, value in update.items(): if key in destination and isinstance(destination[key], dict): self.update(value, destination[key]) else: destination[key] = value return destination def reload(self, **kwargs): '''Given a new set of configurations, make sure we're up to date''' self.updatePath() log.info(repr(kwargs)) subdomain = kwargs.get('subdomain', self.subdomain) apiKey = kwargs.get('api_key', self.apiKey) if not subdomain and not apiKey: log.critical('Need a subdomain and API key') exit(1) self.client = Campfire(subdomain, apiKey) # Now register the bot's name self.name = kwargs.get('name', self.name) or 'campy' self.nameRE = re.compile(r'\s*%s\s+(.+)\s*$' % re.escape(self.name), re.I) # Now join all the appropriate rooms for room in kwargs.get('rooms', []): if not self.joinRoom(room): log.warn('Could not find room %s' % room) else: log.debug('Joined %s' % room) def read(self, overrides={}): '''Re-read the settings, and do all the appropriate imports''' # We're going to build up a single dictionary, making sure to merge # and override all options in subsequent config files. Then, and # only then will we start to go through it. log.debug('Reading configuration files...') self.data = {} for fname in self.configs: try: with file(fname) as f: self.data.update(yaml.load(f)) except Exception as e: log.error('Could not read %s => %s' % (fname, repr(e))) self.update(overrides) # There has to at least be a campy section if 'campy' not in self.data: log.critical('No campy configuration found in config files!') exit(1) # Now go through the remaining sections for section, values in self.data.items(): if section == 'campy': if not values: values = {} self.reload(**values) continue if section == 'logger': import logging.config logging.config.dictConfig(values) continue # If it wasn't the campy section, then we should try to # import the module named in that section, and then to # instantiate each of the plugins that inherit from # plugins.CampyPlugin try: for m in self.findPlugin(section): # Alright! We found a plugin. Now, let's see # if the plugin has been instantiated, or if # it hasn't yet been encountered plugin = self.plugins.get(m.shortname) if plugin: if not values: values = {} plugin.reload(**values) else: log.debug('Storing plugin at %s' % m.shortname) values = values or {} self.plugins[m.shortname] = m(self, **values) except ImportError: log.exception('Unable to import module %s' % section) log.debug('Read configuration files...') def save(self, filename=None): # Try to save the current configuration to a file. try: # To be safe when writing, the best thing to do is to first # save a temporary copy, and then if successful, replace the # original with the temporary path = filename or os.path.join(self.home, 'campy.conf') with file(path + '.bak', 'w+') as f: f.write(yaml.dump(self.data)) # And now move the new file to replace the old file os.rename(path + '.bak', path) log.debug('Wrote to %s' % path) return 'Wrote configuration to %s' % path except Exception as e: log.exception('Campy save exception') return 'Campy save exception => %s' % repr(e) def callback(self, obj): '''Go through all the plugins and give it the appropriate message''' # The object provided looks something like this: # 'body' : Text we heard # 'user_id' : Who said it # 'created_at' : When it happened # 'room_id' : The room where it was said # 'starred' : A string representation ('true' or 'false') # 'type' : Message type (e.g. 'TextMessage') # 'id' : Message ID match = self.nameRE.match(obj.get('body', '')) if match: message = match.group(1) obj['body'] = message else: log.debug('Message "%s" did not match "%s "' % (obj.get('body', ''), self.name)) return # The room that received the message: room = self.rooms.get(obj.get('room_id', ''), None) if room: log.info('I just heard : %s' % repr(obj)) else: log.warn('I do not know about room %s' % obj.get('room_id', '')) return user = self.client.user(obj['user_id']) self.handle_message(self.client, room, obj, user) def handle_message(self, campfire, room, message, speaker): # Handle the join room command match = re.match(r'\s*join\s+(.+)\s*$', message['body']) if match: room.speak(self.joinRoom(match.group(1).strip())) return # Handle the leave room command match = re.match(r'\s*leave\s+(.+)\s*$', message['body']) if match: room.speak(self.leaveRoom(match.group(1).strip())) return # Handle the reload command match = re.match(r'\s*reload\s*$', message['body']) if match: room.speak('Reloading...') self.reload() return # See if it's a built-in command if re.match(r'\s*help\s*$', message['body']): self.handle_help(campfire, room, message, speaker) return # Handle all help requests... match = re.match(r'\s*help\s*(.+?)\s*$', message['body']) if match: name = match.group(1) plugin = self.plugins.get(name, None) if plugin: plugin.send_help(self.client, room, message, speaker) return # Alright, if we've made it here, nothing had help for this command room.speak('No help available for %s' % match.group(1)) return # Otherwise, try to find the plugin that they were trying to talk to match = re.match(r'\s*(\S+)(\s*.*$|$)', message['body']) if match: name = match.group(1) plugin = self.plugins.get(name, None) if plugin: try: plugin.handle_message(self.client, room, message, speaker) return except Exception as e: room.paste('Exception in %s => %s' % (name, repr(e))) log.exception('Exception in %s' % name) else: room.speak('No plugin responds to %s' % message['body']) else: log.warn('No match.') def handle_help(self, campfire, room, message, speaker): room.paste( '''Give this campfire bot commands by saying "campy <command>". To find out more about a command, type "campy help <command>". The currently-loaded plugins are: %s''' % ', '.join(self.plugins.keys())) def errback(self, failure): try: failure.raiseException() except: log.exception('Errback') def joinRoom(self, name): room = self.client.find_room_by_name(name) if room: if room.id in self.rooms: log.debug('Already in room %s' % name) return 'Already in room %s' % name else: self.rooms[room.id] = room room.join() room.listen(self.callback, self.errback, start_reactor=False) log.debug('Joined room %s' % name) return 'Joined room %s' % name else: log.debug('Could not find room %s' % name) return 'Couldn\'t find room %s' % name def leaveRoom(self, name): room = self.client.find_room_by_name(name) if room: if len(self.rooms) > 1: if room.id in self.rooms: del self.rooms[room.id] return 'Left room %s' % name else: return 'I\'m not in room %s' % name else: return '%s is the last room I\'m in, and I\'m not leaving!' % name else: return 'Couldn\'t find room %s' % name def start(self): log.info('Starting campy...') reactor.run() def stop(self): for room in self.rooms.values(): room.speak(self.goodbye) if self.leaveOnExit: room.leave()
class Firebot(Chatbot): def __init__(self, name, commands, auth_token, subdomain, room_name, log_file='/var/log/chatbot.log', log_level=logging.INFO): super(Firebot, self).__init__(name, commands, log_file, log_level) self.auth_token = auth_token self.subdomain = subdomain self.room_name = room_name self.room = None self.users = {} reactor.addSystemEventTrigger('before', 'shutdown', self.disconnect) self.shutting_down = False def speak(self, user, message): if not self.room: self.logger.error("Must have a room before I can speak!") return if "\n" in message or "\r" in message: self.room.paste(message) else: self.room.speak(message) def sound(self, user, message): if not self.room: self.logger.error("Must have a room before I can make sounds!") return self.room.sound(message) def connect(self): self.campfire = Campfire(self.subdomain, self.auth_token) self.room = self.campfire.find_room_by_name(self.room_name) self.room.join() self.username = self.campfire.me()['name'] def callback(message): text = message['body'] user_id = message['user_id'] user = self.users.get(user_id) if user_id: if not user: user = self.campfire.user(user_id)['user']['name'] self.users[user_id] = user self.perform_action(user, text) def err_callback(exc): self.error(exc) self.room.join() self.room.listen(callback, err_callback, start_reactor=False) application = service.Application("firebot") return application def error(self, exc): self.room.leave() self.logger.error(traceback.format_exc()) if not self.shutting_down: self.logger.info("Was disconnected, trying again in 10 seconds.") time.sleep(10) self.connect() def disconnect(self): self.logger.info("Disconnect was called. The next error you see is simply Twisted shutting down.") self.shutting_down = True self.room.leave() reactor.stop()