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("UserlTrack").``. :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() # User no longer expiring self.u_expire_timers.pop(target.nick, None) 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 elif 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 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] @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] 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 @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
class ChannelTrack(BaseExtension): """Tracks channels and the users on the channels. Only the user's casemapped nicks are stored, as well as their statuses. They are stored casemapped to make it easier to look them up in other extensions. This extension adds ``base.channel_track`` as itself as an alias for ``get_extension("ChannelTrack").``. channels Mapping of channels, where the keys are casemapped channel names, and the values are Channel instances. For more elaborate user tracking, see :py:module:`~PyIRC.extensions.usertrack`. """ requires = ["BaseTrack", "BasicRFC", "ISupport"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Convenience method self.base.channel_track = self # Our channel set self.channels = IRCDict(self.case) # Scheduled items self.mode_timers = IRCDict(self.case) def get_channel(self, name): """Retrieve a channel from the tracking dictionary based on name. Use of this method is preferred to directly accessing the channels dictionary. Returns None if channel not found. Arguments: :param name: Name of the channel to retrieve. """ return self.channels.get(name) def add_channel(self, name, **kwargs): """Add a channel to the tracking dictionary. Avoid using this method directly unless you know what you are doing. """ channel = self.get_channel(name) if channel is None: _logger.debug("Adding channel: %s", name) channel = Channel(self.case, name, **kwargs) self.channels[name] = channel self.call_event("channel", "channel_create", channel) return channel def remove_channel(self, name): """Remove a channel from the tracking dictionary. Avoid using this method directly unless you know what you are doing. """ channel = self.get_channel(name) if channel is None: return self.call_event("channel", "channel_delete", channel) del self.channels[name] @event("protocol", "case_change") def case_change(self, _): self.channels = self.channels.convert(self.case) self.mode_timers = self.mode_timers.convert(self.case) @event("link", "disconnected") def close(self, _): self.channels.clear() for timer in self.mode_timers.values(): try: self.unschedule(timer) except ValueError: pass # pylint: disable=unused-argument @event("modes", "mode_prefix") def prefix(self, _, setter, target, mode): # Parse into hostmask in case of usernames-in-host channel = self.get_channel(target) if channel is None: _logger.warning("Got a PREFIX event for an unknown channel: %s", target) return hostmask = Hostmask.parse(mode.param) if mode.adding: channel.users[hostmask.nick].add(mode.mode) else: channel.users[hostmask.nick].discard(mode.mode) @event("modes", "mode_key") @event("modes", "mode_param") @event("modes", "mode_normal") def modes(self, _, setter, target, mode): """Update a channel's modes.""" channel = self.get_channel(target) if channel is None: return if mode.adding: channel.modes[mode.mode] = mode.param else: channel.modes.pop(mode.mode, None) @event("scope", "user_join") def join(self, caller, scope): """Handle a user (possibly us) joining a channel.""" # JOIN event basicrfc = self.base.basic_rfc if self.casecmp(scope.target.nick, basicrfc.nick): # We're joining self.add_channel(scope.scope) self.burst(caller, scope) @event("scope", "user_burst") def burst(self, _, scope): """Add the users being bursted to the channel.""" # NAMES event channel = self.get_channel(scope.scope) if channel is None: return user = scope.target.nick if user not in channel.users: channel.users[user] = set() modes = {m[0] for m in scope.modes} if scope.modes else set() channel.users[user] = modes @event("scope", "user_part") @event("scope", "user_kick") def part(self, _, scope): """Remove a user from a channel.""" channel = self.get_channel(scope.scope) assert channel user = scope.target.nick basicrfc = self.base.basic_rfc if self.casecmp(user, basicrfc.nick): # We are leaving self.remove_channel(channel.name) timer = self.mode_timers.pop(channel.name, None) if timer is not None: try: self.unschedule(timer) except ValueError: pass return _logger.debug("users before deletion: %r", channel.users) del channel.users[user] @event("scope", "user_quit") def quit(self, _, scope): """Remove a user from all the channels which they were joined.""" user = scope.target.nick for channel in self.channels.values(): channel.users.pop(user, None) @event("commands", Numerics.RPL_TOPIC) @event("commands", "TOPIC") def topic(self, _, line): """Update a channel's topic.""" if line.command.lower() == "topic": channel = self.get_channel(line.params[0]) if channel is None: _logger.debug("Topic for unknown channel: %s", line.params[0]) return # TODO server/local time deltas for more accurate timestamps channel.topicwho = line.hostmask channel.topictime = int(time()) else: channel = self.get_channel(line.params[1]) if channel is None: _logger.debug("RPL_TOPIC for unknown channel: %s", line.params[1]) return channel.topic = line.params[-1] @event("commands", Numerics.RPL_NOTOPIC) def no_topic(self, _, line): """Update the fact that a channel has no topic.""" channel = self.get_channel(line.params[1]) if channel is None: _logger.debug("RPL_NOTOPIC for unknown channel: %s", line.params[1]) return channel.topic = '' @event("commands", Numerics.RPL_TOPICWHOTIME) def topic_who_time(self, _, line): """Update the channel's topic metadata.""" channel = self.get_channel(line.params[1]) if channel is None: _logger.debug("Topic time for unknown channel: %s", line.params[1]) return channel.topicwho = Hostmask.parse(line.params[2]) channel.topictime = int(line.params[3]) @event("commands", Numerics.RPL_CHANNELURL) def url(self, _, line): """Update the channel's URL.""" channel = self.get_channel(line.params[1]) if channel is None: _logger.debug("URL for unknown channel: %s", line.params[1]) return channel.url = line.params[-1] @event("commands", Numerics.RPL_CREATIONTIME) def timestamp(self, _, line): """Update the channel's creation time.""" channel = self.get_channel(line.params[1]) if channel is None: _logger.debug("Creation time for unknown channel: %s", line.params[1]) return channel.timestamp = int(line.params[-1]) # Cancel timer = self.mode_timers.pop(channel.name, None) if timer is not None: try: self.unschedule(timer) except ValueError: pass @event("commands", Numerics.RPL_ENDOFNAMES) def names_end(self, _, line): """Schedule a MODE timer since we are finished bursting this channel.""" channel = self.get_channel(line.params[1]) if channel is None: return timer = self.schedule(5, partial(self.send, "MODE", [line.params[1]])) self.mode_timers[channel.name] = timer @event("commands", "NICK") def nick(self, _, line): """Rename a user in all channels.""" oldnick = line.hostmask.nick newnick = line.params[-1] # Change the nick in all channels for channel in self.channels.values(): if oldnick not in channel.users: continue # Change the nick channel.users[newnick] = channel.users.pop(oldnick)
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