banned/src/users.py
2016-11-20 08:25:45 -05:00

518 lines
16 KiB
Python

from weakref import WeakSet
import fnmatch
import re
from src.context import IRCContext, Features, lower
from src import settings as var
from src import db
import botconfig
Bot = None # bot instance
_users = WeakSet()
_arg_msg = "(nick={0!r}, ident={1!r}, host={2!r}, realname={3!r}, account={4!r}, allow_bot={5})"
class _user:
def __init__(self, nick):
self.nick = nick
for name in ("ident", "host", "account", "inchan", "modes", "moded"):
locals()[name] = property(lambda self, name=name: var.USERS[self.nick][name], lambda self, value, name=name: var.USERS[self.nick].__setitem__(name, value))
# This is used to tell if this is a fake nick or not. If this function
# returns a true value, then it's a fake nick. This is useful for
# testing, where we might want everyone to be fake nicks.
predicate = re.compile(r"^[0-9]+$").search
def _get(nick=None, ident=None, host=None, realname=None, account=None, *, allow_multiple=False, allow_none=False, allow_bot=False):
"""Return the matching user(s) from the user list.
This takes up to 5 positional arguments (nick, ident, host, realname,
account) and may take up to three keyword-only arguments:
- allow_multiple (defaulting to False) allows multiple matches,
and returns a list, even if there's only one match;
- allow_none (defaulting to False) allows no match at all, and
returns None instead of raising an error; an empty list will be
returned if this is used with allow_multiple;
- allow_bot (defaulting to False) allows the bot to be matched and
returned;
If allow_multiple is not set and multiple users match, a ValueError
will be raised. If allow_none is not set and no users match, a KeyError
will be raised.
"""
if ident is None and host is None and nick is not None:
nick, ident, host = parse_rawnick(nick)
potential = []
users = set(_users)
if allow_bot:
users.add(Bot)
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:
raise ValueError("More than one user matches: " +
_arg_msg.format(nick, ident, host, realname, account, allow_bot))
if not potential and not allow_none:
raise KeyError(_arg_msg.format(nick, ident, host, realname, account, allow_bot))
if allow_multiple:
return potential
if not potential: # allow_none
return None
return potential[0]
def get(nick, *stuff, **morestuff): # backwards-compatible API - kill this as soon as possible!
var.USERS[nick] # _user(nick) evaluates lazily, so check eagerly if the nick exists
return _user(nick)
def _add(cli, *, nick, ident=None, host=None, realname=None, account=None):
"""Create a new user, add it to the user list and return it.
This function takes up to 5 keyword-only arguments (and no positional
arguments): nick, ident, host, realname and account.
With the exception of the first one, any parameter can be omitted.
If a matching user already exists, a ValueError will be raised.
"""
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
new = cls(cli, nick, ident, host, realname, account)
if new is not Bot:
_users.add(new)
return new
def add(nick, **blah): # backwards-compatible API
var.USERS[nick] = blah
return _user(nick)
def _exists(nick=None, ident=None, host=None, realname=None, account=None, *, allow_multiple=False, allow_bot=False):
"""Return True if a matching user exists.
Positional and keyword arguments are the same as get(), with the
exception that allow_none may not be used (a RuntimeError will be
raised in that case).
"""
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 temp is not Bot or allow_bot
def exists(nick, *stuff, **morestuff): # backwards-compatible API
return nick in var.USERS
def users_():
"""Iterate over the users in the registry."""
yield from _users
class users: # backwards-compatible API
def __iter__(self):
yield from var.USERS
def items(self):
yield from var.USERS.items()
_raw_nick_pattern = re.compile(r"^(?P<nick>.+?)(?:!(?P<ident>.+?)@(?P<host>.+))?$")
def parse_rawnick(rawnick, *, default=None):
"""Return a tuple of (nick, ident, host) from rawnick."""
return _raw_nick_pattern.search(rawnick).groups(default)
def parse_rawnick_as_dict(rawnick, *, default=None):
"""Return a dict of {"nick": nick, "ident": ident, "host": host}."""
return _raw_nick_pattern.search(rawnick).groupdict(default)
def equals(nick1, nick2):
return lower(nick1) == lower(nick2)
class User(IRCContext):
is_user = True
def __new__(cls, cli, nick, ident, host, realname, account):
self = super().__new__(cls)
super(User, self).__init__(nick, cli)
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
# 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)
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):
temp = type(self)(self.client, lower(self.nick), lower(self.ident), lower(self.host), lower(self.realname), lower(self.account))
temp.channels = self.channels
temp.ref = self.ref or self
return temp
def is_owner(self):
if self.is_fake:
return False
hosts = set(botconfig.OWNERS)
accounts = set(botconfig.OWNERS_ACCOUNTS)
if not var.DISABLE_ACCOUNTS and self.account is not None:
for pattern in accounts:
if fnmatch.fnmatch(lower(self.account), lower(pattern)):
return True
for hostmask in hosts:
if match_hostmask(hostmask, self.nick, self.ident, self.host):
return True
return False
def is_admin(self):
if self.is_fake:
return False
flags = var.FLAGS[self.rawnick] + var.FLAGS_ACCS[self.account]
if "F" not in flags:
try:
hosts = set(botconfig.ADMINS)
accounts = set(botconfig.ADMINS_ACCOUNTS)
if not var.DISABLE_ACCOUNTS and self.account is not None:
for pattern in accounts:
if fnmatch.fnmatch(lower(self.account), lower(pattern)):
return True
for hostmask in hosts:
if match_hostmask(hostmask, self.nick, self.ident, self.host):
return True
except AttributeError:
pass
return self.is_owner()
return True
def get_send_type(self, *, is_notice=False, is_privmsg=False):
if is_privmsg:
return "PRIVMSG"
if is_notice:
return "NOTICE"
if self.prefers_notice():
return "NOTICE"
return "PRIVMSG"
def match_hostmask(self, hostmask):
"""Match n!u@h, u@h, or just h by itself."""
nick, ident, host = re.match("(?:(?:(.*?)!)?(.*?)@)?(.*)", hostmask).groups("")
temp = self.lower()
return (fnmatch.fnmatch(temp.nick, lower(nick)) and
fnmatch.fnmatch(temp.ident, lower(ident)) and
fnmatch.fnmatch(temp.host, lower(host)))
def prefers_notice(self):
temp = self.lower()
if temp.account in var.PREFER_NOTICE_ACCS:
return True
if not var.ACCOUNTS_ONLY:
for hostmask in var.PREFER_NOTICE:
if temp.match_hostmask(hostmask):
return True
return False
def prefers_simple(self):
temp = self.lower()
if temp.account in var.SIMPLE_NOTIFY_ACCS:
return True
if not var.ACCOUNTS_ONLY:
for hostmask in var.SIMPLE_NOTIFY:
if temp.match_hostmask(hostmask):
return True
return False
def get_pingif_count(self):
temp = self.lower()
if not var.DISABLE_ACCOUNTS and temp.account is not None:
if temp.account in var.PING_IF_PREFS_ACCS:
return var.PING_IF_PREFS_ACCS[temp.account]
elif not var.ACCOUNTS_ONLY:
for hostmask, pref in var.PING_IF_PREFS.items():
if temp.match_hostmask(hostmask):
return pref
return 0
def set_pingif_count(self, value, old=None):
temp = self.lower()
if not value:
if not var.DISABLE_ACCOUNTS and temp.account:
if temp.account in var.PING_IF_PREFS_ACCS:
del var.PING_IF_PREFS_ACCS[temp.account]
db.set_pingif(0, temp.account, None)
if old is not None:
with var.WARNING_LOCK:
if old in var.PING_IF_NUMS_ACCS:
var.PING_IF_NUMS_ACCS[old].discard(temp.account)
if not var.ACCOUNTS_ONLY:
for hostmask in list(var.PING_IF_PREFS):
if temp.match_hostmask(hostmask):
del var.PING_IF_PREFS[hostmask]
db.set_pingif(0, None, hostmask)
if old is not None:
with var.WARNING_LOCK:
if old in var.PING_IF_NUMS:
var.PING_IF_NUMS[old].discard(hostmask)
var.PING_IF_NUMS[old].discard(temp.host)
else:
if not var.DISABLE_ACCOUNTS and temp.account:
var.PING_IF_PREFS[temp.account] = value
db.set_pingif(value, temp.account, None)
with var.WARNING_LOCK:
if value not in var.PING_IF_NUMS_ACCS:
var.PING_IF_NUMS_ACCS[value] = set()
var.PING_IF_NUMS_ACCS[value].add(temp.account)
if old is not None:
if old in var.PING_IF_NUMS_ACCS:
var.PING_IF_NUMS_ACCS[old].discard(temp.account)
elif not var.ACCOUNTS_ONLY:
var.PING_IF_PREFS[temp.userhost] = value
db.set_pingif(value, None, temp.userhost)
with var.WARNING_LOCK:
if value not in var.PING_IF_NUMS:
var.PING_IF_NUMS[value] = set()
var.PING_IF_NUMS[value].add(temp.userhost)
if old is not None:
if old in var.PING_IF_NUMS:
var.PING_IF_NUMS[old].discard(temp.host)
var.PING_IF_NUMS[old].discard(temp.userhost)
def wants_deadchat(self):
temp = self.lower()
if temp.account in var.DEADCHAT_PREFS_ACCS:
return False
elif var.ACCOUNTS_ONLY:
return True
elif temp.host in var.DEADCHAT_PREFS:
return False
return True
def stasis_count(self):
"""Return the number of games the user is in stasis for."""
temp = self.lower()
amount = 0
if not var.DISABLE_ACCOUNTS:
amount = var.STASISED_ACCS.get(temp.account, 0)
amount = max(amount, var.STASISED.get(temp.userhost, 0))
return amount
@property
def nick(self): # name should be the same as nick (for length calculation)
return self.name
@nick.setter
def nick(self, nick):
self.name = nick
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
@account.setter
def account(self, account):
if account in ("0", "*"):
account = None
self._account = account
@property
def rawnick(self):
if self.nick is None or self.ident is None or self.host is None:
return None
return "{self.nick}!{self.ident}@{self.host}".format(self=self)
@rawnick.setter
def rawnick(self, rawnick):
self.nick, self.ident, self.host = parse_rawnick(rawnick)
@property
def userhost(self):
if self.ident is None or self.host is None:
return None
return "{self.ident}@{self.host}".format(self=self)
@userhost.setter
def userhost(self, userhost):
nick, self.ident, self.host = parse_rawnick(userhost)
class FakeUser(User):
is_fake = True
def __hash__(self):
return hash(self.nick)
@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
@rawnick.setter
def rawnick(self, rawnick):
self.nick = parse_rawnick_as_dict(rawnick)["nick"]
class BotUser(User): # TODO: change all the 'if x is Bot' for 'if isinstance(x, BotUser)'
def __new__(cls, cli, nick):
self = super().__new__(cls, cli, nick, None, None, None, None)
self.modes = set()
return self
def change_nick(self, nick=None):
if nick is None:
nick = self.nick
self.client.send("NICK", nick)