Beispiel #1
0
    def signedOn(self):
        """Called once we've connected to a network"""
        super().signedOn()

        self.logger.info("Signed on as %s" % self.nickname)

        # Give the factory the instance it created in case it needs to
        # interface for error handling or metadata retention.
        self.factory.cardinal = self

        # Setup PluginManager
        self.plugin_manager = PluginManager(self, self.factory.plugins,
                                            self.factory.blacklist)

        if self.factory.server_commands:
            self.logger.info("Sending server commands")
            for command in self.factory.server_commands:
                self.send(command)

        # Attempt to identify with NickServ, if a password was given
        if self.factory.password:
            self.logger.info("Attempting to identify with NickServ")
            self.msg("NickServ", "IDENTIFY %s" % (self.factory.password, ))

        # For servers that support it, set the bot mode
        self.send("MODE {} +B".format(self.nickname))

        # Attempt to join channels
        for channel in self.factory.channels:
            self.join(channel)

        # Set the uptime as now and grab the  boot time from the factory
        self.uptime = datetime.now()
        self.booted = self.factory.booted
Beispiel #2
0
    def signedOn(self):
        """Called once we've connected to a network"""
        # Give the factory access to the bot
        if self.factory is None:
            raise InternalError("Factory must be set on CardinalBot instance")

        self.logger.info("Signed on as %s" % self.nickname)

        # Give the factory the instance it created in case it needs to
        # interface for error handling or metadata retention.
        self.factory.cardinal = self

        # Setup PluginManager
        self.plugin_manager = PluginManager(self, self.factory.plugins,
                                            self.factory.blacklist)

        # Attempt to identify with NickServ, if a password was given
        if self.factory.password:
            self.logger.info("Attempting to identify with NickServ")
            self.msg("NickServ", "IDENTIFY %s" % (self.factory.password, ))

        # For servers that support it, set the bot mode
        #
        # FIXME: is it possible that the server renames us before this? if so,
        # the mode registration would fail erroneously
        self.send("MODE {} +B".format(self.nickname))

        # Attempt to join channels
        for channel in self.factory.channels:
            self.join(channel)

        # Set the uptime as now and grab the  boot time from the factory
        self.uptime = datetime.now()
        self.booted = self.factory.booted
Beispiel #3
0
    def signedOn(self):
        """Called once we've connected to a network"""
        self.logger.info("Signed on as %s" % self.nickname)

        # Give the factory access to the bot
        if self.factory is None:
            raise InternalError("Factory must be set on CardinalBot instance")

        # Give the factory the instance it created in case it needs to
        # interface for error handling or metadata retention.
        self.factory.cardinal = self

        # Set the currently connected network
        self.network = self.factory.network

        # Attempt to identify with NickServ, if a password was given
        if self.factory.password:
            self.logger.info("Attempting to identify with NickServ")
            self.msg("NickServ", "IDENTIFY %s" % (self.factory.password, ))

        # Creates an instance of EventManager
        self.logger.debug("Creating new EventManager instance")
        self.event_manager = EventManager(self)

        # Register events
        try:
            self.event_manager.register("irc.invite", 2)
            self.event_manager.register("irc.privmsg", 3)
            self.event_manager.register("irc.notice", 3)
            self.event_manager.register("irc.nick", 2)
            self.event_manager.register("irc.mode", 3)
            self.event_manager.register("irc.topic", 3)
            self.event_manager.register("irc.join", 2)
            self.event_manager.register("irc.part", 3)
            self.event_manager.register("irc.kick", 4)
            self.event_manager.register("irc.quit", 2)
        except EventAlreadyExistsError:
            self.logger.error("Could not register core IRC events",
                              exc_info=True)

        # Create an instance of PluginManager, giving it an instance of ourself
        # to pass to plugins, as well as a list of initial plugins to load.
        self.logger.debug("Creating new PluginManager instance")
        self.plugin_manager = PluginManager(self, self.factory.plugins)

        # Attempt to join channels
        for channel in self.factory.channels:
            self.join(channel)

        # Set the uptime as now and grab the  boot time from the factory
        self.uptime = datetime.now()
        self.booted = self.factory.booted
Beispiel #4
0
    def setup_method(self):
        mock_cardinal = self.cardinal = Mock(spec=CardinalBot)
        mock_cardinal.nickname = 'Cardinal'
        mock_cardinal.event_manager = self.event_manager = \
            EventManager(mock_cardinal)

        self.blacklist = {}

        self.plugin_manager = PluginManager(
            mock_cardinal, [],
            self.blacklist,
            _plugin_module_import_prefix='fake_plugins',
            _plugin_module_directory_suffix='cardinal/fixtures/fake_plugins')
Beispiel #5
0
    def setup_method(self):
        mock_cardinal = self.cardinal = Mock(spec=CardinalBot)
        mock_cardinal.nickname = 'Cardinal'
        mock_cardinal.event_manager = self.event_manager = \
            EventManager(mock_cardinal)

        self.blacklist = {}

        plugins_directory = os.path.abspath(os.path.join(
            os.path.dirname(os.path.realpath(os.path.abspath(__file__))),
            '..',
            'cardinal/fixtures/fake_plugins',
        ))
        self.plugin_manager = PluginManager(
            mock_cardinal,
            [],
            self.blacklist,
            _plugin_module_import_prefix='fake_plugins',
            _plugin_module_directory=plugins_directory,
        )
Beispiel #6
0
    def signedOn(self):
        """Called once we've connected to a network"""
        self.logger.info("Signed on as %s" % self.nickname)

        # Give the factory access to the bot
        if self.factory is None:
            raise InternalError("Factory must be set on CardinalBot instance")

        # Give the factory the instance it created in case it needs to
        # interface for error handling or metadata retention.
        self.factory.cardinal = self

        # Set the currently connected network
        self.network = self.factory.network

        # Attempt to identify with NickServ, if a password was given
        if self.factory.password:
            self.logger.info("Attempting to identify with NickServ")
            self.msg("NickServ", "IDENTIFY %s" % (self.factory.password,))

        # Creates an instance of EventManager
        self.logger.debug("Creating new EventManager instance")
        self.event_manager = EventManager(self)

        # Register events
        try:
            self.event_manager.register("irc.invite", 2)
            self.event_manager.register("irc.privmsg", 3)
            self.event_manager.register("irc.notice", 3)
            self.event_manager.register("irc.nick", 2)
            self.event_manager.register("irc.mode", 3)
            self.event_manager.register("irc.topic", 3)
            self.event_manager.register("irc.join", 2)
            self.event_manager.register("irc.part", 3)
            self.event_manager.register("irc.kick", 4)
            self.event_manager.register("irc.quit", 2)
        except EventAlreadyExistsError:
            self.logger.error(
                "Could not register core IRC events", exc_info=True)

        # Create an instance of PluginManager, giving it an instance of ourself
        # to pass to plugins, as well as a list of initial plugins to load.
        self.logger.debug("Creating new PluginManager instance")
        self.plugin_manager = PluginManager(self, self.factory.plugins)

        # Attempt to join channels
        for channel in self.factory.channels:
            self.join(channel)

        # Set the uptime as now and grab the  boot time from the factory
        self.uptime = datetime.now()
        self.booted = self.factory.booted
Beispiel #7
0
class CardinalBot(irc.IRCClient, object):
    """Cardinal, in all its glory"""

    logger = None
    """Logging object for CardinalBot"""

    factory = None
    """Should contain an instance of CardinalBotFactory"""

    network = None
    """Currently connected network (e.g. irc.freenode.net)"""

    user_regex = re.compile(r'^(.*?)!(.*?)@(.*?)$')
    """Regex for identifying a user's nick, ident, and vhost"""

    plugin_manager = None
    """Instance of PluginManager"""

    event_manager = None
    """Instance of EventManager"""

    storage_path = None
    """Location of storage directory"""

    uptime = None
    """Time that Cardinal connected to the network"""

    booted = None
    """Time that Cardinal was first launched"""

    @property
    def nickname(self):
        return self.factory.nickname

    @nickname.setter
    def nickname(self, value):
        self.factory.nickname = value

    @property
    def reloads(self):
        return self.factory.reloads

    @reloads.setter
    def reloads(self, value):
        self.factory.reloads = value

    def __init__(self):
        """Initializes the logging and sets storage directory"""
        self.logger = logging.getLogger(__name__)

        self.storage_path = os.path.join(
            os.path.dirname(os.path.realpath(sys.argv[0])),
            'storage'
        )

        # State variables for the WHO command
        self.who_lock = {}
        self.who_cache = {}
        self.who_callbacks = {}

    def signedOn(self):
        """Called once we've connected to a network"""
        self.logger.info("Signed on as %s" % self.nickname)

        # Give the factory access to the bot
        if self.factory is None:
            raise InternalError("Factory must be set on CardinalBot instance")

        # Give the factory the instance it created in case it needs to
        # interface for error handling or metadata retention.
        self.factory.cardinal = self

        # Set the currently connected network
        self.network = self.factory.network

        # Attempt to identify with NickServ, if a password was given
        if self.factory.password:
            self.logger.info("Attempting to identify with NickServ")
            self.msg("NickServ", "IDENTIFY %s" % (self.factory.password,))

        # Creates an instance of EventManager
        self.logger.debug("Creating new EventManager instance")
        self.event_manager = EventManager(self)

        # Register events
        self.event_manager.register("irc.invite", 2)
        self.event_manager.register("irc.privmsg", 3)
        self.event_manager.register("irc.notice", 3)
        self.event_manager.register("irc.nick", 2)
        self.event_manager.register("irc.mode", 3)
        self.event_manager.register("irc.topic", 3)
        self.event_manager.register("irc.join", 2)
        self.event_manager.register("irc.part", 3)
        self.event_manager.register("irc.kick", 4)
        self.event_manager.register("irc.quit", 2)

        # Create an instance of PluginManager, giving it an instance of ourself
        # to pass to plugins, as well as a list of initial plugins to load.
        self.logger.debug("Creating new PluginManager instance")
        self.plugin_manager = PluginManager(self, self.factory.plugins)

        # Attempt to join channels
        for channel in self.factory.channels:
            self.join(channel)

        # Set the uptime as now and grab the  boot time from the factory
        self.uptime = datetime.now()
        self.booted = self.factory.booted

    def joined(self, channel):
        """Called when we join a channel.

        channel -- Channel joined. Provided by Twisted.
        """
        self.logger.info("Joined %s" % channel)

    def irc_PRIVMSG(self, prefix, params):
        """Called when we receive a message in a channel or PM."""
        # Break down the user into usable groups
        user = re.match(self.user_regex, prefix)
        channel = params[0]
        message = params[1]

        self.logger.debug(
            "%s!%s@%s to %s: %s" %
            (user.group(1), user.group(2), user.group(3), channel, message)
        )

        self.event_manager.fire("irc.privmsg", user, channel, message)

        # If the channel is ourselves, this is actually a PM to us, and so
        # we'll update the channel variable to the sender's username to make
        # replying a little easier.
        if channel == self.nickname:
            channel = user.group(1)

        # Attempt to call a command. If it doesn't appear to PluginManager to
        # be a command, this will just fall through. If it matches command
        # syntax but there is no matching command, then we should catch the
        # exception.
        try:
            self.plugin_manager.call_command(user, channel, message)
        except CommandNotFoundError:
            # This is just an info, since anyone can trigger it, not really a
            # bad thing.
            self.logger.info(
                "Unable to find a matching command", exc_info=True)

    def who(self, channel, callback):
        """Lists the users in a channel.

        Keyword arguments:
          channel -- Channel to list users of.
          callback -- A callback that will receive the list of users.

        Returns:
          None. However, the callback will receive a single argument,
          which is the list of users.
        """
        if channel not in self.who_callbacks:
            self.who_callbacks[channel] = []
        self.who_callbacks[channel].append(callback)

        self.logger.info("WHO list requested for %s" % channel)

        if channel not in self.who_lock:
            self.logger.info("Making WHO request to server")
            # Set a lock to prevent trying to track responses from the server
            self.who_lock[channel] = True

            # Empty the cache to ensure no old users show up.
            # TODO: Add actual caching and user tracking.
            self.who_cache[channel] = []

            # Send the actual WHO command to the server. irc_RPL_WHOREPLY will
            # receive a response when the server sends one.
            self.sendLine("WHO %s" % channel)

    def irc_RPL_WHOREPLY(self, *nargs):
        "Receives reply from WHO command and sends to caller"
        response = nargs[1]

        # Same format as other events (nickname!ident@hostname)
        user = (
            response[5],  # nickname
            response[2],  # ident
            response[3],  # hostname
        )
        channel = response[1]

        self.who_cache[channel].append(user)

    def irc_RPL_ENDOFWHO(self, *nargs):
        "Called when WHO output is complete"
        response = nargs[1]
        channel = response[1]

        self.logger.info("Calling WHO callbacks for %s" % channel)
        for callback in self.who_callbacks[channel]:
            callback(self.who_cache[channel])

    def irc_NOTICE(self, prefix, params):
        """Called when a notice is sent to a channel or privately"""
        user = re.match(self.user_regex, prefix)
        channel = params[0]
        message = params[1]

        # Sent by network, not a real user
        if not user:
            self.logger.debug(
                "%s sent notice to %s: %s" % (prefix, channel, message)
            )
            return

        self.logger.debug(
            "%s!%s@%s sent notice to %s: %s" %
            (user.group(1), user.group(2), user.group(3), channel, message)
        )

        # Lots of NOTICE messages when connecting, and event manager may not be
        # initialized yet.
        if self.event_manager:
            self.event_manager.fire("irc.notice", user, channel, message)

    def irc_NICK(self, prefix, params):
        """Called when a user changes their nick"""
        user = re.match(self.user_regex, prefix)
        new_nick = params[0]

        self.logger.debug(
            "%s!%s@%s changed nick to %s" %
            (user.group(1), user.group(2), user.group(3), new_nick)
        )

        self.event_manager.fire("irc.nick", user, new_nick)

    def irc_TOPIC(self, prefix, params):
        """Called when a new topic is set"""
        user = re.match(self.user_regex, prefix)
        channel = params[0]
        topic = params[1]

        self.logger.debug(
            "%s!%s@%s changed topic in %s to %s" %
            (user.group(1), user.group(2), user.group(3), channel, topic)
        )

        self.event_manager.fire("irc.topic", user, channel, topic)

    def irc_MODE(self, prefix, params):
        """Called when a mode is set on a channel"""
        user = re.match(self.user_regex, prefix)
        channel = params[0]
        mode = ' '.join(params[1:])

        # Sent by network, not a real user
        if not user:
            self.logger.debug(
                "%s set mode on %s (%s)" % (prefix, channel, mode)
            )
            return

        self.logger.debug(
            "%s!%s@%s set mode on %s (%s)" %
            (user.group(1), user.group(2), user.group(3), channel, mode)
        )

        # Can get called during connection, in which case EventManager won't be
        # initialized yet
        if self.event_manager:
            self.event_manager.fire("irc.mode", user, channel, mode)

    def irc_JOIN(self, prefix, params):
        """Called when a user joins a channel"""
        user = re.match(self.user_regex, prefix)
        channel = params[0]

        self.logger.debug(
            "%s!%s@%s joined %s" %
            (user.group(1), user.group(2), user.group(3), channel)
        )

        self.event_manager.fire("irc.join", user, channel)

    def irc_PART(self, prefix, params):
        """Called when a user parts a channel"""
        user = re.match(self.user_regex, prefix)
        channel = params[0]
        if len(params) == 1:
            reason = "No Message"
        else:
            reason = params[1]

        self.logger.debug(
            "%s!%s@%s parted %s (%s)" %
            (user.group(1), user.group(2), user.group(3), channel, reason)
        )

        self.event_manager.fire("irc.part", user, channel, reason)

    def irc_KICK(self, prefix, params):
        """Called when a user is kicked from a channel"""
        user = re.match(self.user_regex, prefix)
        nick = params[0]
        channel = params[1]
        if len(params) == 2:
            reason = "No Message"
        else:
            reason = params[2]

        self.logger.debug(
            "%s!%s@%s kicked %s from %s (%s)" %
            (user.group(1), user.group(2), user.group(3),
                nick, channel, reason)
        )

        self.event_manager.fire("irc.kick", user, channel, nick, reason)

    def irc_QUIT(self, prefix, params):
        """Called when a user quits the network"""
        user = re.match(self.user_regex, prefix)
        if len(params) == 0:
            reason = "No Message"
        else:
            reason = params[0]

        self.logger.debug(
            "%s!%s@%s quit (%s)" %
            (user.group(1), user.group(2), user.group(3), reason)
        )

        self.event_manager.fire("irc.quit", user, reason)

    def irc_unknown(self, prefix, command, params):
        """Called when Twisted doesn't understand an IRC command.

        Keyword arguments:
          prefix -- User sending command. Provided by Twisted.
          command -- Command that wasn't recognized. Provided by Twisted.
          params -- Params for command. Provided by Twisted.
        """
        # A user has invited us to a channel
        if command == "INVITE":
            # Break down the user into usable groups
            user = re.match(self.user_regex, prefix)
            channel = params[1]

            self.logger.debug("%s invited us to %s" % (user.group(1), channel))

            # Fire invite event, so plugins can hook into it
            self.event_manager.fire("irc.invite", user, channel)

            # TODO: Call matching plugin events

    def config(self, plugin):
        """Returns a given loaded plugin's config.

        Keyword arguments:
          plugin -- String containing a plugin name.

        Returns:
          dict -- Dictionary containing plugin config.

        Raises:
          ConfigNotFoundError - When config can't be found for the plugin.
        """
        if self.plugin_manager is None:
            self.logger.error(
                "PluginManager has not been initialized! Can't return config "
                "for plugin: %s" % plugin
            )
            raise PluginError("PluginManager has not yet been initialized")

        try:
            config = self.plugin_manager.get_config(plugin)
        except ConfigNotFoundError:
            # Log and raise the exception again
            self.logger.exception(
                "Couldn't find config for plugin: %s" % plugin
            )
            raise

        return config

    def sendMsg(self, channel, message, length=None):
        """Wrapper command to send messages.

        Keyword arguments:
          channel -- Channel to send message to.
          message -- Message to send.
          length -- Length of message. Twisted will calculate if None given.
        """
        self.logger.info("Sending in %s: %s" % (channel, message))
        self.msg(channel, message, length)

    def disconnect(self, message=''):
        """Wrapper command to quit Cardinal.

        Keyword arguments:
          message -- Message to insert into QUIT, if any.
        """
        self.logger.info("Disconnecting from network")
        self.plugin_manager.unload_all()
        self.factory.disconnect = True
        self.quit(message)
Beispiel #8
0
class CardinalBot(irc.IRCClient, object):
    """Cardinal, in all its glory"""
    @property
    def network(self):
        return self.factory.network

    @network.setter
    def network(self, value):
        self.factory.network = value

    @property
    def nickname(self):
        return self.factory.nickname

    @nickname.setter
    def nickname(self, value):
        self.factory.nickname = value

    @property
    def password(self):
        """Twisted.irc.IRCClient server password setting"""
        return self.factory.server_password

    @password.setter
    def password(self, value):
        self.factory.server_password = value

    @property
    def username(self):
        return self.factory.username

    @username.setter
    def username(self, value):
        self.factory.username = value

    @property
    def realname(self):
        return self.factory.realname

    @realname.setter
    def realname(self, value):
        self.factory.realname = value

    @property
    def storage_path(self):
        return self.factory.storage_path

    def __init__(self):
        """Initializes the logging"""
        self.logger = logging.getLogger(__name__)
        self.irc_logger = logging.getLogger("%s.irc" % __name__)

        # Will get set by Twisted before signedOn is called
        self.factory = None

        # PluginManager gets created in signedOn
        self.plugin_manager = None

        # Setup EventManager
        self.event_manager = EventManager(self)

        # Register events
        self.event_manager.register("irc.raw", 2)
        self.event_manager.register("irc.invite", 2)
        self.event_manager.register("irc.privmsg", 3)
        self.event_manager.register("irc.notice", 3)
        self.event_manager.register("irc.nick", 2)
        self.event_manager.register("irc.mode", 3)
        self.event_manager.register("irc.topic", 3)
        self.event_manager.register("irc.join", 2)
        self.event_manager.register("irc.part", 3)
        self.event_manager.register("irc.kick", 4)
        self.event_manager.register("irc.quit", 2)

        # State variables for the WHO command
        self._who_cache = {}
        self._who_deferreds = {}

        # Database file locks
        self.db_locks = {}

    def signedOn(self):
        """Called once we've connected to a network"""
        super().signedOn()

        self.logger.info("Signed on as %s" % self.nickname)

        # Give the factory the instance it created in case it needs to
        # interface for error handling or metadata retention.
        self.factory.cardinal = self

        # Setup PluginManager
        self.plugin_manager = PluginManager(self, self.factory.plugins,
                                            self.factory.blacklist)

        if self.factory.server_commands:
            self.logger.info("Sending server commands")
            for command in self.factory.server_commands:
                self.send(command)

        # Attempt to identify with NickServ, if a password was given
        if self.factory.password:
            self.logger.info("Attempting to identify with NickServ")
            self.msg("NickServ", "IDENTIFY %s" % (self.factory.password, ))

        # For servers that support it, set the bot mode
        self.send("MODE {} +B".format(self.nickname))

        # Attempt to join channels
        for channel in self.factory.channels:
            self.join(channel)

        # Set the uptime as now and grab the  boot time from the factory
        self.uptime = datetime.now()
        self.booted = self.factory.booted

    def joined(self, channel):
        """Called when we join a channel.

        channel -- Channel joined. Provided by Twisted.
        """
        self.logger.info("Joined %s" % channel)

    def lineReceived(self, line):
        """Called for every line received from the server."""
        # The IRC spec does not specify a message encoding, meaning that some
        # messages may fail to decode into a UTF-8 string. While we must be
        # aware of the issue and choose to replace "invalid" characters (which
        # are technically valid per the IRC RFC, hence us warning about this
        # behavior), we must also ensure that the Twisted IRCClient's
        # implementation of lineReceived does not receive these characters, as
        # it will not replace them.
        try:
            line = line.decode('utf-8')
        except UnicodeDecodeError:
            self.logger.warning(
                "Stripping non-UTF-8 data from received line: {}".format(line))
            line = line.decode('utf-8', 'replace')

        # Log raw output
        self.irc_logger.info(line)

        # Log if the command received is in the error range
        _, command, _ = irc.parsemsg(line)
        if command.isnumeric() and 400 <= int(command) <= 599:
            self.logger.warning(
                "Received an error from the server: {}".format(line))

        self.event_manager.fire("irc.raw", command, line)

        # Send IRCClient the version of the line that has had non-UTF-8
        # characters replaced.
        #
        # Bug: https://twistedmatrix.com/trac/ticket/9443
        super().lineReceived(line.encode('utf-8'))

    def irc_PRIVMSG(self, prefix, params):
        """Called when we receive a message in a channel or PM."""
        super().irc_PRIVMSG(prefix, params)

        # Break down the user into usable groups
        user = self.get_user_tuple(prefix)
        nick = user[0]
        channel = params[0]
        message = params[1]

        self.logger.debug("%s!%s@%s to %s: %s" % (user + (channel, message)))

        self.event_manager.fire("irc.privmsg", user, channel, message)

        # If the channel is ourselves, this is actually a PM to us, and so
        # we'll update the channel variable to the sender's username to make
        # replying a little easier.
        if channel == self.nickname:
            channel = nick
        else:
            # If the message is directed at us, strip the prefix telling us so.
            # This allows us to target a specific Cardinal bot if multiple are
            # in a channel, and it works for plugins that use a regex as well.
            # If a plugin needs the original message, they can use the
            # irc.privmsg event.
            nick_prefix = "{}: ".format(self.nickname)
            if message.startswith(nick_prefix):
                message = message[len(nick_prefix):]

        # Attempt to call a command. If it doesn't appear to PluginManager to
        # be a command, this will just fall through. If it matches command
        # syntax but there is no matching command, then we should catch the
        # exception.
        try:
            self.plugin_manager.call_command(user, channel, message)
        except CommandNotFoundError:
            self.logger.debug("Unable to find a matching command",
                              exc_info=True)

    def irc_NOTICE(self, prefix, params):
        """Called when a notice is sent to a channel or privately"""
        super().irc_NOTICE(prefix, params)

        user = self.get_user_tuple(prefix)
        channel = params[0]
        message = params[1]

        # Sent by network, not a real user
        if not user:
            self.logger.debug("%s sent notice to %s: %s" %
                              (prefix, channel, message))
            return

        self.logger.debug("%s!%s@%s sent notice to %s: %s" %
                          (user + (channel, message)))

        self.event_manager.fire("irc.notice", user, channel, message)

    def irc_NICK(self, prefix, params):
        """Called when a user changes their nick"""
        super().irc_NICK(prefix, params)

        user = self.get_user_tuple(prefix)
        new_nick = params[0]

        self.logger.debug("%s!%s@%s changed nick to %s" % (user +
                                                           (new_nick, )))

        self.event_manager.fire("irc.nick", user, new_nick)

    def irc_TOPIC(self, prefix, params):
        """Called when a new topic is set"""
        super().irc_TOPIC(prefix, params)

        user = self.get_user_tuple(prefix)
        channel = params[0]
        topic = params[1]

        self.logger.debug("%s!%s@%s changed topic in %s to %s" %
                          (user + (channel, topic)))

        self.event_manager.fire("irc.topic", user, channel, topic)

    def irc_MODE(self, prefix, params):
        """Called when a mode is set on a channel"""
        super().irc_MODE(prefix, params)

        user = self.get_user_tuple(prefix)
        channel = params[0]
        mode = ' '.join(params[1:])

        # Sent by network, not a real user
        if not user:
            self.logger.debug("%s set mode on %s (%s)" %
                              (prefix, channel, mode))
            return

        self.logger.debug("%s!%s@%s set mode on %s (%s)" % (user +
                                                            (channel, mode)))

        self.event_manager.fire("irc.mode", user, channel, mode)

    def irc_JOIN(self, prefix, params):
        """Called when a user joins a channel"""
        super().irc_JOIN(prefix, params)

        user = self.get_user_tuple(prefix)
        channel = params[0]

        self.logger.debug("%s!%s@%s joined %s" % (user + (channel, )))

        self.event_manager.fire("irc.join", user, channel)

    def irc_PART(self, prefix, params):
        """Called when a user parts a channel"""
        super().irc_PART(prefix, params)

        user = self.get_user_tuple(prefix)
        channel = params[0]
        if len(params) == 1:
            reason = None
        else:
            reason = params[1]

        self.logger.debug("%s!%s@%s parted %s (%s)" %
                          (user +
                           (channel, reason if reason else "No Message")))

        self.event_manager.fire("irc.part", user, channel, reason)

    def irc_KICK(self, prefix, params):
        """Called when a user is kicked from a channel"""
        super().irc_KICK(prefix, params)

        user = self.get_user_tuple(prefix)
        nick = params[1]
        channel = params[0]
        if len(params) == 2:
            reason = None
        else:
            reason = params[2]

        self.logger.debug(
            "%s!%s@%s kicked %s from %s (%s)" %
            (user + (nick, channel, reason if reason else "No Message")))

        self.event_manager.fire("irc.kick", user, channel, nick, reason)

    def irc_QUIT(self, prefix, params):
        """Called when a user quits the network"""
        super().irc_QUIT(prefix, params)

        user = self.get_user_tuple(prefix)
        if len(params[0]) == 0:
            reason = None
        else:
            reason = params[0]

        self.logger.debug("%s!%s@%s quit (%s)" %
                          (user + (reason if reason else "No Message", )))

        self.event_manager.fire("irc.quit", user, reason)

    def irc_RPL_WHOREPLY(self, prefix, params):
        """Called for each user in the WHO reply.

        This is the second piece of the `who()` method call. We will add each
        user listed in the reply to a list for the channel that will be sent
        to the channel's Deferred once all users have been listed.
        """
        # Same format as other events (nickname!ident@hostname)
        self.logger.info(params)
        user = user_info(
            params[5],  # nickname
            params[2],  # ident
            params[3],  # hostname
        )
        channel = params[1]

        self._who_cache[channel].append(user)

    def irc_RPL_ENDOFWHO(self, prefix, params):
        """Called when WHO reply is complete.

        This is the final piece of the `who()` method call. This indicates we
        can consider the WHO listing complete, and resolve the Deferred for the
        given channel.
        """
        self.logger.info(params)
        channel = params[1]

        self.logger.info("WHO reply received for %s" % channel)
        for d in self._who_deferreds[channel]:
            d.callback(self._who_cache[channel])

        del self._who_deferreds[channel]

    def irc_unknown(self, prefix, command, params):
        """Called when Twisted doesn't understand an IRC command.

        Keyword arguments:
          prefix -- User sending command. Provided by Twisted.
          command -- Command that wasn't recognized. Provided by Twisted.
          params -- Params for command. Provided by Twisted.
        """
        super().irc_unknown(prefix, command, params)

        # A user has invited us to a channel
        if command == "INVITE":
            # Break down the user into usable groups
            user = self.get_user_tuple(prefix)
            nick = user[0]
            channel = params[1]

            self.logger.debug("%s invited us to %s" % (nick, channel))

            # Fire invite event, so plugins can hook into it
            self.event_manager.fire("irc.invite", user, channel)

    def who(self, channel):
        """Lists the users in a channel.

        Keyword arguments:
          channel -- Channel to list users of.

        Returns:
          Deferred -- A Deferred which will have its callbacks called when
            the WHO response comes back from the server.
        """
        self.logger.info("WHO list requested for %s" % channel)

        d = defer.Deferred()
        if channel not in self._who_deferreds:
            self._who_cache[channel] = []
            self._who_deferreds[channel] = [d]

            # Send the actual WHO command to the server. irc_RPL_WHOREPLY will
            # receive a response when the server sends one.
            self.logger.info("Making WHO request to server")
            self.sendLine("WHO %s" % channel)
        else:
            self._who_deferreds[channel].append(d)

        return d

    def config(self, plugin):
        """Returns a given loaded plugin's config.

        Keyword arguments:
          plugin -- String containing a plugin name.

        Returns:
          dict -- Dictionary containing plugin config.

        Raises:
          ConfigNotFoundError - When config can't be found for the plugin.
        """
        if self.plugin_manager is None:
            self.logger.error(
                "PluginManager has not been initialized! Can't return config "
                "for plugin: %s" % plugin)
            raise PluginError("PluginManager has not yet been initialized")

        try:
            config = self.plugin_manager.get_config(plugin)
        except ConfigNotFoundError:
            # Log and raise the exception again
            self.logger.exception("Couldn't find config for plugin: %s" %
                                  plugin)
            raise

        return config

    def sendMsg(self, channel, message, length=None):
        """Wrapper command to send messages.

        Keyword arguments:
          channel -- Channel to send message to.
          message -- Message to send.
          length -- Length of message. Twisted will calculate if None given.
        """
        self.logger.info("Sending in %s: %s" % (channel, message))
        self.msg(channel, message, length)

    def send(self, message):
        """Send a raw message to the server.

        Keyword arguments:
          message -- Message to send.
        """
        self.logger.info("Sending to server: %s" % message)
        self.sendLine(message)

    def disconnect(self, message=''):
        """Wrapper command to quit Cardinal.

        Keyword arguments:
          message -- Message to insert into QUIT, if any.
        """
        self.logger.info("Disconnecting from network")
        self.plugin_manager.unload_all()
        self.factory.disconnect = True
        self.quit(message)

    def get_db(self, name, network_specific=True, default=None):
        if default is None:
            default = {}

        db_path = os.path.join(
            self.storage_path, 'database', name +
            ('-{}'.format(self.network) if network_specific else '') + '.json')

        if db_path not in self.db_locks:
            self.db_locks[db_path] = UNLOCKED

        @contextmanager
        def db():
            if self.db_locks[db_path] == LOCKED:
                raise LockInUseError('DB {} locked'.format(db_path))

            self.db_locks[db_path] = LOCKED

            try:
                if not os.path.exists(db_path):
                    with open(db_path, 'w') as f:
                        json.dump(default, f)

                # Load the DB as JSON, use it, then save the result
                with open(db_path, 'r+') as f:
                    database = json.load(f)

                    yield database

                    f.seek(0)
                    f.truncate()
                    json.dump(database, f)
            finally:
                self.db_locks[db_path] = UNLOCKED

        return db

    @staticmethod
    def get_user_tuple(string):
        user = re.match(USER_REGEX, string)
        if user:
            return user_info(user.group(1), user.group(2), user.group(3))
        return user
Beispiel #9
0
class CardinalBot(irc.IRCClient, object):
    """Cardinal, in all its glory"""

    logger = None
    """Logging object for CardinalBot"""

    factory = None
    """Should contain an instance of CardinalBotFactory"""

    network = None
    """Currently connected network (e.g. irc.freenode.net)"""

    user_regex = re.compile(r'^(.*?)!(.*?)@(.*?)$')
    """Regex for identifying a user's nick, ident, and vhost"""

    plugin_manager = None
    """Instance of PluginManager"""

    event_manager = None
    """Instance of EventManager"""

    storage_path = None
    """Location of storage directory"""

    uptime = None
    """Time that Cardinal connected to the network"""

    booted = None
    """Time that Cardinal was first launched"""
    @property
    def nickname(self):
        return self.factory.nickname

    @nickname.setter
    def nickname(self, value):
        self.factory.nickname = value

    @property
    def reloads(self):
        return self.factory.reloads

    @reloads.setter
    def reloads(self, value):
        self.factory.reloads = value

    def __init__(self):
        """Initializes the logging and sets storage directory"""
        self.logger = logging.getLogger(__name__)

        self.storage_path = os.path.join(
            os.path.dirname(os.path.realpath(sys.argv[0])), 'storage')

        # State variables for the WHO command
        self.who_lock = {}
        self.who_cache = {}
        self.who_callbacks = {}

    def signedOn(self):
        """Called once we've connected to a network"""
        self.logger.info("Signed on as %s" % self.nickname)

        # Give the factory access to the bot
        if self.factory is None:
            raise InternalError("Factory must be set on CardinalBot instance")

        # Give the factory the instance it created in case it needs to
        # interface for error handling or metadata retention.
        self.factory.cardinal = self

        # Set the currently connected network
        self.network = self.factory.network

        # Attempt to identify with NickServ, if a password was given
        if self.factory.password:
            self.logger.info("Attempting to identify with NickServ")
            self.msg("NickServ", "IDENTIFY %s" % (self.factory.password, ))

        # Creates an instance of EventManager
        self.logger.debug("Creating new EventManager instance")
        self.event_manager = EventManager(self)

        # Register events
        try:
            self.event_manager.register("irc.invite", 2)
            self.event_manager.register("irc.privmsg", 3)
            self.event_manager.register("irc.notice", 3)
            self.event_manager.register("irc.nick", 2)
            self.event_manager.register("irc.mode", 3)
            self.event_manager.register("irc.topic", 3)
            self.event_manager.register("irc.join", 2)
            self.event_manager.register("irc.part", 3)
            self.event_manager.register("irc.kick", 4)
            self.event_manager.register("irc.quit", 2)
        except EventAlreadyExistsError:
            self.logger.error("Could not register core IRC events",
                              exc_info=True)

        # Create an instance of PluginManager, giving it an instance of ourself
        # to pass to plugins, as well as a list of initial plugins to load.
        self.logger.debug("Creating new PluginManager instance")
        self.plugin_manager = PluginManager(self, self.factory.plugins)

        # Attempt to join channels
        for channel in self.factory.channels:
            self.join(channel)

        # Set the uptime as now and grab the  boot time from the factory
        self.uptime = datetime.now()
        self.booted = self.factory.booted

    def joined(self, channel):
        """Called when we join a channel.

        channel -- Channel joined. Provided by Twisted.
        """
        self.logger.info("Joined %s" % channel)

    def irc_PRIVMSG(self, prefix, params):
        """Called when we receive a message in a channel or PM."""
        # Break down the user into usable groups
        user = re.match(self.user_regex, prefix)
        channel = params[0]
        message = params[1]

        self.logger.debug(
            "%s!%s@%s to %s: %s" %
            (user.group(1), user.group(2), user.group(3), channel, message))

        self.event_manager.fire("irc.privmsg", user, channel, message)

        # If the channel is ourselves, this is actually a PM to us, and so
        # we'll update the channel variable to the sender's username to make
        # replying a little easier.
        if channel == self.nickname:
            channel = user.group(1)

        # Attempt to call a command. If it doesn't appear to PluginManager to
        # be a command, this will just fall through. If it matches command
        # syntax but there is no matching command, then we should catch the
        # exception.
        try:
            self.plugin_manager.call_command(user, channel, message)
        except CommandNotFoundError:
            # This is just an info, since anyone can trigger it, not really a
            # bad thing.
            self.logger.info("Unable to find a matching command",
                             exc_info=True)

    def who(self, channel, callback):
        """Lists the users in a channel.

        Keyword arguments:
          channel -- Channel to list users of.
          callback -- A callback that will receive the list of users.

        Returns:
          None. However, the callback will receive a single argument,
          which is the list of users.
        """
        if channel not in self.who_callbacks:
            self.who_callbacks[channel] = []
        self.who_callbacks[channel].append(callback)

        self.logger.info("WHO list requested for %s" % channel)

        if channel not in self.who_lock:
            self.logger.info("Making WHO request to server")
            # Set a lock to prevent trying to track responses from the server
            self.who_lock[channel] = True

            # Empty the cache to ensure no old users show up.
            # TODO: Add actual caching and user tracking.
            self.who_cache[channel] = []

            # Send the actual WHO command to the server. irc_RPL_WHOREPLY will
            # receive a response when the server sends one.
            self.sendLine("WHO %s" % channel)

    def irc_RPL_WHOREPLY(self, *nargs):
        "Receives reply from WHO command and sends to caller"
        response = nargs[1]

        # Same format as other events (nickname!ident@hostname)
        user = (
            response[5],  # nickname
            response[2],  # ident
            response[3],  # hostname
        )
        channel = response[1]

        self.who_cache[channel].append(user)

    def irc_RPL_ENDOFWHO(self, *nargs):
        "Called when WHO output is complete"
        response = nargs[1]
        channel = response[1]

        self.logger.info("Calling WHO callbacks for %s" % channel)
        for callback in self.who_callbacks[channel]:
            callback(self.who_cache[channel])

    def irc_NOTICE(self, prefix, params):
        """Called when a notice is sent to a channel or privately"""
        user = re.match(self.user_regex, prefix)
        channel = params[0]
        message = params[1]

        # Sent by network, not a real user
        if not user:
            self.logger.debug("%s sent notice to %s: %s" %
                              (prefix, channel, message))
            return

        self.logger.debug(
            "%s!%s@%s sent notice to %s: %s" %
            (user.group(1), user.group(2), user.group(3), channel, message))

        # Lots of NOTICE messages when connecting, and event manager may not be
        # initialized yet.
        if self.event_manager:
            self.event_manager.fire("irc.notice", user, channel, message)

    def irc_NICK(self, prefix, params):
        """Called when a user changes their nick"""
        user = re.match(self.user_regex, prefix)
        new_nick = params[0]

        self.logger.debug(
            "%s!%s@%s changed nick to %s" %
            (user.group(1), user.group(2), user.group(3), new_nick))

        self.event_manager.fire("irc.nick", user, new_nick)

    def irc_TOPIC(self, prefix, params):
        """Called when a new topic is set"""
        user = re.match(self.user_regex, prefix)
        channel = params[0]
        topic = params[1]

        self.logger.debug(
            "%s!%s@%s changed topic in %s to %s" %
            (user.group(1), user.group(2), user.group(3), channel, topic))

        self.event_manager.fire("irc.topic", user, channel, topic)

    def irc_MODE(self, prefix, params):
        """Called when a mode is set on a channel"""
        user = re.match(self.user_regex, prefix)
        channel = params[0]
        mode = ' '.join(params[1:])

        # Sent by network, not a real user
        if not user:
            self.logger.debug("%s set mode on %s (%s)" %
                              (prefix, channel, mode))
            return

        self.logger.debug(
            "%s!%s@%s set mode on %s (%s)" %
            (user.group(1), user.group(2), user.group(3), channel, mode))

        # Can get called during connection, in which case EventManager won't be
        # initialized yet
        if self.event_manager:
            self.event_manager.fire("irc.mode", user, channel, mode)

    def irc_JOIN(self, prefix, params):
        """Called when a user joins a channel"""
        user = re.match(self.user_regex, prefix)
        channel = params[0]

        self.logger.debug(
            "%s!%s@%s joined %s" %
            (user.group(1), user.group(2), user.group(3), channel))

        self.event_manager.fire("irc.join", user, channel)

    def irc_PART(self, prefix, params):
        """Called when a user parts a channel"""
        user = re.match(self.user_regex, prefix)
        channel = params[0]
        if len(params) == 1:
            reason = "No Message"
        else:
            reason = params[1]

        self.logger.debug(
            "%s!%s@%s parted %s (%s)" %
            (user.group(1), user.group(2), user.group(3), channel, reason))

        self.event_manager.fire("irc.part", user, channel, reason)

    def irc_KICK(self, prefix, params):
        """Called when a user is kicked from a channel"""
        user = re.match(self.user_regex, prefix)
        nick = params[0]
        channel = params[1]
        if len(params) == 2:
            reason = "No Message"
        else:
            reason = params[2]

        self.logger.debug("%s!%s@%s kicked %s from %s (%s)" %
                          (user.group(1), user.group(2), user.group(3), nick,
                           channel, reason))

        self.event_manager.fire("irc.kick", user, channel, nick, reason)

    def irc_QUIT(self, prefix, params):
        """Called when a user quits the network"""
        user = re.match(self.user_regex, prefix)
        if len(params) == 0:
            reason = "No Message"
        else:
            reason = params[0]

        self.logger.debug(
            "%s!%s@%s quit (%s)" %
            (user.group(1), user.group(2), user.group(3), reason))

        self.event_manager.fire("irc.quit", user, reason)

    def irc_unknown(self, prefix, command, params):
        """Called when Twisted doesn't understand an IRC command.

        Keyword arguments:
          prefix -- User sending command. Provided by Twisted.
          command -- Command that wasn't recognized. Provided by Twisted.
          params -- Params for command. Provided by Twisted.
        """
        # A user has invited us to a channel
        if command == "INVITE":
            # Break down the user into usable groups
            user = re.match(self.user_regex, prefix)
            channel = params[1]

            self.logger.debug("%s invited us to %s" % (user.group(1), channel))

            # Fire invite event, so plugins can hook into it
            self.event_manager.fire("irc.invite", user, channel)

            # TODO: Call matching plugin events

    def config(self, plugin):
        """Returns a given loaded plugin's config.

        Keyword arguments:
          plugin -- String containing a plugin name.

        Returns:
          dict -- Dictionary containing plugin config.

        Raises:
          ConfigNotFoundError - When config can't be found for the plugin.
        """
        if self.plugin_manager is None:
            self.logger.error(
                "PluginManager has not been initialized! Can't return config "
                "for plugin: %s" % plugin)
            raise PluginError("PluginManager has not yet been initialized")

        try:
            config = self.plugin_manager.get_config(plugin)
        except ConfigNotFoundError:
            # Log and raise the exception again
            self.logger.exception("Couldn't find config for plugin: %s" %
                                  plugin)
            raise

        return config

    def sendMsg(self, channel, message, length=None):
        """Wrapper command to send messages.

        Keyword arguments:
          channel -- Channel to send message to.
          message -- Message to send.
          length -- Length of message. Twisted will calculate if None given.
        """
        self.logger.info("Sending in %s: %s" % (channel, message))
        self.msg(channel, message, length)

    def disconnect(self, message=''):
        """Wrapper command to quit Cardinal.

        Keyword arguments:
          message -- Message to insert into QUIT, if any.
        """
        self.logger.info("Disconnecting from network")
        self.plugin_manager.unload_all()
        self.factory.disconnect = True
        self.quit(message)
Beispiel #10
0
 def test_constructor(self):
     manager = PluginManager(Mock(), [], [])
     assert len(manager.plugins) == 0
Beispiel #11
0
    def test_reload_for_error_in_constructor_succeeds(self):
        """Cardinal has a bug where constructor exceptions brick a plugin.

        Basically, the plugin won't be reloadable without a restart of the bot
        itself. This is obviously problematic for plugin development.

        This test aims to solve the problem and act as a regression test.
        """
        plugin = 'valid'
        with tempdir('cardinal_fixtures') as fixture_dir:
            with open(os.path.join(fixture_dir, '__init__.py'), 'w') as f:
                pass

            plugins_dir = os.path.join(fixture_dir, 'test_plugins')
            os.mkdir(plugins_dir)
            with open(os.path.join(plugins_dir, '__init__.py'), 'w') as f:
                pass

            plugin_dir = os.path.join(
                plugins_dir,
                plugin,
            )
            os.mkdir(plugin_dir)
            with open(os.path.join(plugin_dir, '__init__.py'), 'w') as f:
                pass

            # Write a plugin that will load successfully
            with open(
                os.path.join(
                    FIXTURE_PATH,
                    'fake_plugins',
                    'valid',
                    'plugin.py',
                ),
                'r'
            ) as f:
                valid_plugin = f.read()

            with open(os.path.join(plugin_dir, 'plugin.py'), 'w') as f:
                f.write(valid_plugin)

            # Make sure the fake_plugins dir is in the import path
            sys.path.insert(0, os.path.join(fixture_dir))
            try:
                # Load the plugin successfully
                plugin_manager = PluginManager(
                    self.cardinal,
                    [],
                    self.blacklist,
                    _plugin_module_import_prefix='test_plugins',
                    _plugin_module_directory=plugins_dir)

                self.assert_load_success(
                    [plugin],
                    plugin_manager=plugin_manager,
                )

                # Now overwrite with a plugin that will fail during load
                with open(os.path.join(plugin_dir, 'plugin.py'), 'w') as f:
                    f.write("""
class TestPlugin:
    def __init__(self):
        self.x = y  # this should raise


def setup():
    return TestPlugin()
""")

                # Verify the load failed
                self.assert_load_failed([plugin], plugin_manager)

                # Now back to a valid version
                with open(os.path.join(plugin_dir, 'plugin.py'), 'w') as f:
                    f.write(valid_plugin)

                # and then make sure the valid version does load successfully
                self.assert_load_success(
                    [plugin],
                    plugin_manager=plugin_manager,
                )
            finally:
                sys.path.pop(0)
Beispiel #12
0
class TestPluginManager:
    def setup_method(self):
        mock_cardinal = self.cardinal = Mock(spec=CardinalBot)
        mock_cardinal.nickname = 'Cardinal'
        mock_cardinal.event_manager = self.event_manager = \
            EventManager(mock_cardinal)

        self.blacklist = {}

        plugins_directory = os.path.abspath(os.path.join(
            os.path.dirname(os.path.realpath(os.path.abspath(__file__))),
            '..',
            'cardinal/fixtures/fake_plugins',
        ))
        self.plugin_manager = PluginManager(
            mock_cardinal,
            [],
            self.blacklist,
            _plugin_module_import_prefix='fake_plugins',
            _plugin_module_directory=plugins_directory,
        )

    def assert_load_success(self,
                            plugins,
                            assert_callbacks_is_empty=True,
                            assert_commands_is_empty=True,
                            assert_blacklist_is_empty=True,
                            assert_config_is_none=True,
                            plugin_manager=None):
        plugin_manager = plugin_manager or self.plugin_manager
        failed_plugins = plugin_manager.load(plugins)

        # regardless of the whether plugins was a string or list, the rest of
        # this function requires it to be a list
        if not isinstance(plugins, list):
            plugins = [plugins]

        assert failed_plugins == []
        assert len(list(plugin_manager.plugins.keys())) == len(plugins)

        for name in plugins:
            # class name for plugins must be Test(CamelCaseName)Plugin
            # e.g. an_example -> TestAnExamplePlugin
            class_ = 'Test'
            name_pieces = name.split('_')
            for name_piece in name_pieces:
                class_ += name_piece[0].upper() + name_piece[1:].lower()
            class_ += 'Plugin'

            # check that everything was set correctly
            assert name in list(plugin_manager.plugins.keys())
            assert plugin_manager.plugins[name]['name'] == name
            assert isinstance(
                plugin_manager.plugins[name]['instance'],
                object,
            )
            if assert_commands_is_empty:
                assert plugin_manager.plugins[name]['commands'] == []
            if assert_callbacks_is_empty:
                assert plugin_manager.plugins[name]['callbacks'] == []
                assert plugin_manager.plugins[name]['callback_ids'] == {}
            if assert_config_is_none:
                assert plugin_manager.plugins[name]['config'] is None
            if assert_blacklist_is_empty:
                assert plugin_manager.plugins[name]['blacklist'] == []

    def assert_load_failed(self, plugins, plugin_manager=None):
        plugin_manager = plugin_manager or self.plugin_manager
        failed_plugins = plugin_manager.load(plugins)

        # regardless of the whether plugins was a string or list, the rest of
        # this function requires it to be a list
        if not isinstance(plugins, list):
            plugins = [plugins]

        assert failed_plugins == plugins
        assert plugin_manager.plugins == {}

    def test_constructor(self):
        manager = PluginManager(Mock(), [], [])
        assert len(manager.plugins) == 0

    @patch.object(PluginManager, 'load')
    def test_constructor_loads_plugins(self, load):
        plugins = ['foo', 'bar']
        PluginManager(Mock(), plugins, [])
        load.assert_called_with(plugins)

    @pytest.mark.parametrize("plugins", [
        'a string',
        12345,
        0.0,
        object(),
    ])
    def test_constructor_plugins_not_a_list_typeerror(self, plugins):
        with pytest.raises(TypeError):
            PluginManager(Mock(), plugins)

    @pytest.mark.parametrize("plugins", [
        12345,
        0.0,
        object(),
    ])
    def test_load_plugins_not_a_list_or_string_typeerror(self, plugins):
        with pytest.raises(TypeError):
            self.plugin_manager.load(plugins)

    @pytest.mark.parametrize("plugins", [
        # This plugin won't be found in the plugins directory
        'nonexistent',
        # This plugin is missing a setup() function
        'setup_missing',
        # This plugin's setup() function takes three arguments
        'setup_too_many_arguments',
        # This plugin's entrypoint isn't callable
        'entrypoint_invalid',
    ])
    def test_load_invalid(self, plugins):
        self.assert_load_failed(plugins)

    @pytest.mark.parametrize("plugins", [
        'valid',
        'entrypoint',
        ['valid'],  # test list format
        # This plugin has both a config.yaml and config.json which used to be
        # disallowed, but now config.yaml is ignored
        'config_ambiguous',
    ])
    def test_load_valid(self, plugins):
        self.assert_load_success(plugins, assert_config_is_none=False)

    def test_load_cardinal_passed(self):
        name = 'setup_one_argument'
        self.assert_load_success(name)
        assert self.plugin_manager.plugins[name]['instance'].cardinal is \
            self.cardinal

    def test_load_config_passed(self):
        name = 'setup_two_arguments'
        self.assert_load_success(name, assert_config_is_none=False)
        assert self.plugin_manager.plugins[name]['instance'].cardinal is \
            self.cardinal
        assert self.plugin_manager.plugins[name]['instance'].config == \
            {'test': True}

    def test_load_config_passed_opposite_order(self):
        name = 'setup_config_cardinal_order'
        self.assert_load_success(name, assert_config_is_none=False)
        assert self.plugin_manager.plugins[name]['instance'].cardinal is \
            self.cardinal
        assert self.plugin_manager.plugins[name]['instance'].config == \
            {'test': True}

    def test_load_invalid_json_config(self):
        name = 'config_invalid_json'
        self.assert_load_success(name)  # no error for some reason

        # invalid json should be ignored
        assert self.plugin_manager.plugins[name]['config'] is None

    def test_get_config_unloaded_plugin(self):
        name = 'nonexistent_plugin'
        with pytest.raises(exceptions.ConfigNotFoundError):
            self.plugin_manager.get_config(name)

    def test_get_config_plugin_without_config(self):
        name = 'valid'
        self.assert_load_success(name)

        with pytest.raises(exceptions.ConfigNotFoundError):
            self.plugin_manager.get_config(name)

    def test_get_config_json(self):
        name = 'config_valid_json'
        self.assert_load_success(name, assert_config_is_none=False)

        assert self.plugin_manager.get_config(name) == {'test': True}

    def test_get_config_yaml_ignored(self):
        name = 'config_valid_yaml'
        self.assert_load_success(name, assert_config_is_none=False)

        with pytest.raises(exceptions.ConfigNotFoundError):
            self.plugin_manager.get_config(name)

    def test_plugin_iteration(self):
        plugins = [
            'setup_one_argument',
            'setup_two_arguments',
            'valid',
        ]
        self.assert_load_success(plugins, assert_config_is_none=False)

        for plugin in self.plugin_manager:
            assert plugin == self.plugin_manager.plugins[plugin['name']]

    def test_reload_valid_succeeds(self):
        name = 'valid'
        plugins = [name]

        # first load is not a reload
        self.assert_load_success(plugins)

        # second load is
        self.assert_load_success(plugins)

        # and so on...
        self.assert_load_success(plugins)

    def test_reload_exception_in_close_succeeds(self):
        name = 'close_raises_exception'
        plugins = [name]

        self.assert_load_success(plugins)

        # should reload successfully despite bad close()
        self.assert_load_success(plugins)

    def test_reload_reregisters_event(self):
        name = 'registers_event'
        plugins = [name]

        assert 'test.event' not in self.event_manager.registered_events

        self.assert_load_success(plugins)
        assert 'test.event' in self.event_manager.registered_events

        self.assert_load_success(plugins)
        assert 'test.event' in self.event_manager.registered_events

    def test_reload_for_error_in_constructor_succeeds(self):
        """Cardinal has a bug where constructor exceptions brick a plugin.

        Basically, the plugin won't be reloadable without a restart of the bot
        itself. This is obviously problematic for plugin development.

        This test aims to solve the problem and act as a regression test.
        """
        plugin = 'valid'
        with tempdir('cardinal_fixtures') as fixture_dir:
            with open(os.path.join(fixture_dir, '__init__.py'), 'w') as f:
                pass

            plugins_dir = os.path.join(fixture_dir, 'test_plugins')
            os.mkdir(plugins_dir)
            with open(os.path.join(plugins_dir, '__init__.py'), 'w') as f:
                pass

            plugin_dir = os.path.join(
                plugins_dir,
                plugin,
            )
            os.mkdir(plugin_dir)
            with open(os.path.join(plugin_dir, '__init__.py'), 'w') as f:
                pass

            # Write a plugin that will load successfully
            with open(
                os.path.join(
                    FIXTURE_PATH,
                    'fake_plugins',
                    'valid',
                    'plugin.py',
                ),
                'r'
            ) as f:
                valid_plugin = f.read()

            with open(os.path.join(plugin_dir, 'plugin.py'), 'w') as f:
                f.write(valid_plugin)

            # Make sure the fake_plugins dir is in the import path
            sys.path.insert(0, os.path.join(fixture_dir))
            try:
                # Load the plugin successfully
                plugin_manager = PluginManager(
                    self.cardinal,
                    [],
                    self.blacklist,
                    _plugin_module_import_prefix='test_plugins',
                    _plugin_module_directory=plugins_dir)

                self.assert_load_success(
                    [plugin],
                    plugin_manager=plugin_manager,
                )

                # Now overwrite with a plugin that will fail during load
                with open(os.path.join(plugin_dir, 'plugin.py'), 'w') as f:
                    f.write("""
class TestPlugin:
    def __init__(self):
        self.x = y  # this should raise


def setup():
    return TestPlugin()
""")

                # Verify the load failed
                self.assert_load_failed([plugin], plugin_manager)

                # Now back to a valid version
                with open(os.path.join(plugin_dir, 'plugin.py'), 'w') as f:
                    f.write(valid_plugin)

                # and then make sure the valid version does load successfully
                self.assert_load_success(
                    [plugin],
                    plugin_manager=plugin_manager,
                )
            finally:
                sys.path.pop(0)

    @pytest.mark.parametrize("plugins", [
        12345,
        0.0,
        object(),
    ])
    def test_unload_plugins_not_a_list_or_string_typeerror(self, plugins):
        with pytest.raises(TypeError):
            self.plugin_manager.unload(plugins)

    def test_unload_plugins_never_loaded_plugin_fails(self):
        name = 'test_never_loaded_plugin'
        plugins = [name]

        assert self.plugin_manager.plugins == {}

        failed_plugins = self.plugin_manager.unload(plugins)

        assert failed_plugins == plugins
        assert self.plugin_manager.plugins == {}

    def test_unload_exception_in_close_fails(self):
        name = 'close_raises_exception'
        plugins = [name]

        self.assert_load_success(plugins)

        failed_plugins = self.plugin_manager.unload(plugins)

        # Failed plugins represents a list of plugins that errored on unload
        # but the plugin should still be removed from the PluginManager.
        #
        # FIXME Unfortunately, this means that a plugin might fail to remove
        # its callbacks from an event, and then be inaccessible by Cardinal.
        assert failed_plugins == plugins
        assert self.plugin_manager.plugins == {}

    @pytest.mark.parametrize("plugins", [
        # This plugin contains no close() method
        'valid',
        ['valid'],  # test list format
        # This plugin has a no-op close() method
        'close_no_arguments',
    ])
    def test_unload_valid_succeeds(self, plugins):
        self.assert_load_success(plugins)

        failed_plugins = self.plugin_manager.unload(plugins)

        assert failed_plugins == []
        assert list(self.plugin_manager.plugins.keys()) == []

    def test_unload_passes_cardinal(self):
        plugin = 'close_one_argument'

        self.assert_load_success(plugin)
        instance = self.plugin_manager.plugins[plugin]['instance']

        failed_plugins = self.plugin_manager.unload(plugin)

        assert failed_plugins == []
        assert list(self.plugin_manager.plugins.keys()) == []

        # Our close() method will set module.cardinal for us to inspect
        assert instance.cardinal is self.cardinal

    def test_unload_too_many_arguments_in_close(self):
        plugin = 'close_too_many_arguments'

        self.assert_load_success(plugin)
        instance = self.plugin_manager.plugins[plugin]['instance']

        failed_plugins = self.plugin_manager.unload(plugin)

        assert failed_plugins == [plugin]
        assert list(self.plugin_manager.plugins.keys()) == []

        # Our close() method will set module.called to True if called
        assert instance.called is False

    def test_unload_all(self):
        self.assert_load_success([
            'valid',
            'close_no_arguments',
            'close_too_many_arguments',
        ])

        assert len(self.plugin_manager.plugins) == 3

        # Doesn't return what failed to unload cleanly, but should unload
        # everything regardless
        self.plugin_manager.unload_all()

        assert self.plugin_manager.plugins == {}

    def test_event_callback_registered(self):
        name = 'event_callback'
        event = 'irc.raw'

        self.assert_load_success(name, assert_callbacks_is_empty=False)
        instance = self.plugin_manager.plugins[name]['instance']

        # test that plugin manager is tracking the callback
        assert len(self.plugin_manager.plugins[name]['callback_ids']) == 1
        assert self.plugin_manager.plugins[name]['callbacks'] == [
            {
                'event_names': [event],
                'method': instance.irc_raw_callback,
            }
        ]

        # test that event manager had callback registered
        self.event_manager.register(event, 1)

        message = 'this is a test message'
        self.event_manager.fire(event, message)

        assert instance.cardinal is self.cardinal
        assert instance.messages == [message]

        self.event_manager.fire(event, message)
        assert instance.messages == [message, message]

    def test_event_callback_unregistered(self):
        name = 'event_callback'
        event = 'irc.raw'

        self.assert_load_success(name, assert_callbacks_is_empty=False)
        instance = self.plugin_manager.plugins[name]['instance']

        # make sure an event is sent to the callback
        self.event_manager.register(event, 1)
        message = 'this is a test message'
        self.event_manager.fire(event, message)
        assert instance.messages == [message]

        # unload and make sure no more events are sent
        self.plugin_manager.unload(name)

        self.event_manager.fire(event, message)
        assert instance.messages == [message]

    def test_load_bad_callback_fails(self):
        name = 'event_callback'
        event = 'irc.raw'

        # this will cause registration to fail, as our callback only takes 1
        # param other than cardinal
        self.event_manager.register(event, 2)

        self.assert_load_failed([name])

    @defer.inlineCallbacks
    def test_load_multiple_callbacks_one_fails(self):
        name = 'multiple_event_callbacks_one_fails'
        event = 'foo'

        self.event_manager.register(event, 0)
        self.assert_load_failed([name])

        # if this actually fires the one good callback we should get an
        # exception (we don't expect one)
        res = yield self.event_manager.fire(event)

        # this should return False if no callbacks fired
        assert res is False

    def test_load_command_registration(self):
        name = 'commands'

        self.assert_load_success(name, assert_commands_is_empty=False)
        instance = self.plugin_manager.plugins[name]['instance']

        assert self.plugin_manager.plugins[name]['commands'] == [
            instance.command1,
            instance.command2,
            instance.regex_command,
        ]

    def test_itercommands(self):
        name = 'commands'

        self.assert_load_success(name, assert_commands_is_empty=False)
        instance = self.plugin_manager.plugins[name]['instance']
        commands = [
            instance.command1,
            instance.command2,
            instance.regex_command,
        ]

        for command in self.plugin_manager.itercommands():
            commands.remove(command)

        assert commands == []

    @defer.inlineCallbacks
    def test_call_command_no_regex_match(self):
        yield self.plugin_manager.call_command(('nick', 'ident', 'host'),
                                               "#channel",
                                               "this isn't a command")

    @defer.inlineCallbacks
    def test_call_command_no_command_match(self):
        # FIXME: this isn't really an error...
        with pytest.raises(exceptions.CommandNotFoundError):
            yield self.plugin_manager.call_command(('nick', 'ident', 'host'),
                                                   "#channel",
                                                   ".command")

    @defer.inlineCallbacks
    def test_command_called(self):
        name = 'commands'

        self.assert_load_success(name, assert_commands_is_empty=False)
        instance = self.plugin_manager.plugins[name]['instance']

        user = ('user', 'ident', 'vhost')
        channel = '#channel'
        message = '.command1 foobar'

        expected_calls = []
        assert instance.command1_calls == expected_calls
        assert instance.command2_calls == []
        assert instance.regex_command_calls == []

        expected_calls.append((self.cardinal, user, channel, message))
        yield self.plugin_manager.call_command(user, channel, message)
        assert instance.command1_calls == expected_calls
        assert instance.command2_calls == []
        assert instance.regex_command_calls == []

        message = '.command1_alias bar bar bar'
        expected_calls.append((self.cardinal, user, channel, message))
        yield self.plugin_manager.call_command(user, channel, message)
        assert instance.command1_calls == expected_calls
        assert instance.command2_calls == []
        assert instance.regex_command_calls == []

        message = 'this shouldnt trigger'
        yield self.plugin_manager.call_command(user, channel, message)
        assert instance.command1_calls == expected_calls
        assert instance.command2_calls == []
        assert instance.regex_command_calls == []

    @defer.inlineCallbacks
    def test_regex_command_called(self):
        name = 'commands'

        self.assert_load_success(name, assert_commands_is_empty=False)
        instance = self.plugin_manager.plugins[name]['instance']

        user = ('user', 'ident', 'vhost')
        channel = '#channel'
        message = 'regex foobar'

        expected_calls = []
        assert instance.regex_command_calls == expected_calls
        assert instance.command1_calls == []
        assert instance.command2_calls == []

        expected_calls.append((self.cardinal, user, channel, message))
        yield self.plugin_manager.call_command(user, channel, message)
        assert instance.regex_command_calls == expected_calls
        assert instance.command1_calls == []
        assert instance.command2_calls == []

        message = 'regex bar bar bar'
        expected_calls.append((self.cardinal, user, channel, message))
        yield self.plugin_manager.call_command(user, channel, message)
        assert instance.regex_command_calls == expected_calls
        assert instance.command1_calls == []
        assert instance.command2_calls == []

        message = 'this shouldnt trigger'
        yield self.plugin_manager.call_command(user, channel, message)
        assert instance.regex_command_calls == expected_calls
        assert instance.command1_calls == []
        assert instance.command2_calls == []

    @defer.inlineCallbacks
    def test_command_raises_exception_caught(self):
        name = 'command_raises_exception'

        self.assert_load_success(name, assert_commands_is_empty=False)
        instance = self.plugin_manager.plugins[name]['instance']

        user = ('user', 'ident', 'vhost')
        channel = '#channel'
        message = '.command foobar'

        expected_calls = []
        assert instance.command_calls == expected_calls

        expected_calls.append((self.cardinal, user, channel, message))
        yield self.plugin_manager.call_command(user, channel, message)
        assert instance.command_calls == expected_calls

    def test_blacklist_unloaded_plugin(self):
        name = 'commands'
        channel = '#channel'

        assert self.plugin_manager.blacklist(name, channel) is False

    def test_blacklist_invalid_channel(self):
        name = 'commands'
        channel = 321

        with pytest.raises(TypeError):
            self.plugin_manager.blacklist(name, channel)

    def test_blacklist(self):
        name = 'commands'
        channel = '#channel'
        self.assert_load_success(name, assert_commands_is_empty=False)

        assert self.plugin_manager.blacklist(name, channel) is True
        assert self.plugin_manager.plugins[name]['blacklist'] == [channel]

    def test_blacklist_from_config(self):
        name = 'commands'
        channel = '#channel'

        # this works since dict is by-reference in Python
        self.blacklist[name] = [channel]
        self.assert_load_success(name,
                                 assert_commands_is_empty=False,
                                 assert_blacklist_is_empty=False)

        assert self.plugin_manager.plugins[name]['blacklist'] == [channel]

    def test_blacklist_multiple_channels(self):
        name = 'commands'
        channels = ['#channel1', '#channel2']
        self.assert_load_success(name, assert_commands_is_empty=False)

        assert self.plugin_manager.blacklist(name, channels) is True
        assert self.plugin_manager.plugins[name]['blacklist'] == channels

    def test_unblacklist_invalid_channel(self):
        name = 'commands'
        channel = 321

        with pytest.raises(TypeError):
            self.plugin_manager.unblacklist(name, channel)

    def test_unblacklist_unloaded_plugin(self):
        name = 'commands'
        channel = '#channel'

        assert self.plugin_manager.unblacklist(name, channel) is False

    def test_unblacklist(self):
        name = 'commands'
        channel = '#channel'
        self.assert_load_success(name, assert_commands_is_empty=False)

        assert self.plugin_manager.blacklist(name, channel) is True
        assert self.plugin_manager.plugins[name]['blacklist'] == [channel]

        assert self.plugin_manager.unblacklist(name, channel) == []
        assert self.plugin_manager.plugins[name]['blacklist'] == []

    def test_unblacklist_multiple_channels(self):
        name = 'commands'
        channels = ['#channel1', '#channel2']
        self.assert_load_success(name, assert_commands_is_empty=False)

        assert self.plugin_manager.blacklist(name, channels) is True
        assert self.plugin_manager.plugins[name]['blacklist'] == channels

        assert self.plugin_manager.unblacklist(name, channels) == []
        assert self.plugin_manager.plugins[name]['blacklist'] == []

    def test_unblacklist_non_blacklisted_channels(self):
        name = 'commands'
        channel = '#channel'
        self.assert_load_success(name, assert_commands_is_empty=False)

        assert self.plugin_manager.blacklist(name, channel) is True
        assert self.plugin_manager.plugins[name]['blacklist'] == [channel]

        assert self.plugin_manager.unblacklist(
            name, [channel, '#notblacklisted']) == ['#notblacklisted']
        assert self.plugin_manager.plugins[name]['blacklist'] == []

    def test_itercommands_adheres_to_blacklist(self):
        name = 'commands'
        channel = '#channel'

        self.assert_load_success(name, assert_commands_is_empty=False)
        self.plugin_manager.blacklist(name, channel)

        commands = [command for command in self.plugin_manager.itercommands()]
        assert len(commands) == 3

        commands = [command for command in
                    self.plugin_manager.itercommands(channel)]
        assert len(commands) == 0

    @defer.inlineCallbacks
    def test_call_command_adheres_to_blacklist(self):
        name = 'commands'
        channel = '#channel'

        self.assert_load_success(name, assert_commands_is_empty=False)
        instance = self.plugin_manager.plugins[name]['instance']

        self.plugin_manager.blacklist(name, channel)

        user = ('user', 'ident', 'vhost')
        message = '.command1 foobar'

        expected_calls = []
        assert instance.command1_calls == expected_calls
        # FIXME this should maybe not fire if we find a command but it is
        # blacklisted
        with pytest.raises(exceptions.CommandNotFoundError):
            yield self.plugin_manager.call_command(user, channel, message)
        assert instance.command1_calls == expected_calls
Beispiel #13
0
 def test_constructor_plugins_not_a_list_typeerror(self, plugins):
     with pytest.raises(TypeError):
         PluginManager(Mock(), plugins)
Beispiel #14
0
 def test_constructor_loads_plugins(self, load):
     plugins = ['foo', 'bar']
     PluginManager(Mock(), plugins, [])
     load.assert_called_with(plugins)