Exemple #1
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Get max number of monitorable users
        max_monitor = self.base.isupport.get("MONITOR")
        if max_monitor is None:
            _logger.warning(
                "Could not use MonitorUserTrack extension on server "
                "because it is not supported.")
            self._usable = False
            return

        self.max_monitor = max_monitor

        self._usable = True

        # Users we (the extension) are monitoring
        self.monitoring = IRCSet()

        # Users being monitored outside the extension
        self.ext_monitoring = IRCSet()

        # Unset
        user_track = self.base.user_track
        user_track.remove_no_channels = False
Exemple #2
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.base.user_track = self

        self.u_expire_timers = IRCDict(self.case)
        self.who_timers = IRCDict(self.case)

        self.users = IRCDict(self.case)
        self.whois_send = IRCSet(self.case)

        # Authentication callbacks
        self.auth_cb = IRCDefaultDict(self.case, list)

        # WHOX sent list
        self.whox_send = list()

        # Whether or not to time users out
        self.do_timeout = kwargs.get("do_timeout", True)
        self.timeout = kwargs.get("timeout", 30)

        # Remove a user when they are out of all channels
        self.remove_no_channels = True

        # Create ourselves
        basicrfc = self.base.basic_rfc
        self.add_user(basicrfc.nick, user=self.username,
                      gecos=self.gecos)
Exemple #3
0
    def __init__(self, base, **kwargs):
        self.base = base

        self.u_expire_timers = IRCDict(self.case)
        self.who_timers = IRCDict(self.case)

        self.users = IRCDict(self.case)
        self.whois_send = IRCSet(self.case)

        # Authentication callbacks
        self.auth_cb = IRCDefaultDict(self.case, list)

        # WHOX sent list
        self.whox_send = list()

        # Create ourselves
        basicrfc = self.get_extension("BasicRFC")
        self.add_user(basicrfc.nick, user=self.username,
                      gecos=self.gecos)
Exemple #4
0
class UserTrack(BaseExtension):

    """Track various user metrics, such as account info, and some channel
    tracking.

    This extension adds ``base.user_track`` as itself as an alias for
    ``get_extension("UserTrack").``.

    :ivar users:
        Mapping of users, where the keys are casemapped nicks, and values are
        User instances. You should probably prefer
        :py:class:`~PyIRC.extensions.usertrack.Usertrack.get_user` to direct
        lookups on this dictionary.
    """

    caps = {
        "account-notify": [],
        "away-notify": [],
        "chghost": [],
    }

    requires = ["BasicRFC", "ISupport", "BaseTrack"]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.base.user_track = self

        self.u_expire_timers = IRCDict(self.case)
        self.who_timers = IRCDict(self.case)

        self.users = IRCDict(self.case)
        self.whois_send = IRCSet(self.case)

        # Authentication callbacks
        self.auth_cb = IRCDefaultDict(self.case, list)

        # WHOX sent list
        self.whox_send = list()

        # Whether or not to time users out
        self.do_timeout = kwargs.get("do_timeout", True)
        self.timeout = kwargs.get("timeout", 30)

        # Remove a user when they are out of all channels
        self.remove_no_channels = True

        # Create ourselves
        basicrfc = self.base.basic_rfc
        self.add_user(basicrfc.nick, user=self.username,
                      gecos=self.gecos)

    def authenticate(self, nick, callback):
        """Get authentication for a user and return result in a callback.

        :param nick:
            Nickname of user to check authentication for

        :param callback:
            Callback to call for user when authentication is discovered. The
            User instance is passed in as the first parameter, or None if the
            user is not found. Use functools.partial to pass other arguments.
        """
        user = self.get_user(nick)
        if not user:
            # Add a user for now, get details later.
            self.users[nick] = User(self.case, nick)

        if user.account is not None:
            # User account is known
            callback(user)
        elif nick not in self.whois_send:
            # Defer for a whois
            self.auth_cb[nick].append(callback)
            self.send("WHOIS", ["*", user.nick])
            self.whois_send.add(nick)

    def get_user(self, nick):
        """Retrieve a user from the tracking dictionary based on nick.

        Use of this method is preferred to directly accessing the user
        dictionary.

        :param nick:
            Nickname of the user to retrieve.

        :returns: A :class:`User` instance, or None if user not found.
        """
        return self.users.get(nick)

    def add_user(self, nick, **kwargs):
        """Add a user to the tracking dictionary.

        Avoid using this method directly unless you know what you are
        doing.
        """
        user = self.get_user(nick)
        if not user:
            user = User(self.case, nick, **kwargs)
            self.users[nick] = user

        self.call_event("user", "user_create", user)

        return user

    def remove_user(self, nick):
        """Remove a user from the tracking dictionary.

        Avoid using this method directly unless you know what you are
        doing.
        """
        if nick not in self.users:
            _logger.warning("Deleting nonexistent user: %s", nick)
            return

        self.call_event("user", "user_delete", self.users[nick])

        _logger.debug("Deleted user: %s", nick)

        del self.users[nick]

    def timeout_user(self, nick):
        """Time a user out, cancelling existing timeouts.

        Avoid using this method directly unless you know what you are
        doing.
        """
        if not self.do_timeout:
            return

        if nick in self.u_expire_timers:
            self.unschedule(self.u_expire_timers[nick])

        callback = partial(self.remove_user, nick)
        self.u_expire_timers[nick] = self.schedule(self.timeout, callback)

    def update_username_host(self, hostmask_or_line):
        """Update a user's basic info, based on line hostmask info.

        Avoid using this method directly unless you know what you are doing.

        .. note::
            This mostly exists for brain-dead networks that don't quit users
            when they get cloaked.
        """
        if hasattr(hostmask_or_line, 'hostmask'):
            hostmask = hostmask_or_line.hostmask
        elif hasattr(hostmask_or_line, 'nick'):
            hostmask = hostmask_or_line
        else:
            raise ValueError("Expected a Hostmask or Line instance")

        if not hostmask or hostmask.nick:
            return

        user = self.get_user(hostmask.nick)
        if not user:
            return

        # Update
        user.nick = hostmask.nick

        if hostmask.username:
            user.username = hostmask.username

        if hostmask.host:
            user.host = hostmask.host

    @event("protocol", "case_change")
    def case_change(self, _):
        case = self.case

        self.u_expire_timers = self.u_expire_timers.convert(case)
        self.who_timers = self.who_timers.convert(case)

        self.users = self.users.convert(case)
        self.whois_send = self.whois_send.convert(case)

        self.auth_cb = self.auth_cb.convert(case)

    @event("link", "disconnected")
    def close(self, _):
        timers = chain(self.u_expire_timers.values(),
                       self.who_timers.values())
        for timer in timers:
            try:
                self.unschedule(timer)
            except ValueError:
                pass

        self.users.clear()
        self.whox_send.clear()

    @event("modes", "mode_prefix")
    def prefix(self, _, setter, target, mode):
        """Update the channel mode of a user."""
        # pylint: disable=unused-argument
        # Parse into hostmask in case of usernames-in-host
        hostmask = Hostmask.parse(mode.param)

        assert hostmask

        user = self.get_user(hostmask.nick)
        if user is None:
            # This can happen from override or ChanServ guard off
            user = self.add_user(hostmask.nick, username=hostmask.username,
                                 host=hostmask.host)
            self.timeout_user(hostmask.nick)

        channel = user.channels[target]
        if mode.adding:
            channel.add(mode.mode)
        else:
            channel.discard(mode.mode)

    @event("scope", "user_burst")
    def burst(self, _, scope):
        """Create or update users from a join burst."""
        target = scope.target
        channel = scope.scope
        modes = {m[0] for m in scope.modes} if scope.modes else set()

        if target.nick in self.u_expire_timers:
            # They're back.  Cancel pending expiry.
            self.unschedule(self.u_expire_timers[target.nick])
            del self.u_expire_timers[target.nick]

        user = self.get_user(target.nick)
        if not user:
            user = self.add_user(target.nick, username=target.username,
                                 host=target.host, gecos=scope.gecos,
                                 account=scope.account)
        else:
            self.update_username_host(target)

        # Add the channel
        user.channels[channel] = modes

    @event("scope", "user_join")
    def join(self, caller, scope):
        """Handle a user join.

        Schedule a WHO(X) for them.
        """
        self.burst(caller, scope)

        target = scope.target
        channel = scope.scope

        basicrfc = self.base.basic_rfc
        if self.casecmp(target.nick, basicrfc.nick):
            # It's us!
            isupport = self.base.isupport
            params = [channel]
            if isupport.get("WHOX"):
                # Use WHOX if possible
                num = ''.join(str(randint(0, 9)) for x in range(randint(1, 3)))
                params.append("%tcuihsnflar," + num)
                self.whox_send.append(num)

            sched = self.schedule(2, partial(self.send, "WHO", params))
            self.who_timers[channel] = sched

    @event("scope", "user_part")
    @event("scope", "user_kick")
    def part(self, _, scope):
        """Handle a user leaving, possibly removing them if we no longer share
        any channels with them."""
        target = scope.target
        channel = scope.scope

        user = self.get_user(target.nick)
        if not user:
            _logger.warning("Got a part/kick for a user not found: %s (in %s)",
                            target.nick, channel)
            return

        if channel not in user.channels:
            _logger.warning("Got a part/kick for a user not in a channel: %s "
                            "(in %s)", target.nick, channel)
            return

        user.channels.pop(channel)

        basicrfc = self.base.basic_rfc
        if self.casecmp(target.nick, basicrfc.nick):
            # We left the channel, scan all users to remove unneeded ones
            for u_nick, u_user in list(self.users.items()):
                if channel in u_user.channels:
                    # Purge from the cache since we don't know for certain.
                    del u_user.channels[channel]

                if self.casecmp(u_nick, basicrfc.nick):
                    # Don't delete ourselves!
                    continue

                if not u_user.channels:
                    # Delete the user outright to purge any cached data
                    # The data must be considered invalid when we leave
                    # TODO - possible WATCH support?
                    self.remove_user(u_nick)
                    continue

        elif not user.channels:
            if self.do_timeout:
                self.timeout_user(target.nick)
            elif self.remove_no_channels:
                self.remove_user(target.nick)

    @event("scope", "user_quit")
    def quit(self, _, scope):
        """Remove a user from tracking since they have quit."""
        # User's gone
        self.remove_user(scope.target.nick)

    @event("commands", Numerics.RPL_WELCOME)
    def welcome(self, _, line):
        """Retrieve our current host on-connect."""
        # Obtain our own host
        self.send("USERHOST", [line.params[0]])

    @event("commands", Numerics.RPL_USERHOST)
    def userhost(self, _, line):
        """Update a user's host - possibly our own."""
        params = line.params
        if not (len(params) > 1 and params[1]):
            return

        basicrfc = self.base.basic_rfc

        for mask in params[1].split(' '):
            if not mask:
                continue

            parse = userhost_parse(mask)
            hostmask = parse.hostmask
            user = self.get_user(hostmask.nick)
            if not user:
                continue

            if hostmask.username:
                user.username = hostmask.username

            user.operator = parse.operator
            if not parse.away:
                user.away = False

            if self.casecmp(hostmask.nick, basicrfc.nick):
                user.realhost = hostmask.host
            else:
                user.host = hostmask.host

    @event("commands", Numerics.RPL_HOSTHIDDEN)
    def host_hidden(self, _, line):
        """Update our own host."""
        params = line.params

        user = self.get_user(params[0])
        assert user  # This should NEVER fire!

        user.host = params[1]

    @event("commands", "ACCOUNT")
    def account(self, _, line):
        """Update a user's account information."""
        self.update_username_host(line)

        account = line.params[0]

        user = self.get_user(line.hostmask.nick)
        assert user

        user.account = '' if account == '*' else account

        if user.nick in self.auth_cb:
            # User is awaiting authentication
            for callback in self.auth_cb[user.nick]:
                callback(user)

            del self.auth_cb[user.nick]

    @event("commands", "AWAY")
    def away(self, _, line):
        """Update a user's away flag."""
        self.update_username_host(line)

        user = self.get_user(line.hostmask.nick)
        assert user

        user.away = bool(line.params)

    @event("commands", "CHGHOST")
    def chghost(self, _, line):
        # NB - we don't know if a user is cloaking or uncloaking, or changing
        # cloak, so do NOT update user's cloak.
        self.update_username_host(line)

        user = self.get_user(line.hostmask.nick)
        assert user

        user.username = line.params[0]
        user.host = line.params[1]

    @event("commands", "NICK")
    def nick(self, _, line):
        """Update a user's nickname."""
        self.update_username_host(line)

        oldnick = line.hostmask.nick
        newnick = line.params[0]

        assert self.get_user(oldnick)

        self.users[newnick] = self.get_user(oldnick)
        self.users[newnick].nick = newnick

        if not self.casecmp(oldnick, newnick):
            del self.users[oldnick]

    @event("commands", Numerics.ERR_NOSUCHNICK)
    def notfound(self, _, line):
        """Remove a non-existent user."""
        nick = line.params[1]
        if nick in self.auth_cb:
            # User doesn't exist, call back
            for callback in self.auth_cb[nick]:
                callback(None)

            del self.auth_cb[nick]

        self.remove_user(nick)

    @event("commands", "PRIVMSG")
    @event("commands", "NOTICE")
    def message(self, _, line):
        if line.params[0] == '*':
            # We are not registered, do nothing.
            return

        hostmask = line.hostmask
        if not hostmask.nick:
            return

        if hostmask.nick in self.u_expire_timers:
            # User is expiring
            user = self.get_user(hostmask.nick)
            if hostmask.username != user.username or hostmask.host != user.host:
                # User is suspect, delete and find out more.
                self.remove_user(hostmask.nick)
            else:
                # Rearm timeout
                self.timeout_user(hostmask.nick)

        if not self.get_user(hostmask.nick):
            if not self.do_timeout:
                return

            # Obtain more information about the user
            user = self.add_user(hostmask.nick, user=hostmask.username,
                                 host=hostmask.host)

            if hostmask.nick not in self.whois_send:
                self.send("WHOIS", ['*', hostmask.nick])
                self.whois_send.add(hostmask.nick)

            self.timeout_user(hostmask.nick)

    @event("commands", Numerics.RPL_ENDOFWHO)
    def who_end(self, _, line):
        if not self.whox_send:
            return

        channel = line.params[1]
        del self.who_timers[channel]
        del self.whox_send[0]

    @event("commands", Numerics.RPL_ENDOFWHOIS)
    def whois_end(self, _, line):
        """Finish updating a user's information."""
        nick = line.params[1]

        self.whois_send.discard(nick)

        user = self.get_user(nick)

        # If the user is awaiting auth, we aren't gonna find out their auth
        # status through whois. If it's not in whois, we probably aren't going
        # to find it any other way (sensibly at least).
        if nick in self.auth_cb:
            # User is awaiting authentication
            for callback in self.auth_cb[nick]:
                callback(user)

            del self.auth_cb[nick]

    @event("commands", Numerics.RPL_WHOISUSER)
    def whois_user(self, _, line):
        """Update a user's identity information."""
        nick = line.params[1]
        username = line.params[2]
        host = line.params[3]
        gecos = line.params[5]

        user = self.get_user(nick)
        if not user:
            return

        user.nick = nick
        user.username = username
        user.host = host
        user.gecos = gecos

    @event("commands", Numerics.RPL_WHOISCHANNELS)
    def whois_channels(self, _, line):
        """Update a user's channel set."""
        user = self.get_user(line.params[1])
        if not user:
            return

        isupport = self.base.isupport
        prefix = prefix_parse(isupport.get("PREFIX"))

        for channel in line.params[-1].split():
            mode = set()
            mode, channel = status_prefix_parse(channel, prefix)
            user.channels[channel] = mode

    @event("commands", Numerics.RPL_WHOISHOST)
    def whois_host(self, _, line):
        """Update a user's IP and/or host."""
        user = self.get_user(line.params[1])
        if not user:
            return

        # F*****g unreal did this shit.
        string, _, ip_ = line.params[-1].rpartition(' ')
        string, _, realhost = string.rpartition(' ')

        user.ip = ip_
        user.realhost = realhost

    @event("commands", Numerics.RPL_WHOISIDLE)
    def whois_idle(self, _, line):
        user = self.get_user(line.params[1])
        if not user:
            return

        user.signon = int(line.params[3])

    @event("commands", Numerics.RPL_WHOISOPERATOR)
    def whois_operator(self, _, line):
        """Update a user's operator flag."""
        user = self.get_user(line.params[1])
        if not user:
            return

        user.operator = True

    @event("commands", Numerics.RPL_WHOISSECURE)
    def whois_secure(self, _, line):
        """Update a user's SSL flag."""
        user = self.get_user(line.params[1])
        if not user:
            return

        user.secure = True

    @event("commands", Numerics.RPL_WHOISSERVER)
    def whois_server(self, _, line):
        """Update a user's currently connected server name."""
        user = self.get_user(line.params[1])
        if not user:
            return

        user.server = line.params[2]
        user.server_desc = line.params[3]

    @event("commands", Numerics.RPL_WHOISLOGGEDIN)
    def whois_account(self, _, line):
        """Update a user's account, possibly firing auth callbacks."""
        user = self.get_user(line.params[1])
        if not user:
            return

        user.account = line.params[2]

        nick = user.nick
        if nick in self.auth_cb:
            # User is awaiting authentication
            for callback in self.auth_cb[nick]:
                callback(user)

            del self.auth_cb[nick]

    # pylint: disable=too-many-locals
    @event("commands", Numerics.RPL_WHOREPLY)
    def who(self, _, line):
        """Process a WHO response, updating the corresponding user object."""
        if len(line.params) < 8:
            # Some bizarre RFC breaking server
            _logger.warning("Malformed WHO from server")
            return

        channel = line.params[1]
        username = line.params[2]
        host = line.params[3]
        server = line.params[4]
        nick = line.params[5]
        flags = who_flag_parse(line.params[6])
        other = line.params[7]
        # TODO: find something to do with this stuff
        # pylint: disable=unused-variable
        hopcount, _, other = other.partition(' ')

        user = self.get_user(nick)
        if not user:
            return

        isupport = self.base.isupport

        if isupport.get("RFC2812"):
            # IRCNet, for some stupid braindead reason, sends SID here. Why? I
            # don't know. They mentioned it might break clients in the commit
            # log. I really have no idea why it exists, why it's useful to
            # anyone, or anything like that. But honestly, WHO sucks enough...
            sid, _, gecos = other.partition(' ')
        else:
            sid = None
            gecos = other

        if channel != '*':
            # Convert symbols to modes
            prefix = prefix_parse(isupport.get("PREFIX")).prefix_to_mode

            mode = set()
            for char in flags.modes:
                char = prefix.get(char)
                if char is not None:
                    mode.add(char)

            user.channels[channel] = mode

        away = flags.away
        operator = flags.operator

        # NB - these two members aren't guaranteed to exist (yet?)
        user.sid = sid
        user.server = server

        user.username = username
        user.host = host
        user.gecos = gecos
        user.away = away
        user.operator = operator

    # pylint: disable=too-many-locals
    @event("commands", Numerics.RPL_WHOSPCRPL)
    def whox(self, _, line):
        """Process a WHOX response, updating the corresponding user object."""
        if len(line.params) != 12:
            # Not from us!
            return

        # Verify the server supports WHOX for real because Bahamut has its own
        # Eldritch abomination we don't support (RWHO... you don't wanna know)
        isupport = self.base.isupport
        if not isupport.get("WHOX"):
            return

        whoxid = line.params[1]
        channel = line.params[2]
        username = line.params[3]
        ip_ = line.params[4]
        host = line.params[5]
        server = line.params[6]
        nick = line.params[7]
        flags = who_flag_parse(line.params[8])
        idle = line.params[9]
        account = line.params[10]
        gecos = line.params[11]

        user = self.get_user(nick)
        if not user:
            return

        if whoxid not in self.whox_send:
            # Not sent by us, weird!
            return

        if channel != '*':
            # Convert symbols to modes
            prefix = prefix_parse(isupport.get("PREFIX")).prefix_to_mode

            mode = set()
            for char in flags.modes:
                char = prefix.get(char)
                if char is not None:
                    mode.add(char)

            user.channels[channel] = mode

        away = flags.away
        operator = flags.operator

        if account == '0':
            # Not logged in
            account = ''

        if ip_ == '255.255.255.255':
            # Cloaked
            ip_ = None

        user.server = server
        user.idle = idle
        user.username = username
        user.host = host
        user.server = server
        user.gecos = gecos
        user.away = away
        user.operator = operator
        user.account = account
        user.ip = ip_
Exemple #5
0
class MonitorUserTrack(BaseExtension):
    """Support for the MONITOR extension.

    This works in tandem with :py:class:`~PyIRC.extensions.usertrack.UserTrack`
    to remove users as-needed.
    """

    requires = ["UserTrack", "ISupport"]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Get max number of monitorable users
        max_monitor = self.base.isupport.get("MONITOR")
        if max_monitor is None:
            _logger.warning(
                "Could not use MonitorUserTrack extension on server "
                "because it is not supported.")
            self._usable = False
            return

        self.max_monitor = max_monitor

        self._usable = True

        # Users we (the extension) are monitoring
        self.monitoring = IRCSet()

        # Users being monitored outside the extension
        self.ext_monitoring = IRCSet()

        # Unset
        user_track = self.base.user_track
        user_track.remove_no_channels = False

    def monitor(self, *args):
        """Monitor the given nicknames.

        This is the preferred API for setting MONITORs.
        """
        if not args:
            raise ValueError("Must have at least one nickname to monitor")

        nicks = []
        monitors = self.monitoring + self.ext_monitoring
        for nick in args:
            if nick not in monitors:
                nicks.append(nick)

            self.ext_monitoring.add(nick)

        if nicks:
            self.send("MONITOR", ["+", ",".join(nicks)])

    def unmonitor(self, *args):
        """Unmonitor the given nicknames.

        This is the preferred API for unsetting MONITORs.
        """
        if not args:
            raise ValueError("Must have at least one nickname to unmonitor")

        for nick in args:
            if nick in self.monitoring:
                # Don't remove nicks we are monitoring for in the extension.
                continue
            elif nick in self.ext_monitoring:
                self.ext_monitoring.delete(nick)
                nicks.append(nick)

        if nicks:
            self.send("MONITOR", ["-", ",".join(nicks)])

    @event("commands_out", "MONITOR")
    def monitor_out(self, _, line):
        if len(line.params) < 2:
            return

        user_track = self.base.user_track
        users = line.params[1].split(",")
        if line.params[0] == "+":
            for nick in users:
                if nick in self.monitoring:
                    continue

                self.ext_monitoring.add(nick)
        elif line.params[0] == "-":
            for nick in users:
                if nick in self.monitoring:
                    # The user doesn't want them tracked, fine.
                    user_track.user_remove(nick)
                    self.monitoring.delete(nick)

                self.ext_monitoring.discard(nick)

    @event("commands", Numerics.RPL_MONONLINE)
    def monitor_online(self, _, line):
        """Add users who come online on MONITOR."""
        if not self._usable:
            return

        monitor = [Hostmask.parse(h) for h in line.params[-1].split(",")]
        if not monitor:
            return

        user_track = self.base.user_track
        for user in monitor:
            user_track.add_user(user.nick,
                                username=user.username,
                                host=user.host)

    @event("commands", Numerics.RPL_MONOFFLINE)
    def monitor_offline(self, _, line):
        """Remove users who go offline on MONITOR."""
        if not self._usable:
            return

        monitor = [Hostmask.parse(h) for h in line.params[-1].split(",")]
        if not monitor:
            return

        user_track = self.base.user_track
        for user in monitor:
            if user_track.get_user(user.nick):
                user_track.remove_user(user.nick)

    @event("scope", "user_part", priority=10000)  # Ensure this comes last
    @event("scope", "user_kick", priority=10000)
    def part(self, _, scope):
        """Handle a user parting, registering them for monitoring."""
        if not self._usable:
            return

        target = scope.target
        nick = target.nick
        user_track = self.base.user_track

        user = user_track.get_user(nick)
        if not user or user.channels:
            return

        if len(self.monitoring + self.ext_monitoring) >= self.max_monitor:
            # Delete the user, we're out of space!
            user_track.remove_user(nick)
            return

        if nick in self.monitoring:
            return

        self.monitoring.add(nick)
        self.send("MONITOR", ["+", nick])

    @event("user", "user_delete")
    def user_delete(self, user):
        """Handle when a user is deleted."""
        if user.nick in self.monitoring:
            self.monitoring.delete(user.nick)
            if user not in self.ext_monitoring:
                self.send("MONITOR", ["-", user.nick])
Exemple #6
0
class UserTrack(BaseExtension):

    """ Track various user metrics, such as account info, and some channel
    tracking.

    The following attribute is publlicly available:

    users
        Mapping of users, where the keys are casemapped nicks, and values are
        User instances.
    """

    caps = {
        "account-notify": [],
        "away-notify": [],
        "chghost": [],
        "extended-join": [],
        "multi-prefix": [],
        "userhost-in-names": [],
    }

    requires = ["BasicRFC", "ISupport", "ModeHandler"]

    def __init__(self, base, **kwargs):
        self.base = base

        self.u_expire_timers = IRCDict(self.case)
        self.who_timers = IRCDict(self.case)

        self.users = IRCDict(self.case)
        self.whois_send = IRCSet(self.case)

        # Authentication callbacks
        self.auth_cb = IRCDefaultDict(self.case, list)

        # WHOX sent list
        self.whox_send = list()

        # Create ourselves
        basicrfc = self.get_extension("BasicRFC")
        self.add_user(basicrfc.nick, user=self.username,
                      gecos=self.gecos)

    def authenticate(self, nick, callback):
        """Get authentication for a user and return result in a callback

        Arguments:

        :param nick:
            Nickname of user to check authentication for

        :param callback:
            Callback to call for user when authentication is discovered. The
            User instance is passed in as the first parameter, or None if the
            user is not found. Use functools.partial to pass other arguments.
        """
        user = self.get_user(nick)
        if not user:
            # Add a user for now, get details later.
            self.users[nick] = User(self.case, nick)

        if user.account is not None:
            # User account is known
            callback(user)
        elif nick not in self.whois_send:
            # Defer for a whois
            self.auth_cb[nick].append(callback)
            self.send("WHOIS", ["*", user.nick])
            self.whois_send.add(nick)

    def get_user(self, nick):
        """Retrieve a user from the tracking dictionary based on nick.

        Use of this method is preferred to directly accessing the user
        dictionary.

        Returns None if user not found.

        Arguments:

        :param nick:
            Nickname of the user to retrieve.
        """
        return self.users.get(nick)

    def add_user(self, nick, **kwargs):
        """Add a user to the tracking dictionary.

        Avoid using this method directly unless you know what you are doing.
        """
        user = self.get_user(nick)
        if not user:
            user = User(self.case, nick, **kwargs)
            self.users[nick] = user

        return user

    def remove_user(self, nick):
        """Remove a user from the tracking dictionary.

        Avoid using this method directly unless you know what you are doing.
        """
        if nick not in self.users:
            logger.warning("Deleting nonexistent user: %s", nick)
            return

        logger.debug("Deleted user: %s", nick)

        del self.users[nick]

    def timeout_user(self, nick):
        """Time a user out, cancelling existing timeouts

        Avoid using this method directly unless you know what you are doing.
        """
        if nick in self.u_expire_timers:
            self.unschedule(self.u_expire_timers[nick])

        callback = partial(self.remove_user, nick)
        self.u_expire_timers[nick] = self.schedule(30, callback)

    def update_username_host(self, line):
        """Update a user's basic info, based on line hostmask info.

        Avoid using this method directly unless you know what you are doing.

        .. note::
            This mostly exists for brain-dead networks that don't quit users
            when they get cloaked.
        """
        if not line.hostmask or not line.hostmask.nick:
            return

        hostmask = line.hostmask
        user = self.get_user(hostmask.nick)
        if not user:
            return

        # Update
        user.nick = hostmask.nick

        if hostmask.username:
            user.username = hostmask.username

        if hostmask.host:
            user.host = hostmask.host

    @hook("hooks", "case_change")
    def case_change(self, event):
        case = self.case

        self.u_expire_timers = self.u_expire_timers.convert(case)
        self.who_timers = self.who_timers.convert(case)

        self.users = self.users.convert(case)
        self.whois_send = self.whois_send.convert(case)

        self.auth_cb = self.auth_cb.convert(case)

    @hook("hooks", "disconnected")
    def close(self, event):
        timers = chain(self.u_expire_timers.values(),
                       self.who_timers.values())
        for timer in timers:
            try:
                self.unschedule(timer)
            except ValueError:
                pass

        self.users.clear()
        self.whox_send.clear()

    @hook("commands", Numerics.RPL_WELCOME)
    def welcome(self, event):
        # Obtain our own host
        self.send("USERHOST", [event.line.params[0]])

    @hook("commands", Numerics.RPL_USERHOST)
    def userhost(self, event):
        line = event.line
        params = line.params
        if not (len(params) > 1 and params[1]):
            return

        basicrfc = self.get_extension("BasicRFC")

        for mask in params[1].split(' '):
            if not mask:
                continue

            parse = userhost_parse(mask)
            hostmask = parse.hostmask
            user = self.get_user(hostmask.nick)
            if not user:
                continue

            if hostmask.username:
                user.username = hostmask.username

            user.operator = parse.operator
            if not parse.away:
                user.away = False

            if self.casecmp(hostmask.nick, basicrfc.nick):
                user.realhost = hostmask.host
            else:
                user.host = hostmask.host

    @hook("commands", Numerics.RPL_HOSTHIDDEN)
    def host_hidden(self, event):
        line = event.line
        params = line.params

        user = self.get_user(params[0])
        assert user  # This should NEVER fire!

        user.host = params[1]

    @hook("commands", "ACCOUNT")
    def account(self, event):
        self.update_username_host(event.line)

        user = self.get_user(event.line.hostmask.nick)
        assert user

        user.account = '' if account == '*' else account

        if user.nick in self.auth_cb:
            # User is awaiting authentication
            for callback in self.auth_cb[user.nick]:
                callback(user)

            del self.auth_cb[user.nick]

    @hook("commands", "AWAY")
    def away(self, event):
        self.update_username_host(event.line)

        user = self.get_user(event.line.hostmask.nick)
        assert user

        user.away = bool(event.line.params)

    @hook("commands", "CHGHOST")
    def chghost(self, event):
        # NB - we don't know if a user is cloaking or uncloaking, or changing
        # cloak, so do NOT update user's cloak.
        self.update_username_host(event.line)

        user = self.get_user(event.line.hostmask.nick)
        assert user

        user.username = event.line.params[0]
        user.host = event.line.params[1]

    @hook("commands", "JOIN")
    def join(self, event):
        channel = event.line.params[0]
        nick = event.line.hostmask.nick

        user = self.get_user(nick)
        if user:
            # Remove any pending expiration timer
            self.u_expire_timers.pop(nick, None)
        else:
            # Create a new user with available info
            cap_negotiate = self.get_extension("CapNegotiate")
            if cap_negotiate and 'extended-join' in cap_negotiate.local:
                account = event.line.params[1]
                if account == '*':
                    account = ''

                gecos = event.line.params[2]

            user = self.add_user(nick, account=account, gecos=gecos)

        # Update info
        self.update_username_host(event.line)

        basicrfc = self.get_extension("BasicRFC")
        if self.casecmp(nick, basicrfc.nick):
            # It's us!
            isupport = self.get_extension("ISupport")

            params = [channel]
            if isupport.get("WHOX"):
                # Use WHOX if possible
                num = ''.join(str(randint(0, 9)) for x in range(randint(1, 3)))
                params.append("%tcuihsnflar," + num)
                self.whox_send.append(num)

            sched = self.schedule(2, partial(self.send, "WHO", params))
            self.who_timers[channel] = sched

        # Add the channel
        user.channels[channel] = set()

    @hook("modes", "mode_prefix")
    def prefix(self, event):
        # Parse into hostmask in case of usernames-in-host
        hostmask = Hostmask.parse(event.param)

        assert hostmask

        user = self.get_user(hostmask.nick)
        if not user:
            # Add the user
            user = self.add_user(hostmask.nick, user=hostmask.username,
                                 host=hostmask.host)

        channel = user.channels[event.target]
        if event.adding:
            channel.add(event.mode)
        else:
            channel.discard(event.mode)

    @hook("commands", "NICK")
    def nick(self, event):
        self.update_username_host(event.line)

        oldnick = event.line.hostmask.nick
        newnick = event.line.params[0]

        assert self.get_user(oldnick)

        self.users[newnick] = self.get_user(oldnick)
        self.users[newnick].nick = newnick

        del self.users[oldnick]

    @hook("commands", Numerics.ERR_NOSUCHNICK)
    def notfound(self, event):
        nick = event.line.params[1]
        if nick in self.auth_cb:
            # User doesn't exist, call back
            for callback in self.auth_cb[nick]:
                callback(None)

            del self.auth_cb[nick]

        self.remove_user(nick)

    @hook("commands", "PRIVMSG")
    @hook("commands", "NOTICE")
    def message(self, event):
        if event.line.params[0] == '*':
            # We are not registered, do nothing.
            return

        hostmask = event.line.hostmask
        if not hostmask.nick:
            return

        if hostmask.nick in self.u_expire_timers:
            # User is expiring
            user = self.get_user(hostmask.nick)
            if hostmask.username != user.username or hostmask.host != user.host:
                # User is suspect, delete and find out more.
                self.remove_user(hostmask.nick)
            else:
                # Rearm timeout
                self.timeout_user(hostmask.nick)

        if not self.get_user(hostmask.nick):
            # Obtain more information about the user
            user = self.add_user(hostmask.nick, user=hostmask.username,
                                 host=hostmask.host)

            if hostmask.nick not in self.whois_send:
                self.send("WHOIS", ['*', hostmask.nick])
                self.whois_send.add(hostmask.nick)

            self.timeout_user(hostmask.nick)

    @hook("commands", "KICK")
    @hook("commands", "PART")
    def part(self, event):
        channel = event.line.params[0]

        if event.line.command.lower() == 'part':
            user = self.get_user(event.line.hostmask.nick)
        elif event.line.command.lower() == 'kick':
            user = self.get_user(event.line.params[1])
        assert user

        assert channel in user.channels
        del user.channels[channel]

        basicrfc = self.get_extension("BasicRFC")
        if self.casecmp(user.nick, basicrfc.nick):
            # We left the channel, scan all users to remove unneeded ones
            for u_nick, u_user in list(self.users.items()):
                if channel in u_user.channels:
                    # Purge from the cache since we don't know for certain.
                    del u_user.channels[channel]

                if self.casecmp(u_nick, basicrfc.nick):
                    # Don't delete ourselves!
                    continue

                if not u_user.channels:
                    # Delete the user outright to purge any cached data
                    # The data must be considered invalid when we leave
                    # TODO - possible MONITOR support?
                    self.remove_user(u_nick)
                    continue

        elif not user.channels:
            # No more channels and not us, delete
            # TODO - possible MONITOR support?
            self.remove_user(event.line.hostmask.nick)

    @hook("commands", "QUIT")
    def quit(self, event):
        assert event.line.hostmask.nick in self.users
        self.remove_user(event.line.hostmask.nick)

    @hook("commands", Numerics.RPL_ENDOFWHO)
    def who_end(self, event):
        if not self.whox_send:
            return

        channel = event.line.params[1]
        del self.who_timers[channel]
        del self.whox_send[0]

    @hook("commands", Numerics.RPL_ENDOFWHOIS)
    def whois_end(self, event):
        nick = event.line.params[1]

        self.whois_send.discard(nick)

        user = self.get_user(nick)

        # If the user is awaiting auth, we aren't gonna find out their auth
        # status through whois. If it's not in whois, we probably aren't going
        # to find it any other way (sensibly at least).
        if nick in self.auth_cb:
            # User is awaiting authentication
            for callback in self.auth_cb[nick]:
                callback(user)

            del self.auth_cb[nick]

    @hook("commands", Numerics.RPL_WHOISUSER)
    def whois_user(self, event):
        nick = event.line.params[1]
        username = event.line.params[2]
        host = event.line.params[3]
        gecos = event.line.params[5]

        user = self.get_user(nick)
        if not user:
            return

        user.nick = nick
        user.username = username
        user.host = host
        user.gecos = gecos

    @hook("commands", Numerics.RPL_WHOISCHANNELS)
    def whois_channels(self, event):
        user = self.get_user(event.line.params[1])
        if not user:
            return

        isupport = self.get_extension("ISupport")
        prefix = prefix_parse(isupport.get("PREFIX"))

        for channel in event.line.params[-1].split():
            mode = set()
            mode, channel = status_prefix_parse(channel, prefix)
            user.channels[channel] = mode

    @hook("commands", Numerics.RPL_WHOISHOST)
    def whois_host(self, event):
        user = self.get_user(event.line.params[1])
        if not user:
            return

        # F*****g unreal did this shit.
        string, _, ip = event.line.params[-1].rpartition(' ')
        string, _, realhost = string.rpartition(' ')

        user.ip = ip
        user.realhost = realhost

    @hook("commands", Numerics.RPL_WHOISIDLE)
    def whois_idle(self, event):
        user = self.get_user(event.line.params[1])
        if not user:
            return

        user.signon = int(event.line.params[3])

    @hook("commands", Numerics.RPL_WHOISOPERATOR)
    def whois_operator(self, event):
        user = self.get_user(event.line.params[1])
        if not user:
            return

        user.operator = True

    @hook("commands", Numerics.RPL_WHOISSECURE)
    def whois_secure(self, event):
        user = self.get_user(event.line.params[1])
        if not user:
            return

        user.secure = True

    @hook("commands", Numerics.RPL_WHOISSERVER)
    def whois_server(self, event):
        user = self.get_user(event.line.params[1])
        if not user:
            return

        user.server = event.line.params[2]
        user.server_desc = event.line.params[3]

    @hook("commands", Numerics.RPL_WHOISLOGGEDIN)
    def whois_account(self, event):
        user = self.get_user(event.line.params[1])
        if not user:
            return

        user.account = event.line.params[2]

        nick = user.nick
        if nick in self.auth_cb:
            # User is awaiting authentication
            for callback in self.auth_cb[nick]:
                callback(user)

            del self.auth_cb[nick]

    @hook("commands", Numerics.RPL_WHOREPLY)
    def who(self, event):
        if len(event.line.params) < 8:
            # Some bizarre RFC breaking server
            logger.warn("Malformed WHO from server")
            return

        channel = event.line.params[1]
        username = event.line.params[2]
        host = event.line.params[3]
        server = event.line.params[4]
        nick = event.line.params[5]
        flags = who_flag_parse(event.line.params[6])
        other = event.line.params[7]
        hopcount, _, other = other.partition(' ')

        user = self.get_user(nick)
        if not user:
            return

        isupport = self.get_extension("ISupport")

        if isupport.get("RFC2812"):
            # IRCNet, for some stupid braindead reason, sends SID here. Why? I
            # don't know. They mentioned it might break clients in the commit
            # log. I really have no idea why it exists, why it's useful to
            # anyone, or anything like that. But honestly, WHO sucks enough...
            sid, _, realname = other.partition(' ')
        else:
            sid = None
            realname = other

        if channel != '*':
            # Convert symbols to modes
            prefix = prefix_parse(isupport.get("PREFIX")).prefix_to_mode

            mode = set()
            for char in flags.modes:
                char = prefix.get(char)
                if char is not None:
                    mode.add(char)

            user.channels[channel] = mode

        away = flags.away
        operator = flags.operator

        if account == '0':
            # Not logged in
            account = ''

        if ip == '255.255.255.255':
            # Cloaked
            ip = None

        user.username = username
        user.host = host
        user.gecos = gecos
        user.away = away
        user.operator = operator
        user.account = account
        user.ip = ip

    @hook("commands", Numerics.RPL_WHOSPCRPL)
    def whox(self, event):
        if len(event.line.params) != 12:
            # Not from us!
            return

        whoxid = event.line.params[1]
        channel = event.line.params[2]
        username = event.line.params[3]
        ip = event.line.params[4]
        host = event.line.params[5]
        server = event.line.params[6]
        nick = event.line.params[7]
        flags = who_flag_parse(event.line.params[8])
        # idle = event.line.params[9]
        account = event.line.params[10]
        gecos = event.line.params[11]

        user = self.get_user(nick)
        if not user:
            return

        if whoxid not in self.whox_send:
            # Not sent by us, weird!
            return

        if channel != '*':
            # Convert symbols to modes
            isupport = self.get_extension("ISupport")
            prefix = prefix_parse(isupport.get("PREFIX")).prefix_to_mode

            mode = set()
            for char in flags.modes:
                char = prefix.get(char)
                if char is not None:
                    mode.add(char)

            user.channels[channel] = mode

        away = flags.away
        operator = flags.operator

        if account == '0':
            # Not logged in
            account = ''

        if ip == '255.255.255.255':
            # Cloaked
            ip = None

        user.username = username
        user.host = host
        user.server = server
        user.gecos = gecos
        user.away = away
        user.operator = operator
        user.account = account
        user.ip = ip