3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-11-01 01:09:22 +01:00

Rework irc.users and User() to transparently create a store of nicks -> UIDs

- This turns IRCNetwork.users into a new UserMapping class, which stores User objects by UID (str) and provides a 'bynick' dict storing case-normalized nicks to lists of UIDs.
- Turn User.nick into a property, where the setter implicitly updates the 'bynick' index and computes a case-normalized version of the nick (User.lower_nick)
This commit is contained in:
James Lu 2018-03-24 00:10:00 -07:00
parent 815535d76b
commit a085aed924

View File

@ -18,6 +18,8 @@ import queue
import functools
import string
import re
import collections
import collections.abc
try:
import ircmatch
@ -44,6 +46,131 @@ class ChannelState(structures.IRCCaseInsensitiveDict):
return self._data[key]
class User():
"""PyLink IRC user class."""
def __init__(self, irc, nick, ts, uid, server, ident='null', host='null',
realname='PyLink dummy client', realhost='null',
ip='0.0.0.0', manipulatable=False, opertype='IRC Operator'):
self._nick = nick
self.lower_nick = irc.to_lower(nick)
self.ts = ts
self.uid = uid
self.ident = ident
self.host = host
self.realhost = realhost
self.ip = ip
self.realname = realname
self.modes = set() # Tracks user modes
self.server = server
self._irc = irc
# Tracks PyLink identification status
self.account = ''
# Tracks oper type (for display only)
self.opertype = opertype
# Tracks external services identification status
self.services_account = ''
# Tracks channels the user is in
self.channels = structures.IRCCaseInsensitiveSet(self._irc)
# Tracks away message status
self.away = ''
# This sets whether the client should be marked as manipulatable.
# Plugins like bots.py's commands should take caution against
# manipulating these "protected" clients, to prevent desyncs and such.
# For "serious" service clients, this should always be False.
self.manipulatable = manipulatable
# Cloaked host for IRCds that use it
self.cloaked_host = None
# Stores service bot name if applicable
self.service = None
@property
def nick(self):
return self._nick
@nick.setter
def nick(self, newnick):
oldnick = self.lower_nick
self._nick = newnick
self.lower_nick = self._irc.to_lower(newnick)
# Update the irc.users bynick index:
if oldnick in self._irc.users.bynick:
# Remove existing value -> key mappings.
self._irc.users.bynick[oldnick].remove(self.uid)
# Remove now-empty keys as well.
if not self._irc.users.bynick[oldnick]:
del self._irc.users.bynick[oldnick]
# Update the new nick.
self._irc.users.bynick.setdefault(self.lower_nick, []).append(self.uid)
def __repr__(self):
return 'User(%s/%s)' % (self.uid, self.nick)
IrcUser = User
# Bidirectional dict based off https://stackoverflow.com/a/21894086
class UserMapping(collections.abc.MutableMapping, structures.CopyWrapper):
"""
A mapping storing User objects by UID, as well as UIDs by nick via
the 'bynick' attribute
"""
def __init__(self, *, data=None):
if data is not None:
assert isinstance(data, dict)
self._data = data
else:
self._data = {}
self.bynick = collections.defaultdict(list)
def __getitem__(self, key):
return self._data[key]
def __setitem__(self, key, userobj):
assert isinstance(userobj, User), "UserMapping can only hold User objects"
if key in self._data:
log.warning('(%s) Attempting to replace User object for %r: %r -> %r', self.name,
key, self._data.get(key), userobj)
self._data[key] = userobj
self.bynick.setdefault(userobj.lower_nick, []).append(key)
def __delitem__(self, key):
# Remove this entry from the bynick index
if self[key].lower_nick in self.bynick:
self.bynick[self[key].lower_nick].remove(key)
if not self.bynick[self[key].lower_nick]:
del self.bynick[self[key].lower_nick]
del self._data[key]
# Generic container methods. XXX: consider abstracting this out in structures?
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, self._data)
def __iter__(self):
return iter(self._data)
def __len__(self):
return len(self._data)
def __contains__(self, key):
return self._data.__contains__(key)
def __copy__(self):
return self.__class__(data=self._data.copy())
class PyLinkNetworkCore(structures.CamelCaseToSnakeCase):
"""Base IRC object for PyLink."""
@ -133,7 +260,7 @@ class PyLinkNetworkCore(structures.CamelCaseToSnakeCase):
# Intialize the server, channel, and user indexes to be populated by
# our protocol module.
self.servers = {}
self.users = {}
self.users = UserMapping()
# Two versions of the channels index exist in PyLink 2.0, and they are joined together
# - irc._channels which implicitly creates channels on access (mostly used
@ -445,9 +572,14 @@ class PyLinkNetworkCore(structures.CamelCaseToSnakeCase):
def nick_to_uid(self, nick):
"""Looks up the UID of a user with the given nick, if one is present."""
nick = self.to_lower(nick)
for k, v in self.users.copy().items():
if self.to_lower(v.nick) == nick:
return k
uids = self.users.bynick.get(nick, [])
if len(uids) > 1:
log.warning('(%s) Multiple UIDs found for nick %r: %r', self.name, nick, uids)
try:
return uids[0]
except IndexError:
return None
def is_internal_client(self, numeric):
"""
@ -1628,54 +1760,6 @@ class IRCNetwork(PyLinkNetworkCoreWithUtils):
Irc = IRCNetwork
class User():
"""PyLink IRC user class."""
def __init__(self, irc, nick, ts, uid, server, ident='null', host='null',
realname='PyLink dummy client', realhost='null',
ip='0.0.0.0', manipulatable=False, opertype='IRC Operator'):
self.nick = nick
self.ts = ts
self.uid = uid
self.ident = ident
self.host = host
self.realhost = realhost
self.ip = ip
self.realname = realname
self.modes = set() # Tracks user modes
self.server = server
self._irc = irc
# Tracks PyLink identification status
self.account = ''
# Tracks oper type (for display only)
self.opertype = opertype
# Tracks external services identification status
self.services_account = ''
# Tracks channels the user is in
self.channels = structures.IRCCaseInsensitiveSet(self._irc)
# Tracks away message status
self.away = ''
# This sets whether the client should be marked as manipulatable.
# Plugins like bots.py's commands should take caution against
# manipulating these "protected" clients, to prevent desyncs and such.
# For "serious" service clients, this should always be False.
self.manipulatable = manipulatable
# Cloaked host for IRCds that use it
self.cloaked_host = None
# Stores service bot name if applicable
self.service = None
def __repr__(self):
return 'User(%s/%s)' % (self.uid, self.nick)
IrcUser = User
class Server():
"""PyLink IRC server class.