Massively improve user handling

This changes how users are handled, making it less likely to encounter duplicate users; in normal circumstances, duplicates shouldn't happen.
This commit is contained in:
Vgr E. Barry 2016-11-15 19:54:42 -05:00
parent 01924504fe
commit 360204bf43
2 changed files with 141 additions and 76 deletions

View File

@ -46,18 +46,9 @@ def who_reply(cli, bot_server, bot_nick, chan, ident, host, server, nick, status
modes = {Features["PREFIX"].get(s) for s in status} - {None}
if nick == bot_nick:
users.Bot.nick = nick
cli.ident = users.Bot.ident = ident
cli.hostmask = users.Bot.host = host
cli.real_name = users.Bot.realname = realname
try: # FIXME: These function names are temporary until everything is moved over
user = users._get(nick, ident, host, realname, allow_bot=True)
except KeyError:
user = users._add(cli, nick=nick, ident=ident, host=host, realname=realname)
user = users._add(cli, nick=nick, ident=ident, host=host, realname=realname) # FIXME
ch = channels.add(chan, cli)
if ch not in user.channels:
user.channels[ch] = modes
ch.users.add(user)
@ -70,7 +61,7 @@ def who_reply(cli, bot_server, bot_nick, chan, ident, host, server, nick, status
event.dispatch(var, ch, user)
if ch is channels.Main and not users.exists(nick): # FIXME
users.add(nick, ident=ident,host=host,account="*",inchan=True,modes=modes,moded=set())
users.add(nick, ident=ident, host=host, account="*", inchan=True, modes=modes, moded=set())
@hook("whospcrpl")
def extended_who_reply(cli, bot_server, bot_nick, data, chan, ident, ip_address, host, server, nick, status, hop, idle, account, realname):
@ -118,19 +109,9 @@ def extended_who_reply(cli, bot_server, bot_nick, data, chan, ident, ip_address,
modes = {Features["PREFIX"].get(s) for s in status} - {None}
if nick == bot_nick:
users.Bot.nick = nick
cli.ident = users.Bot.ident = ident
cli.hostmask = users.Bot.host = host
cli.real_name = users.Bot.realname = realname
users.Bot.account = account
try: # FIXME
user = users._get(nick, ident, host, realname, account, allow_bot=True)
except KeyError:
user = users._add(cli, nick=nick, ident=ident, host=host, realname=realname, account=account)
user = users._add(cli, nick=nick, ident=ident, host=host, realname=realname, account=account) # FIXME
ch = channels.add(chan, cli)
if ch not in user.channels:
user.channels[ch] = modes
ch.users.add(user)
@ -143,7 +124,7 @@ def extended_who_reply(cli, bot_server, bot_nick, data, chan, ident, ip_address,
event.dispatch(var, ch, user)
if ch is channels.Main and not users.exists(nick): # FIXME
users.add(nick, ident=ident,host=host,account=account,inchan=True,modes=modes,moded=set())
users.add(nick, ident=ident, host=host, account=account, inchan=True, modes=modes, moded=set())
@hook("endofwho")
def end_who(cli, bot_server, bot_nick, target, rest):
@ -291,7 +272,7 @@ def mode_change(cli, rawnick, chan, mode, *targets):
"""
actor = users._get(rawnick, allow_none=True) # FIXME
actor = users._add(cli, nick=rawnick) # FIXME
if chan == users.Bot.nick: # we only see user modes set to ourselves
users.Bot.modes.update(mode)
return
@ -469,7 +450,7 @@ def on_nick_change(cli, old_nick, nick):
"""
user = users._get(old_nick, allow_bot=True) # FIXME
user = users._get(old_nick) # FIXME
user.nick = nick
Event("nick_change", {}).dispatch(var, user, old_nick)
@ -504,17 +485,12 @@ def join_chan(cli, rawnick, chan, account=None, realname=None):
ch = channels.add(chan, cli)
ch.state = channels._States.Joined
if users.parse_rawnick_as_dict(rawnick)["nick"] == users.Bot.nick: # we may not be fully set up yet
user = users._add(cli, nick=rawnick, realname=realname, account=account) # FIXME
if user is users.Bot:
ch.mode()
ch.mode(Features["CHANMODES"][0])
ch.who()
user = users.Bot
else:
try: # FIXME
user = users._get(rawnick, account=account, realname=realname, allow_bot=True)
except KeyError:
user = users._add(cli, nick=rawnick, account=account, realname=realname)
ch.users.add(user)
user.channels[ch] = set()
@ -540,7 +516,7 @@ def part_chan(cli, rawnick, chan, reason=""):
"""
ch = channels.add(chan, cli)
user = users._get(rawnick, allow_bot=True) # FIXME
user = users._add(cli, nick=rawnick) # FIXME
if user is users.Bot: # oh snap! we're no longer in the channel!
ch._clear()
@ -566,8 +542,8 @@ def kicked_from_chan(cli, rawnick, chan, target, reason):
"""
ch = channels.add(chan, cli)
actor = users._get(rawnick, allow_bot=True) # FIXME
user = users._get(target, allow_bot=True) # FIXME
actor = users._add(cli, nick=rawnick) # FIXME
user = users._add(cli, nick=target) # FIXME
if user is users.Bot:
ch._clear()
@ -608,7 +584,7 @@ def on_quit(cli, rawnick, reason):
"""
user = users._get(rawnick, allow_bot=True) # FIXME
user = users._add(cli, nick=rawnick) # FIXME
for chan in set(user.channels):
if user is users.Bot:

View File

@ -57,18 +57,14 @@ def _get(nick=None, ident=None, host=None, realname=None, account=None, *, allow
if allow_bot:
users.add(Bot)
for user in users:
if nick is not None and user.nick != nick:
continue
if ident is not None and user.ident != ident:
continue
if host is not None and user.host != host:
continue
if realname is not None and user.realname != realname:
continue
if account is not None and user.account != account:
continue
sentinel = object()
temp = User(sentinel, nick, ident, host, realname, account)
if temp.client is not sentinel:
return temp # actual client
for user in users:
if user == temp:
if not potential or allow_multiple:
potential.append(user)
else:
@ -103,14 +99,12 @@ def _add(cli, *, nick, ident=None, host=None, realname=None, account=None):
if ident is None and host is None and nick is not None:
nick, ident, host = parse_rawnick(nick)
if _exists(nick, ident, host, realname, account, allow_multiple=True, allow_bot=True): # FIXME
raise ValueError("User already exists: " + _arg_msg.format(nick, ident, host, realname, account, True))
cls = User
if predicate(nick):
cls = FakeUser
new = cls(cli, nick, ident, host, realname, account)
if new is not Bot:
_users.add(new)
return new
@ -127,12 +121,21 @@ def _exists(nick=None, ident=None, host=None, realname=None, account=None, *, al
"""
try: # FIXME
_get(nick, ident, host, realname, account, allow_multiple=allow_multiple, allow_bot=allow_bot)
except (KeyError, ValueError):
sentinel = object()
if ident is None and host is None and nick is not None:
nick, ident, host = parse_rawnick(nick)
cls = User
if predicate(nick):
cls = FakeUser
temp = cls(sentinel, nick, ident, host, realname, account)
if temp.client is sentinel: # doesn't exist; if it did, the client would be an actual client
return False
return True
return temp is not Bot or allow_bot
def exists(nick, *stuff, **morestuff): # backwards-compatible API
return nick in var.USERS
@ -141,15 +144,12 @@ def users_():
"""Iterate over the users in the registry."""
yield from _users
def users(): # backwards-compatible API
class users: # backwards-compatible API
def __iter__(self):
yield from var.USERS
def _items(): # backwards-compat crap (really, it stinks)
def items(self):
yield from var.USERS.items()
users.items = _items
del _items
_raw_nick_pattern = re.compile(r"^(?P<nick>.+?)(?:!(?P<ident>.+?)@(?P<host>.+))?$")
def parse_rawnick(rawnick, *, default=None):
@ -171,14 +171,37 @@ class User(IRCContext):
_messages = defaultdict(list)
def __init__(self, cli, nick, ident, host, realname, account, **kwargs):
super().__init__(nick, cli, **kwargs)
self.nick = nick
def __new__(cls, cli, nick, ident, host, realname, account, **kwargs):
self = super().__new__(cls)
super(cls, self).__init__(nick, cli, **kwargs)
self._ident = ident
self._host = host
self.realname = realname
self.account = account
self.channels = {}
if Bot is not None and Bot.nick == nick and {Bot.ident, Bot.host, Bot.realname, Bot.account} == {None}:
self = Bot
self.ident = ident
self.host = host
self.realname = realname
self.account = account
self.channels = {}
# check the set to see if this already exists
elif ident is not None and host is not None:
users = set(_users)
users.add(Bot)
if self in _users: # quirk: this actually checks for the hash first (also, this is O(1))
for user in _users:
if self == user:
self = user
break # this may only happen once
return self
def __init__(*args, **kwargs):
pass # everything that needed to be done was done in __new__
def __str__(self):
return "{self.__class__.__name__}: {self.nick}!{self.ident}@{self.host}#{self.realname}:{self.account}".format(self=self)
@ -186,6 +209,25 @@ class User(IRCContext):
def __repr__(self):
return "{self.__class__.__name__}({self.nick!r}, {self.ident!r}, {self.host!r}, {self.realname!r}, {self.account!r}, {self.channels!r})".format(self=self)
def __hash__(self):
if self.ident is None or self.host is None:
raise ValueError("cannot hash a User with no ident or host")
return hash((self.ident, self.host))
def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
done = False
for a, b in ((self.nick, other.nick), (self.ident, other.ident), (self.host, other.host), (self.realname, other.realname), (self.account, other.account)):
if a is None or b is None:
continue
done = True
if a != b:
return False
return done
def lower(self):
return type(self)(self.client, lower(self.nick), lower(self.ident), lower(self.host), lower(self.realname), lower(self.account), channels, ref=(self.ref or self))
@ -390,6 +432,42 @@ class User(IRCContext):
if self is Bot: # update the client's nickname as well
self.client.nickname = nick
@property
def ident(self): # prevent changing ident and host after they were set (so hash remains the same)
return self._ident
@ident.setter
def ident(self, ident):
if self._ident is None:
self._ident = ident
if self is Bot:
self.client.ident = ident
elif self._ident != ident:
raise ValueError("may not change the ident of a live user")
@property
def host(self):
return self._host
@host.setter
def host(self, host):
if self._host is None:
self._host = host
if self is Bot:
self.client.hostmask = host
elif self._host != host:
raise ValueError("may not change the host of a live user")
@property
def realname(self):
return self._realname
@realname.setter
def realname(self, realname):
self._realname = realname
if self is Bot:
self.client.real_name = realname
@property
def account(self): # automatically converts "0" and "*" to None
return self._account
@ -424,9 +502,20 @@ class FakeUser(User):
is_fake = True
def __hash__(self):
return hash(self.nick)
def queue_message(self, message):
self.send(message) # don't actually queue it
@property
def nick(self):
return self.name
@nick.setter
def nick(self, nick):
raise ValueError("may not change the nick of a fake user")
@property
def rawnick(self):
return self.nick # we don't have a raw nick