예제 #1
0
class VitaminBot:
   def __init__(self, config):
      self.config = config

      self._current_users = []
      self._dj_queue = []
      self._djs = []
      self._mods = []

      self._waitingDj = False
      self._waiting_down = False
      self._waiting_user = None

      self._bot = Bot(self.config.authInfo.auth, self.config.authInfo.userid, self.config.authInfo.roomid)

      self._bot.on('speak', self._speak)
      self._bot.on('registered', self._user_in)
      self._bot.on('deregistered', self._user_out)
      self._bot.on('add_dj', self._dj_added)
      self._bot.on('rem_dj', self._dj_removed)
      self._bot.on('roomChanged', self._room_changed)
      self._bot.on('endsong', self._end_song)

      #This prefix can be used by anyone in the room, and it specifies local utility commands for bot management
      #under certain circumstances
      self.LOCAL_COMMAND_PREFIX = ":%:"

#Local helper functions
   def _getUserById(self, l, userid):
      for user in l:
         if user.uid == userid:
            return user
   
   def _getUserByName(self, l, name):
      for user in l:
         if user.name == name:
            return user

   def _removeUser(self, l, userid):
      for user in l:
         if user.uid == userid:
            l.remove(user)
            return user

      return None

   def _queueTimeout(self, user):
      self._bot.speak("Well fine then @%s! You're out of line!" % user.name)
      self._removeUser(self._dj_queue, user.uid)
      self._waitingDj = False
      if len(self._dj_queue) > 0:
         self._notifyQueueUp(self._dj_queue[0])


   def _halfTimeout(self, user):
      self._bot.speak("@%s, 15 seconds and you're out breh..." % user.name)
      self._up_timer = Timer(15,self._queueTimeout, [user])
      self._up_timer.start()

   def _stepDownTimeout(self, user):
      self._bot.speak("Okay @%s, time's up!" % user.name)
      self._waiting_down = False
      self._waiting_user = None

      self._bot.remDj(user.uid)

   def _notifyQueueUp(self, user):
      if not self._waitingDj:
         self._bot.speak("Alright @%s, you're up!" % user.name)
         self._up_timer = Timer(15, self._halfTimeout, [user])
         self._up_timer.start()
         self._waitingDj = True

#Public API functions
#NOTE: These are the only functions that should be called from an
#      implementing module
   def start(self):
      self._bot.start()

   def stop(self):
      self._bot.stop()

   def startDaemon(self, pidfile):
      _daemon = VitaminDaemon(pidfile, self._bot)
      _daemon.start()

   def stopDaemon(self, pidfile):
      _daemon = VitaminDaemon(pidfile, self._bot)
      _daemon.stop()

   def speak(self, s):
      self._bot.speak(str(s))

   def printQueue(self):
      if not self.config.q_enabled:
         self._bot.speak("We are not currently using a queue")
         return

      l = ""

      for i in range(len(self._dj_queue)):
         l += self._dj_queue[i].name + ", "

      self._bot.speak(l)

   def printCommands(self):
      l = ""

      for cmd in self.config.commandList:
         l += cmd + ", "

      self._bot.speak(l)

   def addQueue(self, name, userid):
      user = self._getUserById(self._current_users, userid)

      if not user:
         return

      if not self.config.q_enabled:
         self._bot.speak("We are not currently using a queue")
         return

      if self._dj_queue.count(user) == 0:
         self._dj_queue.append(user)
         self._bot.speak("You have been added to the queue @%s" % name)

         #Now make sure we don't have room for them already
         if len(self._djs) < self.config.MAX_DJS:
            self._notifyQueueUp(user)

      else:
         self._bot.speak("You are already added to the queue... dumb shit.")

   def removeQueue(self, name, userid):
      user = self._getUserById(self._current_users, userid)

      if not user: 
         return

      if not self.config.q_enabled:
         self._bot.speak("We are not currently using a queue")
         return

      if self._dj_queue.count(user) > 0:
         try:
            index = self._dj_queue.index(user)
         except ValueError:
            index = -1

         self._dj_queue.remove(user)
         self._bot.speak("You have been removed from the queue @%s" % name)

         #If this is the user we are waiting on to get up, cancel his timer
         if(index == 0):
            self._up_timer.cancel()
            self._waitingDj = False
            if len(self._dj_queue) > 0:
               self._notifyQueueUp(self._dj_queue[0])

      else:
         self._bot.speak("You aren't in the queue. Get you shit together @%s!" % name)

   def _roomInfoclb(self, data):
      new_users = []
      new_mods = []
      for u in data['users']:
         new_users.append(UserInfo(u['name'], u['userid']))

      for m in data['room']['metadata']['moderator_id']:
         new_mods.append(m)

      self._mods = new_mods

      self._current_users = new_users


   def roomUpdate(self):
      self._bot.roomInfo(False, self._roomInfoclb)


   def enableQueue(self):
      self.config.q_enabled = True

   def disableQueue(self):
      self.config.q_enabled = False

   def vote(self, val):
      if val == 'up' or val == 'down':
         self._bot.vote(val)

   def bop(self):
      self.vote('up')
#Event Handlers

   def _user_id_clb(self, data):
      if data['success']:
         self._current_users.append(UserInfo(self.pendingUserName, data['userid']))

   def _speak(self, data):
      name = data['name']
      text = data['text']

      if text.startswith(self.LOCAL_COMMAND_PREFIX):
         print "Local Command Captured"
         newText = text[len(self.LOCAL_COMMAND_PREFIX):]
         tokenList = newText.split()
         commandToken = tokenList[0]
         print("Token: %s" % commandToken)
         if commandToken == "adduser":
            self.pendingUserName = tokenList[1]
            self._bot.getUserId(tokenList[1], self._user_id_clb)
            return
         if commandToken == "update":
            self.roomUpdate()
            return
         
      #Check if this is a bot command
      if text.startswith(self.config.COMMAND_PREFIX):
         newText = text[len(self.config.COMMAND_PREFIX):]
         tokenList = newText.split()
         commandToken = tokenList[0]
         tokenList.pop(0)
         lCommandToken = commandToken.lower()

         if lCommandToken in self.config.commandList:
            (self.config.commandList[lCommandToken])(data['name'], data['userid'], tokenList)
         else:
            if lCommandToken in self.config.modCommandList:
               if data['userid'] in self.config.modList or data['userid'] in self._mods:
                  (self.config.modCommandList[lCommandToken])(data['name'], data['userid'], tokenList)
               else:
                  self._bot.speak("I don't take those kind of orders from bitches like you, @%s" % data['name'])
            else:
               self._bot.speak("Sorry, I don't recognize %s as a command" % commandToken)

   def _user_in(self, data):
      self._current_users.append(UserInfo(data['user'][0]['name'], data['user'][0]['userid']))
      self._bot.speak("Hello @%s, welcome to SS1!" % data['user'][0]['name'])

   def _user_out(self, data):
      self._removeUser(self._current_users, data['user'][0]['userid'])

   def _dj_added(self, data):
      if self.config.q_enabled:
         if len(self._dj_queue) > 0 and data['user'][0]['userid'] == self._dj_queue[0].uid:
            self._djs.append(self._dj_queue.pop(0))
            self._up_timer.cancel()
            self._waitingDj = False
            if len(self._djs) < self.config.MAX_DJS and len(self._dj_queue) > 0:
               self._notifyQueueUp(self._dj_queue[0])
         else:
            self._bot.remDj(data['user'][0]['userid'])
            self._bot.speak("Get your ass in back in line @%s!" % data['user'][0]['name'])

      else:
         user = self._getUserById(self._current_users, data['user'][0]['userid'])
         self._djs.append(user)

   def _dj_removed(self, data):
      user = self._removeUser(self._djs, data['user'][0]['userid'])
      
      #The user must've been kicked for getting up out of turn
      if not user or not self.config.q_enabled:
         return

      if self._waiting_down and user == self._waiting_user:
         self._waiting_down = False
         self._down_timer.cancel()

      user.playcount = 0
      self._dj_queue.append(user)

      self._bot.speak("You have been added back into the queue @%s" % user.name)

      if len(self._dj_queue):
         self._notifyQueueUp(self._dj_queue[0])

   def _room_changed(self, data):
      self.roomUpdate()

   def _end_song(self, data):
      if not self.config.q_enabled:
         return

      user = self._getUserById(self._djs, data['room']['metadata']['current_dj'])

      if not user:
         return

      user.playcount += 1

      if user.playcount >= self.config.playcount and not self._waiting_down:
         self._bot.speak("Alright @%s. Your plays are up! GTFO" % user.name)
         self._down_timer = Timer(15, self._stepDownTimeout, [user])
         self._waiting_down = True
         self._waiting_user = user
         self._down_timer.start()
예제 #2
0
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
예제 #3
0
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