示例#1
0
    def __init__(self, server, language='aml'):
        """
        @type   server:     firefly.containers.Server

        @type   language:   C{str}
        @param  language:   The language engine to use for this instance.
        """
        # Set up logging
        self._log = logging.getLogger('firefly')

        self.language = None
        """@type : firefly.languages.interface.LanguageInterface"""
        self._load_language_interface(language)

        # Set up our registry and server containers, then run setup
        self.registry = _Registry(self)
        self.server_info = ServerInfo()
        self.server = server
        self._setup()
        self.load_language_files()

        # Set up our authentication manager
        self.auth = Auth(self)

        # Finally, now that everything is set up, load our plugins
        self.plugins = pkg_resources.get_entry_map('firefly_irc', 'firefly.plugins')
        scanner = venusian.Scanner(firefly=self)
        scanner.scan(plugins)
示例#2
0
class FireflyIRC(IRCClient):
    """
    Firefly IRC client.
    """
    # Default nick
    nickname = "Firefly"

    # Path constants
    CONFIG_DIR = os.path.join(appdirs.user_config_dir('firefly'), 'irc')
    DATA_DIR   = os.path.join(appdirs.user_data_dir('firefly'), 'irc')
    LOG_DIR    = os.path.join(appdirs.user_log_dir('firefly'), 'irc')

    def __init__(self, server, language='aml'):
        """
        @type   server:     firefly.containers.Server

        @type   language:   C{str}
        @param  language:   The language engine to use for this instance.
        """
        # Set up logging
        self._log = logging.getLogger('firefly')

        self.language = None
        """@type : firefly.languages.interface.LanguageInterface"""
        self._load_language_interface(language)

        # Set up our registry and server containers, then run setup
        self.registry = _Registry(self)
        self.server_info = ServerInfo()
        self.server = server
        self._setup()
        self.load_language_files()

        # Set up our authentication manager
        self.auth = Auth(self)

        # Finally, now that everything is set up, load our plugins
        self.plugins = pkg_resources.get_entry_map('firefly_irc', 'firefly.plugins')
        scanner = venusian.Scanner(firefly=self)
        scanner.scan(plugins)

    @staticmethod
    def load_configuration(name, plugin=None, basedir=None, default=None, ext='.cfg'):
        """
        Load a single configuration file.

        @type   name:       str
        @param  name:       Name of the configuration file *WITHOUT* the .cfg file extension

        @type   plugin:     PluginAbstract or None
        @param  plugin:     Plugin to load a configuration file from, or None to load a system configuration file.

        @type   basedir:    str or None
        @param  basedir:    Optional config path prefix

        @type   default:    str or None
        @param  default:    Name of the default configuration file. Defaults to the name argument.

        @type   ext:        str or None
        @param  ext:        Configuration file extension. None for no extension (e.g. server configuration files).

        @raise  ValueError: Raised if the supplied configuration file does not exist

        @rtype: ConfigParser
        """
        log = logging.getLogger('firefly')
        ext = ext or ''  # If None, we need to convert the extension to an empty string
        paths = []

        ################################
        # Default Configuration        #
        ################################

        app_path = plugin.plugin_path if plugin else os.path.dirname(os.path.realpath(__file__))
        app_path = os.path.join(app_path, 'config')

        if basedir:
            app_path = os.path.join(app_path, basedir)

        app_path = os.path.join(app_path, '{fn}{ext}'.format(fn=default or name, ext='.cfg' if default else ext))

        # Make sure the configuration file actually exists
        if not os.path.isfile(app_path):
            raise ValueError('Default configuration file %s does not exist', app_path)

        paths.append(app_path)

        ################################
        # User Configuration           #
        ################################

        user_path = os.path.join(FireflyIRC.CONFIG_DIR, 'config')
        if plugin:
            user_path = os.path.join(user_path, 'plugins', plugin.name)
        if basedir:
            user_path = os.path.join(user_path, basedir)

        # Make sure the directory exists
        if not os.path.isdir(user_path):
            log.info('Creating user configuration directory: %s', user_path)
            os.makedirs(user_path, 0o755)

        user_path = os.path.join(user_path, '{fn}{ext}'.format(fn=name, ext=ext))
        log.debug('Attempting to load user configuration file: %s', user_path)

        if not os.path.isfile(user_path):
            if default:
                raise ValueError('User configuration file %s does not exist', app_path)  # TODO: Ambiguous exceptions

            log.debug('User configuration file does not exist, attempting to create it')

            # Load the base system path
            shutil.copyfile(app_path, user_path)

        paths.append(user_path)

        # Load the configuration files
        config = ConfigParser()
        log.debug('Attempting to load configuration files: %s', str(paths))
        result = config.read(paths)
        log.debug('Configuration files loaded: %s', str(result))
        return config

    def _load_language_interface(self, language):
        """
        Load and instantiate the specific language engine.

        @type   language:   C{str}
        """
        self._log.info('Loading language interface: {lang}'.format(lang=language))
        try:
            module = importlib.import_module('firefly.languages.{module}'.format(module=language))
            self.language = module.__LANGUAGE_CLASS__()
        except ImportError as e:
            self._log.error('ImportError raised when loading language')
            raise LanguageImportError('Unable to import language engine "{lang}": {err}'
                                      .format(lang=language, err=e.message))
        except AttributeError:
            self._log.error('Language module does not contain a specified language class, load failed')
            raise LanguageImportError('Language "{lang}" does not have a defined __LANGUAGE_CLASS__'
                                      .format(lang=language))

    def load_language_files(self, plugin=None, basedir=None, container=None):
        """
        Load language files.

        @type   plugin:     PluginAbstract or None
        @param  plugin:     Plugin to load language files from, or None to load user language files.

        @type   basedir:    str or None
        @param  basedir:    Language basedir. Defaults to lang

        @type   container:  str or None
        @param  container:  Name of the identity container. Defaults to the system configuration.
        """
        basedir = basedir or 'lang'
        container = container or self.server.identity.container
        lang_path = plugin.plugin_path if plugin else FireflyIRC.CONFIG_DIR
        lang_path = os.path.join(lang_path, basedir, container)

        # Make sure the configuration file actually exists
        if not os.path.exists(lang_path):
            self._log.warn('Language directory %s does not exist', lang_path)
            return

        self.language.load_directory(lang_path)

    def _setup(self):
        """
        Run generic setup tasks.
        """
        # Load language files
        lang_dir = os.path.join(self.CONFIG_DIR, 'language')
        if os.path.isdir(lang_dir):
            self.language.load_directory(lang_dir)

        # Make sure our configuration and data directories exist
        if not os.path.isdir(self.CONFIG_DIR):
            os.makedirs(self.CONFIG_DIR, 0o755)

        if not os.path.isdir(self.DATA_DIR):
            os.makedirs(self.DATA_DIR, 0o755)

    def _fire_event(self, event_name, has_reply=False, is_command=False, **kwargs):
        """
        Fire an IRC event.

        @type   event_name: C{str}
        @param  event_name: Name of the event to fire, see firefly.irc for a list of event constants

        @type   has_reply:  bool
        @param  has_reply:  Indicates that this event triggered a language response before firing

        @type   is_command: bool
        @param  is_command: Indicates that this event triggered a command before firing

        @param  kwargs:     Arbitrary event arguments
        """
        self._log.info('Firing event: %s', event_name)
        events = self.registry.get_events(event_name)

        for cls, func, params in events:
            # Commands ok?
            if is_command and not params['command_ok']:
                self._log.info('Event is not responding to command triggers, skipping')
                continue

            # Replies ok?
            if has_reply and not params['reply_ok']:
                self._log.info('Event is not responding to language triggers, skipping')
                continue

            self._log.info('Firing event: %s (%s); Params: %s', str(cls), str(func), str(params))

            # response = func(cls, **kwargs)
            message = kwargs.get('message')
            response = Response(
                self,
                message,
                message.source if message else None,
                message.destination if message and message.destination.is_channel else None
            )

            func(cls, response, **kwargs)
            response.send()

    def _fire_command(self, plugin, name, cmd_args, message):
        """
        Fire an IRC command.

        @type   plugin:     str
        @param  plugin:     Name of the command plugin

        @type   name:       str
        @param  name:       Name of the command

        @type   cmd_args:   list of str
        @param  cmd_args:   List of command arguments

        @type   message:    Message
        @param  message:    Command message container
        """
        self._log.info('Firing command: %s %s (%s)', plugin, name, str(cmd_args))
        cls, func, argparse, params = self.registry.get_command(plugin, name)

        # Make sure we have permission
        perm = params['permission']
        user = self.auth.check(message.source)

        if (perm != 'guest') and not user:
            error = 'You must be registered and authenticated in order to use this command.'

            if self.server.public_errors:
                self.msg(message.destination, error)
            else:
                self.notice(message.source, error)

            return

        if (perm == 'admin') and not user.is_admin:
            error = 'You do not have permission to use this command.'

            if self.server.public_errors:
                self.msg(message.destination, error)
            else:
                self.notice(message.source, error)

            return

        # Execute the command
        try:
            response = Response(
                self, message, message.source, message.destination if message.destination.is_channel else None
            )

            func(argparse.parse_args(cmd_args), response)
            response.send()
        except ArgumentParserError as e:
            self._log.info('Argument parser error: %s', e.message)

            usage    = style(argparse.format_usage().strip(), bold=True)
            desc     = ' -- {desc}'.format(desc=argparse.description.strip()) if argparse.description else ''
            help_msg = '({usage}){desc}'.format(usage=usage, desc=desc)

            # If this command was sent in a query, return the error now
            if message.destination.is_user:
                self.msg(message.source, e.message)
                self.msg(message.source, help_msg)
                return

            # Otherwise, check if we should send the messages as a notice or channel message
            if self.server.public_errors:
                self.msg(message.destination, e.message)
                self.msg(message.destination, help_msg)
            else:
                self.notice(message.source, e.message)
                self.notice(message.source, help_msg)

    def msg(self, user, message, length=None):
        """
        Send a message to a user or channel.

        The message will be split into multiple commands to the server if:
         - The message contains any newline characters
         - Any span between newline characters is longer than the given
           line-length.

        @type   user:       Destination, Hostmask or str
        @param  user:       The user or channel to send a notice to.

        @type   message:    str
        @param  message:    The contents of the notice to send.

        @type   length:     int
        @param  length:     Maximum number of octets to send in a single command, including the IRC protocol framing.
                            If None is given then IRCClient._safeMaximumLineLength is used to determine a value.
        """
        if isinstance(user, Destination):
            self._log.debug('Implicitly converting Destination to raw format for message delivery: %s --> %s',
                            repr(user), user.raw)
            user = user.raw

        if isinstance(user, Hostmask):
            self._log.debug('Implicitly converting Hostmask to nick format for message delivery: %s --> %s',
                            repr(user), user.nick)
            user = user.nick

        self._log.debug('Delivering message to %s : %s', user, (message[:35] + '..') if len(message) > 35 else message)
        IRCClient.msg(self, user, message.encode('utf-8'), length)

    def notice(self, user, message):
        """
        Send a notice to a user.

        Notices are like normal message, but should never get automated
        replies.

        @type   user:   Destination, Hostmask or str
        @param  user:   The user or channel to send a notice to.
        
        @type   message: str
        @param  message: The contents of the notice to send.
        """
        if isinstance(user, Destination):
            self._log.debug('Implicitly converting Destination to raw format for notice delivery: %s --> %s',
                            repr(user), user.raw)
            user = user.raw

        if isinstance(user, Hostmask):
            self._log.debug('Implicitly converting Hostmask to nick format for notice delivery: %s --> %s',
                            repr(user), user.nick)
            user = user.nick

        IRCClient.notice(self, user, message)

    def describe(self, channel, action):
        """
        Strike a pose.

        @type   channel:    Destination, Hostmask or str
        @param  channel:    The user or channel to perform an action in.

        @type   action: str
        @param  action: The action to preform.
        """
        if isinstance(channel, Destination):
            self._log.debug('Implicitly converting Destination to raw format for action performance: %s --> %s',
                            repr(channel), channel.raw)
            channel = channel.raw

        if isinstance(channel, Hostmask):
            self._log.debug('Implicitly converting Hostmask to nick format for action performance: %s --> %s',
                            repr(channel), channel.nick)
            channel = channel.nick

        IRCClient.describe(self, channel, action)

    ################################
    # High-level IRC Events        #
    ################################

    def created(self, when):
        """
        Called with creation date information about the server, usually at logon.

        @type   when: C{str}
        @param  when: A string describing when the server was created, probably.
        """
        self._fire_event(irc.on_created, when=when)

    def yourHost(self, info):
        """
        Called with daemon information about the server, usually at logon.
        # TODO: Consider integrating into ServerInfo container

        @param  info: A string describing what software the server is running, probably.
        @type   info: C{str}
        """
        self._fire_event(irc.on_server_host, info=info)

    def myInfo(self, server_name, version, umodes, cmodes):
        """
        Called with information about the server, usually at logon.

        @type   server_name: C{str}
        @param  server_name: The hostname of this server.

        @type   version: C{str}
        @param  version: A description of what software this server runs.

        @type   umodes: C{str}
        @param  umodes: All the available user modes.

        @type   cmodes: C{str}
        @param  cmodes: All the available channel modes.
        """
        self._fire_event(irc.on_client_info, server_name=server_name, version=version,
                         umodes=umodes, cmodes=cmodes)

    def luserClient(self, info):
        """
        Called with information about the number of connections, usually at logon.

        @type   info: C{str}
        @param  info: A description of the number of clients and servers connected to the network, probably.
        """
        self._fire_event(irc.on_luser_client, info=info)

    def bounce(self, info):
        """
        Called with information about where the client should reconnect.

        @type   info: C{str}
        @param  info: A plaintext description of the address that should be connected to.
        """
        self._fire_event(irc.on_bounce, info=info)

    def isupport(self, options):
        """
        Called with various information about what the server supports.

        @type   options: C{list} of C{str}
        @param  options: Descriptions of features or limits of the server, possibly in the form "NAME=VALUE".
        """
        self.server_info.parse_supports(options)
        self._fire_event(irc.on_server_supports, options=options)

    def luserChannels(self, channels):
        """
        Called with the number of channels existent on the server.

        @type channels: C{int}
        """
        self._fire_event(irc.on_luser_channels, channels=channels)

    def luserOp(self, ops):
        """
        Called with the number of ops logged on to the server.

        @type ops: C{int}
        """
        self._fire_event(irc.on_luser_ops, ops=ops)

    def luserMe(self, info):
        """
        Called with information about the server connected to.

        @type info: C{str}
        @param info: A plaintext string describing the number of users and servers
        connected to this server.
        """
        self._fire_event(irc.on_luser_connection, info=info)

    def privmsg(self, user, channel, message):
        """
        Called when I have a message from a user to me or a channel.

        @type   user:       C{str}
        @param  user:       Hostmask of the sender.

        @type   channel:    C{str}
        @param  channel:    Name of the source channel or user nick.

        @type   message:    C{str}
        """
        hostmask    = Hostmask(user)
        destination = Destination(self, channel)
        message     = Message(message, destination, hostmask)
        is_command  = False
        has_reply   = False
        reply_dest  = destination
        groups = set()

        # Log the message
        self.server.get_or_create_channel(channel).message_log.add_message(message)

        # Is the message a command?
        if message.is_command:
            self._log.debug('Message registered as a command: %s', repr(message))
            is_command = True
            groups.add('command')

            command_plugin, command_name, command_args = message.command_parts
            self._fire_command(command_plugin, command_name, command_args, message)

        # Is this a private message?
        if message.destination.is_user:
            groups.add(None)
            groups.add('private')
            reply_dest = hostmask

        # Have we been mentioned in this message?
        nicks = self.server.identity.nicks
        try:
            nick, raw_message, match = message.get_mentions(nicks)
            groups.add(None)
        except TypeError:
            self._log.debug('Message has no mentions at the beginning')

            try:
                nick, raw_message, match = message.get_mentions(nicks, message.MENTION_END)
                groups.add(None)
            except TypeError:
                self._log.debug('Message has no mentions at the end')
                nick = self.nickname
                raw_message = message.stripped
                match = False
                if message.destination.is_channel:
                    groups.add('public')

        # Do we have a language response?
        reply = self.language.get_reply(raw_message, groups=groups)
        if reply:
            self._log.debug('Reply matched: %s', reply)
            has_reply = True

            self.msg(reply_dest, reply)

        # Fire global event
        self._fire_event(irc.on_message, has_reply, is_command, message=message)

        # Fire custom events
        if message.destination.is_channel:
            self.channelMessage(message, has_reply, is_command)
        elif message.destination.is_user:
            self.privateMessage(message, has_reply, is_command)

    def joined(self, channel):
        """
        Called when I finish joining a channel.

        @type   channel:    C{str}
        @param  channel:    Name of the channel.
        """
        self.server.add_channel(channel)
        self._fire_event(irc.on_client_join, channel=Destination(self, channel))

    def left(self, channel):
        """
        Called when I have left a channel.

        @type   channel:    C{str}
        @param  channel:    Name of the channel.
        """
        self.server.remove_channel(channel)
        self._fire_event(irc.on_client_part, channel=Destination(self, channel))

    def noticed(self, user, channel, message):
        """
        Called when I have a notice from a user to me or a channel.

        If the client makes any automated replies, it must not do so in response to a NOTICE message, per the RFC::

            The difference between NOTICE and PRIVMSG is that
            automatic replies MUST NEVER be sent in response to a
            NOTICE message. [...] The object of this rule is to avoid
            loops between clients automatically sending something in
            response to something it received.

        @type   user:       C{str}
        @param  user:       Hostmask of the sender.

        @type   channel:    C{str}
        @param  channel:    Name of the source channel or user nick.

        @type   message:    C{str}
        """
        notice = Message(message, Destination(self, channel), Hostmask(user), Message.NOTICE)
        self._fire_event(irc.on_notice, notice=notice)

        # Fire custom events
        if notice.destination.is_channel:
            self.channelNotice(notice)
        elif notice.destination.is_user:
            self.privateNotice(notice)

    def modeChanged(self, user, channel, set, modes, args):
        """
        Called when users or channel's modes are changed.
        TODO

        @type   user: C{str}
        @param  user: The user and hostmask which instigated this change.

        @type   channel: C{str}
        @param  channel: The channel where the modes are changed. If args is empty the channel for which the modes
        are changing. If the changes are at server level it could be equal to C{user}.

        @type   set: C{bool} or C{int}
        @param  set: True if the mode(s) is being added, False if it is being removed. If some modes are added and
        others removed at the same time this function will be called twice, the first time with all the added modes,
        the second with the removed ones. (To change this behaviour override the irc_MODE method)

        @type   modes: C{str}
        @param  modes: The mode or modes which are being changed.

        @type   args: C{tuple}
        @param  args: Any additional information required for the mode change.
        """
        self._fire_event(irc.on_mode_changed, user=Hostmask(user), source=Destination(self, channel), set=set, modes=modes, args=args)

    def pong(self, user, secs):
        """
        Called with the results of a CTCP PING query.

        @type   user: C{str}
        @param  user: The user and hostmask.

        @type   secs: C{float}
        @param  secs: Ping latency
        """
        self._fire_event(irc.on_pong, user=Hostmask(user), secs=secs)

    def signedOn(self):
        """
        Called after successfully signing on to the server.
        """
        # Connect to our autojoin channels
        channels = self.server.autojoin_channels

        for name, channel in channels.iteritems():
            if channel.autojoin:
                self.join(channel.name)

        self._fire_event(irc.on_client_signed_on)

    def kickedFrom(self, channel, kicker, message):
        """
        Called when I am kicked from a channel.

        @type   channel:    C{str}
        @param  channel:    The channel we were kicked from.

        @type   kicker:     C{str}
        @param  kicker:     The user that kicked us.

        @type   message:    C{str}
        @param  message:    The kick message.
        """
        self._fire_event(irc.on_client_kicked, channel=channel, kicker=kicker, message=message)

    def nickChanged(self, nick):
        """
        Called when my nick has been changed.

        @type   nick:   C{str}
        @param  nick:   Our new nick
        """
        self.nickname = nick
        self._fire_event(irc.on_client_nick, nick=nick)

    def userJoined(self, user, channel):
        """
        Called when I see another user joining a channel.

        @type   user:       C{str}
        @param  user:       The user joining the channel.

        @type   channel:    C{str}
        @param  channel:    The channel being joined.
        """
        self._fire_event(irc.on_channel_join, user=Hostmask(user), channel=channel)

    def userLeft(self, user, channel):
        """
        Called when I see another user leaving a channel.

        @type   user:       C{str}
        @param  user:       The user parting the channel.

        @type   channel:    C{str}
        @param  channel:    The channel being parted.
        """
        self._fire_event(irc.on_channel_part, user=Hostmask(user), channel=channel)

    def userQuit(self, user, quitMessage):
        """
        Called when I see another user disconnect from the network.

        @type   user:           C{str}
        @param  user:           The user quitting the network.

        @type   quitMessage:    C{str}
        @param  quitMessage:    The quit message.
        """
        message = Message(quitMessage, None, Hostmask(user))
        self._fire_event(irc.on_user_quit, user=Hostmask(user), quit_message=quitMessage, message=message)

    def userKicked(self, kickee, channel, kicker, message):
        """
        Called when I observe someone else being kicked from a channel.

        @type   kickee:     C{str}
        @param  kickee:     The user being kicked.

        @type   channel:    C{str}
        @param  channel:    The channel the user is being kicked from.

        @type   kicker:     C{str}
        @param  kicker:     The user that is kicking.

        @type   message:    C{str}
        @param  message:    The kick message.
        """
        message = Message(message, Destination(self, channel), Hostmask(kicker))
        self._fire_event(irc.on_channel_kick, kickee=kickee, channel=channel, kicker=kicker, message=message)

    def action(self, user, channel, data):
        """
        Called when I see a user perform an ACTION on a channel.

        @type   user:       C{str}
        @param  user:       The user performing the action.

        @type   channel:    C{str}
        @param  channel:    The user or channel.

        @type   data:       C{str}
        @param  data:       The action being performed.
        """
        action = Message(data, Destination(self, channel), Hostmask(user), Message.ACTION)
        self._fire_event(irc.on_action, action=action)

        if action.destination.is_channel:
            self.channelAction(action, False)
        elif action.destination.is_user:
            self.privateAction(action, False)

    def topicUpdated(self, user, channel, newTopic):
        """
        In channel, user changed the topic to newTopic.
        Also called when first joining a channel.

        @type   user:       C{str} or C{None}
        @param  user:       The user updating the topic, if relevant.

        @type   channel:    C{str}
        @param  channel:    The channel the topic is being changed on.

        @type   newTopic:   C{str}
        @param  newTopic:   The updated topic.
        """
        self._fire_event(irc.on_channel_topic_updated, user=Hostmask(user), channel=channel, new_topic=newTopic)

    def userRenamed(self, oldname, newname):
        """
        A user changed their name from oldname to newname.

        @type   oldname:    C{str}
        @param  oldname:    The users old nick.

        @type   newname:    C{str}
        @param  newname:    The users new nick.
        """
        self._fire_event(irc.on_user_nick_changed, old_nick=oldname, new_nick=newname)

    def receivedMOTD(self, motd):
        """
        I received a message-of-the-day banner from the server.

        motd is a list of strings, where each string was sent as a separate
        message from the server. To display, you might want to use::

            '\\n'.join(motd)

        to get a nicely formatted string.

        @type   motd:   C{list}
        """
        self._fire_event(irc.on_server_motd, motd=motd)

    ################################
    # Custom IRC Events            #
    ################################

    def channelMessage(self, message, has_reply, is_command):
        """
        Called when I have a message from a user to a channel.

        @type   message:    Message
        @param  message:    The message container object.

        @type   has_reply:  bool
        @param  has_reply:  Indicates that this event triggered a language response before firing

        @type   is_command: bool
        @param  is_command: Indicates that this event triggered a command before firing
        """
        self._fire_event(irc.on_channel_message, has_reply, is_command, message=message)

    def privateMessage(self, message, has_reply, is_command):
        """
        Called when I have a message from a user to me.

        @type   message:    Message
        @param  message:    The message container object.

        @type   has_reply:  bool
        @param  has_reply:  Indicates that this event triggered a language response before firing

        @type   is_command: bool
        @param  is_command: Indicates that this event triggered a command before firing
        """
        self._fire_event(irc.on_private_message, has_reply, is_command, message=message)

    def channelNotice(self, notice):
        """
        Called when I have a notice from a user to a channel.

        @type   notice: Message
        @param  notice: The notice (message) container object.
        """
        self._fire_event(irc.on_channel_notice, notice=notice)

    def privateNotice(self, notice):
        """
        Called when I have a notice from a user to me.

        @type   notice: Message
        @param  notice: The notice (message) container object.
        """
        self._fire_event(irc.on_private_notice, notice=notice)

    def channelAction(self, action, has_reply):
        """
        Called when I see a user perform an ACTION on a channel.

        @type   action: Message
        @param  action: The action (message) container object.

        @type   has_reply:  bool
        @param  has_reply:  Indicates that this event triggered a language response before firing
        """
        self._fire_event(irc.on_channel_action, has_reply, action=action)

    def privateAction(self, action, has_reply):
        """
        Called when I see a user perform an ACTION to me.

        @type   action: Message
        @param  action: The action (message) container object.

        @type   has_reply:  bool
        @param  has_reply:  Indicates that this event triggered a language response before firing
        """
        self._fire_event(irc.on_private_action, has_reply, action=action)

    ################################
    # Low-level IRC Events         #
    ################################

    def irc_ERR_NICKNAMEINUSE(self, prefix, params):
        """
        Called when we try to register or change to a nickname that is already
        taken.
        """
        IRCClient.irc_ERR_NICKNAMEINUSE(self, prefix, params)
        self._fire_event(irc.on_err_nick_in_use, prefix=prefix, params=params)

    def irc_ERR_ERRONEUSNICKNAME(self, prefix, params):
        """
        Called when we try to register or change to an illegal nickname.

        The server should send this reply when the nickname contains any
        disallowed characters.  The bot will stall, waiting for RPL_WELCOME, if
        we don't handle this during sign-on.

        @note: The method uses the spelling I{erroneus}, as it appears in
            the RFC, section 6.1.
        """
        IRCClient.irc_ERR_ERRONEUSNICKNAME(self, prefix, params)
        self._fire_event(irc.on_err_nick_in_use, prefix=prefix, params=params)

    def irc_ERR_PASSWDMISMATCH(self, prefix, params):
        """
        Called when the login was incorrect.
        """
        IRCClient.irc_ERR_PASSWDMISMATCH(self, prefix, params)
        self._fire_event(irc.on_err_bad_password, prefix=prefix, params=params)

    def irc_RPL_WELCOME(self, prefix, params):
        """
        Called when we have received the welcome from the server.
        """
        IRCClient.irc_RPL_WELCOME(self, prefix, params)
        self._fire_event(irc.on_server_welcome, prefix=prefix, params=params)

    def irc_unknown(self, prefix, command, params):
        self._log.debug('Unknown IRC event: ({pr} {c} {pa})'.format(pr=str(prefix), c=str(command), pa=str(params)))
        self._fire_event(irc.on_unknown, prefix=prefix, command=command, params=params)

    def ctcpQuery(self, user, channel, messages):
        """
        Dispatch method for any CTCP queries received.

        Duplicated CTCP queries are ignored and no dispatch is
        made. Unrecognized CTCP queries invoke L{IRCClient.ctcpUnknownQuery}.
        """
        self._fire_event(irc.on_ctcp, user=Hostmask(user), channel=channel, messages=messages)
        IRCClient.ctcpQuery(self, user, channel, messages)

    def ctcpQuery_PING(self, user, channel, data):
        self._fire_event(irc.on_ctcp_ping, user=Hostmask(user), channel=channel, data=data)
        IRCClient.ctcpQuery_PING(self, user, channel, data)

    def ctcpQuery_FINGER(self, user, channel, data):
        self._fire_event(irc.on_ctcp_finger, user=Hostmask(user), channel=channel, data=data)
        IRCClient.ctcpQuery_FINGER(self, user, channel, data)

    def ctcpQuery_VERSION(self, user, channel, data):
        self._fire_event(irc.on_ctcp_version, user=Hostmask(user), channel=channel, data=data)

    def ctcpQuery_SOURCE(self, user, channel, data):
        self._fire_event(irc.on_ctcp_source, user=Hostmask(user), channel=channel, data=data)

    def ctcpQuery_USERINFO(self, user, channel, data):
        self._fire_event(irc.on_ctcp_userinfo, user=Hostmask(user), channel=channel, data=data)
        IRCClient.ctcpQuery_USERINFO(self, user, channel, data)

    def ctcpQuery_TIME(self, user, channel, data):
        self._fire_event(irc.on_ctcp_time, user=Hostmask(user), channel=channel, data=data)
        IRCClient.ctcpQuery_TIME(self, user, channel, data)