Exemplo n.º 1
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)
Exemplo n.º 2
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
Exemplo n.º 3
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)
Exemplo n.º 4
0
Arquivo: bot.py Projeto: s-ol/Cardinal
class CardinalBot(irc.IRCClient, object):
    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, e:
            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
Exemplo n.º 5
0
class TestEventManager:
    def setup_method(self):
        mock_cardinal = self.cardinal = Mock(spec=CardinalBot)
        self.event_manager = EventManager(mock_cardinal)

    def _callback(self, cardinal):
        """Used as a test callback."""

    def assert_register_success(self, name, params=0):
        self.event_manager.register(name, params)
        assert name in self.event_manager.registered_events
        assert name in self.event_manager.registered_callbacks
        assert self.event_manager.registered_events[name] == params

    def assert_register_callback_success(self, name, callback=None):
        callback = callback or self._callback

        callback_count = len(self.event_manager.registered_callbacks[name]) \
            if name in self.event_manager.registered_callbacks else 0

        # callback id is used for removal
        callback_id = self.event_manager.register_callback(name, callback)

        assert isinstance(callback_id, str)
        assert len(self.event_manager.registered_callbacks[name]) == \
            callback_count + 1

        return callback_id

    def assert_remove_callback_success(self, name, callback_id):
        callback_count = len(self.event_manager.registered_callbacks[name])

        self.event_manager.remove_callback(name, callback_id)

        assert len(self.event_manager.registered_callbacks[name]) == \
            callback_count - 1

    def test_constructor(self):
        assert self.event_manager.cardinal == self.cardinal
        assert isinstance(self.event_manager.logger, logging.Logger)
        assert isinstance(self.event_manager.registered_events, dict)
        assert isinstance(self.event_manager.registered_callbacks, dict)

    def test_register(self):
        name = 'test_event'
        self.assert_register_success(name)
        assert len(self.event_manager.registered_callbacks[name]) == 0

    def test_register_duplicate_event(self):
        name = 'test_event'

        self.assert_register_success(name)

        with pytest.raises(exceptions.EventAlreadyExistsError):
            self.event_manager.register(name, 1)

    @pytest.mark.parametrize("param_count", [
        3.14,
        'foobar',
        object(),
    ])
    def test_register_invalid_param_count(self, param_count):
        with pytest.raises(TypeError):
            self.event_manager.register('test_event', param_count)

    def test_remove(self):
        name = 'test_event'
        self.assert_register_success(name)

        self.event_manager.remove(name)

        assert name not in self.event_manager.registered_events
        assert name not in self.event_manager.registered_callbacks

    def test_remove_event_doesnt_exist(self):
        name = 'test_event'

        with pytest.raises(exceptions.EventDoesNotExistError):
            self.event_manager.remove(name)

    def test_add_callback(self):
        def callback(cardinal):
            pass

        name = 'test_event'
        self.assert_register_success(name)

        self.assert_register_callback_success(name, callback)

    def test_add_callback_registered_varargs(self):
        def callback(*args):
            pass

        name = 'test_event'
        self.assert_register_success(name, params=3)

        self.assert_register_callback_success(name, callback)

    def test_add_callback_non_callable(self):
        callback = 'this is not callable'

        name = 'test_event'
        self.assert_register_success(name)

        with pytest.raises(exceptions.EventCallbackError):
            self.event_manager.register_callback(name, callback)

    def test_add_callback_without_registered_event(self):
        # only accepts cardinal
        def callback(cardinal):
            pass

        # accepts cardinal and another param
        def callback2(cardinal, _):
            pass

        name = 'test_event'

        # should not validate params aside from at least accepting cardinal
        self.assert_register_callback_success(name, callback)
        self.assert_register_callback_success(name, callback2)

    def test_add_callback_method_ignores_self(self):
        name = 'test_event'

        self.assert_register_success(name)
        self.assert_register_callback_success(name, self._callback)

    def test_add_callback_validates_required_args(self):
        def callback(cardinal, one_too_many_args):
            pass

        name = 'test_event'

        self.assert_register_success(name)
        with pytest.raises(exceptions.EventCallbackError):
            self.event_manager.register_callback(name, callback)

    def test_add_callback_requires_cardinal_arg(self):
        def callback():
            pass

        name = 'test_event'

        with pytest.raises(exceptions.EventCallbackError):
            self.event_manager.register_callback(name, callback)

    def test_add_callback_varargs_success(self):
        def callback(*args, **kwargs):
            pass

        name = 'test_event'

        self.event_manager.register_callback(name, callback)

    def test_add_callback_requires_keyword_fails(self):
        def callback(cardinal, *args, foobar):
            pass

        name = 'test_event'

        with pytest.raises(exceptions.EventCallbackError):
            self.event_manager.register_callback(name, callback)

    def test_add_callback_deferred_signature(self):
        @defer.inlineCallbacks
        def callback(cardinal):
            pass

        name = 'test_event'

        self.event_manager.register_callback(name, callback)

    def test_remove_callback(self):
        name = 'test_event'

        cb_id = self.assert_register_callback_success(name)
        self.assert_remove_callback_success(name, cb_id)

    def test_remove_callback_nonexistent_event_silent(self):
        name = 'test_event'
        callback_id = 'nonexistent'

        assert name not in self.event_manager.registered_callbacks
        self.event_manager.remove_callback(name, callback_id)

    def test_remove_callback_nonexistent_callback_silent(self):
        name = 'test_event'
        callback_id = 'nonexistent'

        self.assert_register_success(name)

        assert len(self.event_manager.registered_callbacks[name]) == 0
        self.event_manager.remove_callback(name, callback_id)
        assert len(self.event_manager.registered_callbacks[name]) == 0

    def test_fire_event_does_not_exist(self):
        name = 'test_event'

        with pytest.raises(exceptions.EventDoesNotExistError):
            self.event_manager.fire(name)

    @defer.inlineCallbacks
    def test_fire(self):
        args = []

        def callback(*fargs):
            for arg in fargs:
                args.append(arg)

        name = 'test_event'

        self.assert_register_success(name)
        self.assert_register_callback_success(name, callback)

        accepted = yield self.event_manager.fire(name)

        assert accepted is True
        assert args == [self.cardinal]

    @defer.inlineCallbacks
    def test_fire_callback_rejects(self):
        args = []

        def callback(*fargs):
            for arg in fargs:
                args.append(arg)
            raise exceptions.EventRejectedMessage()

        name = 'test_event'

        self.assert_register_success(name)
        self.assert_register_callback_success(name, callback)

        accepted = yield self.event_manager.fire(name)

        assert accepted is False
        assert args == [self.cardinal]

    @defer.inlineCallbacks
    def test_fire_multiple_callbacks(self):
        args = []

        def generate_cb(reject):
            def callback(*fargs):
                for arg in fargs:
                    args.append(arg)
                if reject:
                    raise exceptions.EventRejectedMessage()
            return callback

        name = 'test_event'

        self.assert_register_success(name)
        self.assert_register_callback_success(name, generate_cb(True))
        self.assert_register_callback_success(name, generate_cb(False))
        self.assert_register_callback_success(name, generate_cb(False))

        accepted = yield self.event_manager.fire(name)

        assert accepted is True
        assert args == [self.cardinal, self.cardinal, self.cardinal]

    @defer.inlineCallbacks
    def test_fire_multiple_callbacks_all_reject(self):
        def generate_cb(reject):
            def callback(*fargs):
                if reject:
                    raise exceptions.EventRejectedMessage()
            return callback

        name = 'test_event'

        self.assert_register_success(name)
        self.assert_register_callback_success(name, generate_cb(True))
        self.assert_register_callback_success(name, generate_cb(True))

        accepted = yield self.event_manager.fire(name)

        assert accepted is False

    @defer.inlineCallbacks
    def test_fire_multiple_callbacks_one_errors(self):
        def generate_cb(error):
            def callback(*fargs):
                if error:
                    raise Exception()
            return callback

        name = 'test_event'

        self.assert_register_success(name)
        self.assert_register_callback_success(name, generate_cb(False))
        self.assert_register_callback_success(name, generate_cb(True))

        accepted = yield self.event_manager.fire(name)

        assert accepted is True

    @defer.inlineCallbacks
    def test_fire_multiple_callbacks_all_error(self):
        def generate_cb(error):
            def callback(*fargs):
                if error:
                    raise Exception()
            return callback

        name = 'test_event'

        self.assert_register_success(name)
        self.assert_register_callback_success(name, generate_cb(True))
        self.assert_register_callback_success(name, generate_cb(True))
        self.assert_register_callback_success(name, generate_cb(True))

        accepted = yield self.event_manager.fire(name)

        assert accepted is False

    def test_add_callback_wont_duplicate_id(self):
        name = 'test_event'

        with patch.object(self.event_manager, '_generate_id') as mock_gen_id:
            mock_gen_id.side_effect = ['ABC123', 'ABC123', 'DEF456']
            event_id1 = self.assert_register_callback_success(name)
            event_id2 = self.assert_register_callback_success(name)

        assert event_id1 == 'ABC123'
        assert event_id2 != 'ABC123'