from silan.entities import user from silan.lines import description, kick, message, mode, nickchange, topic, userjoin, userpart, userquit class UserCrate(object): """ UserCrate is a class to perform elegant tracking of IRC users and the nicknames they are known as. In previous iterations of silan, the maintenance of users was handled by the generation of a massive list. All operations down to locating a single user was done very neatly and cleanly through modifications to this list. While this was very storage-light, it turned out to require a massive amount of computational power to use. While UserCrate also maintains a list of all users, it also has two dictionaries. One contains nicks:users, and the other contains addresses:users. Instead of iterating through the user list (called "pool"), a user is found by looking up whatever information is handy through the two dictionaries. If the information can't be found, new dictionary entries are made to point at a new user. If new data is made available about an existing user, the UserCrate intelligently fills in the gaps with new dictionary entries pointing to a single user instance. The list of users is only referred to if necessary to obtain all users at once, or to do advanced searching of user information. In practice, this is a far more efficient method of handling users. The trade-off is increased memory usage. """ def __init__(self): self.pool = [] self.lu_nick = {} self.lu_address = {} self.ownuser = None def _get_user(self, nick, address=None): """ nick: nickname of user address: (optional) user@host of user Same as find_user (even calls it for the same functionality), but will add a user if they don't already exist. This is a "private" method in order to avoid it being used outside of contexts of initialization. find_user should be used for everything else. (It is not really "private" as it is used by one or more external functions called during log initialization.) """ inpool = self.find_user(nick, address) if not inpool: return self.add_user(nick, address) else: inpool.tick_nick(nick) return inpool def find_user(self, nick, address=None): """ nick: nickname of user address: (optional) user@host of user Find a user by nick and/or address. If a nick xor address does not exist in the dictionary, but the address xor nick (respectively) does, this will also call add_alternate to fill in the gap in the appropriate dictionary. This is a general-purpose method and should be used by any output function or plugin to get a user from whatever information is available. """ if address: fromnick = self.lu_nick.get(nick, None) fromaddress = self.lu_address.get(address, None) if fromnick and not fromaddress: return self.add_alternate(fromnick, nick, address) elif fromaddress and not fromnick: return self.add_alternate(fromaddress, nick, address) else: return fromnick else: return self.lu_nick.get(nick, None) def add_user(self, nick, address=None): newuser = user.User(nick, address) if address: self.lu_address[address] = newuser self.lu_nick[nick] = newuser self.pool.append(newuser) return newuser def merge_into(self, inuser, outuser): """ inuser: User that will be consumed and removed outuser: User that will be kept, merged with inuser Effectively turns two user instances into one. Will also redefine appropriate dictionary entries in the lookup dicts to all point to the one merged user. """ outuser.merge(inuser) for addr in (addr for addr in self.lu_address if self.lu_address[addr] == inuser): self.lu_address[addr] = outuser for nick in (nick for nick in self.lu_nick if self.lu_nick[nick] == inuser): self.lu_nick[nick] = outuser self.pool.remove(inuser) return outuser def add_alternate(self, user, altnick, altaddress=None): """ user: Existing user instance altnick: Additional nickname that will belong to the user altaddress: (optional) Additional user@host for the user Registers a new nickname or address with an existing user instance. This can be called regardless of whether or not the nick and/or host has been previously registered. Effectively, this will 'tick' the nick as having been used. """ user.add_nick(altnick) if altaddress: user.add_address(altaddress) self.lu_nick[altnick] = user if altaddress: self.lu_address[altaddress] = user return user def juggle_alternates(self, newnick, oldnick=None, address=None): """ newnick: New nickname user has changed to oldnick: (optional) old nickname. None if local user. address: (optional) address associated with old nick. Performs nick change handling. Creates new user if neither the old or new nicks are recognized. Merges two users together if a person changes their nick to one that originally belonged to a separate user instance. Registers alternate nicks with existing users. """ # If oldnick is None, skip the latter half of this method and # recognize this as the user who the log belongs to. if oldnick: olduser = self.find_user(oldnick, address) elif self.ownuser: return self.add_alternate(self.ownuser, newnick) else: self.ownuser = self._get_user(newnick) return self.ownuser newuser = self.find_user(newnick) if olduser and not newuser: return self.add_alternate(olduser, newnick) elif newuser and not olduser: return self.add_alternate(newuser, oldnick, address) elif olduser and newuser and olduser != newuser: # Merges two separate user instances together merged = self.merge_into(newuser, olduser) merged.tick_nick(newnick) return merged elif olduser and newuser and olduser == newuser: # If both nicks are already recognized and belong to # the same user instance, 'tick' the new nick. newuser.tick_nick(newnick) return newuser elif not olduser and not newuser: # If neither nick has been seen before, make a new # user instance. rolluser = self._get_user(oldnick, address) return self.add_alternate(rolluser, newnick) def extract_users_into(line, crate): """ line: An instance of a silan.lines package class as contained in a Log crate: The UserCrate nick tracking should occur in This is the core of nick tracking operations for UserCrate. All Log lines will ideally be passed through this function to create new users and to update assignments of nicknames and addresses to user instances. Also, every type of line will ultimately trigger a nickname 'tick'. This is defined as incrementing the usage count of a particular nick by 1. The purpose of this is to determine the primary nick of a user based on how frequently lines are logged with a specific nickname. The most frequently used nick for a user will be its 'primary'. All other nicks will be its 'alternates'. """ description, kick, message, mode, nickchange, topic, userjoin, userpart, userquit if isinstance(line, description.Description): crate._get_user(line.nick) elif isinstance(line, kick.Kick): crate._get_user(line.nick, line.address) crate._get_user(line.target) elif isinstance(line, message.Message): crate._get_user(line.nick, line.address) elif isinstance(line, mode.Mode): crate._get_user(line.nick, line.address) if isinstance(line, mode.ChannelSetAdmin) or isinstance(line, mode.ChannelSetHalfOp) or isinstance(line, mode.ChannelSetOp) or isinstance(line, mode.ChannelSetOwner) or isinstance(line, mode.ChannelUnsetAdmin) or isinstance(line, mode.ChannelUnsetHalfOp) or isinstance(line, mode.ChannelSetOp) or isinstance(line, mode.ChannelSetOwner): crate._get_user(line.target) elif isinstance(line, nickchange.NickChange): crate.juggle_alternates(line.newnick, line.nick, line.address) elif isinstance(line, topic.Topic): crate._get_user(line.nick, line.address) elif isinstance(line, userjoin.UserJoin): crate._get_user(line.nick, line.address) elif isinstance(line, userpart.UserPart): crate._get_user(line.nick, line.address) elif isinstance(line, userquit.UserQuit): crate._get_user(line.nick, line.address)