Update as per @skizzerz's comments
This commit is contained in:
parent
5ec273c6e0
commit
2877abea55
@ -1 +1,2 @@
|
||||
typing
|
||||
enum
|
||||
|
107
src/channels.py
107
src/channels.py
@ -1,48 +1,63 @@
|
||||
import time
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from src.context import IRCContext, Features
|
||||
from src.logger import debuglog
|
||||
from src import users
|
||||
|
||||
Main = None # main channel
|
||||
|
||||
all_channels = {}
|
||||
_channels = {}
|
||||
|
||||
_states = ("not yet joined", "pending join", "joined", "pending leave", "left channel", "", "deleted", "cleared")
|
||||
|
||||
def _strip(name):
|
||||
return name.lstrip("".join(Features["STATUSMSG"]))
|
||||
class _States(Enum):
|
||||
NotJoined = "not yet joined"
|
||||
PendingJoin = "pending join"
|
||||
Joined = "joined"
|
||||
PendingLeave = "pending leave"
|
||||
Left = "left channel"
|
||||
|
||||
Deleted = "deleted"
|
||||
Cleared = "cleared"
|
||||
|
||||
# This is used to tell if this is a fake channel or not. If this
|
||||
# function returns a true value, then it's a fake channel. This is
|
||||
# useful for testing, where we might want the users in fake channels.
|
||||
def predicate(name):
|
||||
return not name.startswith(tuple(Features["CHANTYPES"]))
|
||||
|
||||
def get(name):
|
||||
"""Return an existing channel, or raise a KeyError if it doesn't exist."""
|
||||
get = _channels.__getitem__
|
||||
|
||||
return all_channels[_strip(name)]
|
||||
|
||||
def add(name, cli):
|
||||
def add(name, cli, key=""):
|
||||
"""Add and return a new channel, or an existing one if it exists."""
|
||||
|
||||
name = _strip(name)
|
||||
# We use add() in a bunch of places where the channel probably (but
|
||||
# not surely) already exists. If it does, obviously we want to use
|
||||
# that one. However, if the client is *not* the same, that means we
|
||||
# would be trying to send something from one connection over to
|
||||
# another one (or some other weird stuff like that). Instead of
|
||||
# jumping through hoops, we just disallow it here.
|
||||
|
||||
if name in all_channels:
|
||||
if cli is not all_channels[name].client:
|
||||
if name in _channels:
|
||||
if cli is not _channels[name].client:
|
||||
raise RuntimeError("different IRC client for channel {0}".format(name))
|
||||
return all_channels[name]
|
||||
return _channels[name]
|
||||
|
||||
cls = Channel
|
||||
if predicate(name):
|
||||
cls = FakeChannel
|
||||
|
||||
chan = all_channels[name] = cls(name, cli)
|
||||
chan.join()
|
||||
chan = _channels[name] = cls(name, cli)
|
||||
chan.join(key)
|
||||
return chan
|
||||
|
||||
def exists(name):
|
||||
"""Return True if a channel with the name exists, False otherwise."""
|
||||
exists = _channels.__contains__
|
||||
|
||||
return _strip(name) in all_channels
|
||||
def channels():
|
||||
"""Iterate over all the current channels."""
|
||||
yield from _channels.values()
|
||||
|
||||
class Channel(IRCContext):
|
||||
|
||||
@ -53,37 +68,51 @@ class Channel(IRCContext):
|
||||
self.users = set()
|
||||
self.modes = {}
|
||||
self.timestamp = None
|
||||
self.state = 0
|
||||
self.state = _States.NotJoined
|
||||
|
||||
def __del__(self):
|
||||
self.users.clear()
|
||||
self.modes.clear()
|
||||
self.state = -2
|
||||
self.state = _States.Deleted
|
||||
self.client = None
|
||||
self.timestamp = None
|
||||
|
||||
def __str__(self):
|
||||
return "{self.__class__.__name__}: {self.name} ({0})".format(_states[self.state], self=self)
|
||||
return "{self.__class__.__name__}: {self.name} ({0})".format(self.state.value, self=self)
|
||||
|
||||
def __repr__(self):
|
||||
return "{self.__class__.__name__}({self.name!r})".format(self=self)
|
||||
|
||||
def join(self, key=""):
|
||||
if self.state in (0, 4):
|
||||
self.state = 1
|
||||
if self.state in (_States.NotJoined, _States.Left):
|
||||
self.state = _States.PendingJoin
|
||||
self.client.send("JOIN {0} :{1}".format(self.name, key))
|
||||
|
||||
def part(self, message=""):
|
||||
if self.state == 2:
|
||||
self.state = 3
|
||||
if self.state is _States.Joined:
|
||||
self.state = _States.PendingLeave
|
||||
self.client.send("PART {0} :{1}".format(self.name, message))
|
||||
|
||||
def kick(self, target, message=""):
|
||||
if self.state == 2:
|
||||
if self.state is _States.Joined:
|
||||
self.client.send("KICK {0} {1} :{2}".format(self.name, target, message))
|
||||
|
||||
def mode(self, *changes):
|
||||
if not changes:
|
||||
"""Perform a mode change on the channel.
|
||||
|
||||
Usage:
|
||||
|
||||
chan.mode() # Will get back the modes on the channel
|
||||
chan.mode("b") # Will get the banlist back
|
||||
chan.mode("-m")
|
||||
chan.mode(["-v", "woffle"], ["+o", "Vgr"])
|
||||
chan.mode("-m", ("+v", "jacob1"), ("-o", "nyuszika7h"))
|
||||
|
||||
This performs both single and complex mode changes.
|
||||
|
||||
"""
|
||||
|
||||
if not changes: # bare call; get channel modes
|
||||
self.client.send("MODE", self.name)
|
||||
return
|
||||
|
||||
@ -93,7 +122,7 @@ class Channel(IRCContext):
|
||||
if isinstance(change, str):
|
||||
change = (change, None)
|
||||
params.append(change)
|
||||
params.sort(key=lambda x: x[0][0])
|
||||
params.sort(key=lambda x: x[0][0]) # sort by prefix
|
||||
|
||||
while params:
|
||||
cur, params = params[:max_modes], params[max_modes:]
|
||||
@ -109,13 +138,23 @@ class Channel(IRCContext):
|
||||
final.append(mode)
|
||||
|
||||
for target in targets:
|
||||
if target is not None:
|
||||
if target is not None: # target will be None if the mode is parameter-less
|
||||
final.append(" ")
|
||||
final.append(target)
|
||||
|
||||
self.client.send("MODE", self.name, "".join(final))
|
||||
|
||||
def update_modes(self, rawnick, mode, targets):
|
||||
"""Update the channel's mode registry with the new modes.
|
||||
|
||||
This is called whenever a MODE event is received. All of the
|
||||
modes are kept up-to-date in the channel, even if we don't need
|
||||
it. For instance, banlists are updated properly when the bot
|
||||
receives them. We don't need all the mode information, but it's
|
||||
better to have everything stored than only some parts.
|
||||
|
||||
"""
|
||||
|
||||
set_time = int(time.time()) # for list modes timestamp
|
||||
list_modes, all_set, only_set, no_set = Features["CHANMODES"]
|
||||
status_modes = Features["PREFIX"].values()
|
||||
@ -127,7 +166,7 @@ class Channel(IRCContext):
|
||||
continue
|
||||
|
||||
if prefix == "+":
|
||||
if c in status_modes:
|
||||
if c in status_modes: # op/voice status; keep it here and update the user's registry too
|
||||
if c not in self.modes:
|
||||
self.modes[c] = set()
|
||||
user = users.get(targets[i], allow_bot=True)
|
||||
@ -135,14 +174,14 @@ class Channel(IRCContext):
|
||||
user.channels[self].add(c)
|
||||
i += 1
|
||||
|
||||
elif c in list_modes:
|
||||
elif c in list_modes: # stuff like bans, quiets, and ban and invite exempts
|
||||
if c not in self.modes:
|
||||
self.modes[c] = {}
|
||||
self.modes[c][targets[i]] = (rawnick, set_time)
|
||||
i += 1
|
||||
|
||||
else:
|
||||
if c in no_set:
|
||||
if c in no_set: # everything else; e.g. +m, +i, +f, etc.
|
||||
targ = None
|
||||
else:
|
||||
targ = targets[i]
|
||||
@ -182,14 +221,14 @@ class Channel(IRCContext):
|
||||
del self.modes[mode]
|
||||
del user.channels[self]
|
||||
|
||||
def clear(self):
|
||||
def _clear(self):
|
||||
for user in self.users:
|
||||
del user.channels[self]
|
||||
self.users.clear()
|
||||
self.modes.clear()
|
||||
self.state = -1
|
||||
self.state = _States.Cleared
|
||||
self.timestamp = None
|
||||
del all_channels[self.name]
|
||||
del _channels[self.name]
|
||||
|
||||
class FakeChannel(Channel):
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
Features = {"CASEMAPPING": "rfc1459", "CHARSET": "utf-8", "STATUSMSG": {"@", "+"}, "CHANTYPES": {"#"}} # IRC server features (these are defaults)
|
||||
Features = {"CASEMAPPING": "rfc1459", "CHARSET": "utf-8", "STATUSMSG": {"@", "+"}, "CHANTYPES": {"#"}}
|
||||
|
||||
def lower(nick):
|
||||
if nick is None:
|
||||
@ -41,17 +41,18 @@ class IRCContext:
|
||||
return "PRIVMSG"
|
||||
|
||||
@staticmethod
|
||||
def raw_send(data, client, send_type, name):
|
||||
def _send(data, client, send_type, name):
|
||||
full_address = "{cli.nickname}!{cli.ident}@{cli.hostmask}".format(cli=client)
|
||||
|
||||
# Maximum length of sent data is 0x200 (512) bytes. However,
|
||||
# we have to reduce the maximum length allowed to account for:
|
||||
# Maximum length of sent data is 512 bytes. However, we have to
|
||||
# reduce the maximum length allowed to account for:
|
||||
# 1 (1) - The initial colon at the front of the data
|
||||
# 2 (1) - The space between the command and the target
|
||||
# 2 (1) - The space between the target and the data
|
||||
# 3 (1) - The colon at the front of the data to send
|
||||
# 4 (3) - I don't know why, but we need 3 more/less characters
|
||||
length = 0x200 - 7
|
||||
# 2 (1) - The space between the sender (us) and the command
|
||||
# 3 (1) - The space between the command and the target
|
||||
# 4 (1) - The space between the target and the data
|
||||
# 5 (1) - The colon at the front of the data to send
|
||||
# 6 (2) - The trailing \r\n
|
||||
length = 512 - 7
|
||||
# Next, we need to reduce the length to account for our address
|
||||
length -= len(full_address)
|
||||
# Then we also need to account for the target's length
|
||||
@ -66,4 +67,4 @@ class IRCContext:
|
||||
|
||||
def send(self, data, target=None, *, notice=False, privmsg=False):
|
||||
send_type = self.get_send_type(is_notice=notice, is_privmsg=privmsg)
|
||||
self.raw_send(data, self.client, send_type, self.name)
|
||||
self._send(data, self.client, send_type, self.name)
|
||||
|
60
src/users.py
60
src/users.py
@ -12,17 +12,20 @@ import botconfig
|
||||
|
||||
Bot = None # bot instance
|
||||
|
||||
all_users = WeakSet()
|
||||
_users = WeakSet()
|
||||
|
||||
_arg_msg = "(nick={0}, ident={1}, host={2}, realname={3}, account={4}, allow_bot={5})"
|
||||
|
||||
# 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, raw_nick=False):
|
||||
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 four keyword-only arguments:
|
||||
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;
|
||||
@ -34,23 +37,17 @@ def get(nick=None, ident=None, host=None, realname=None, account=None, *, allow_
|
||||
- allow_bot (defaulting to False) allows the bot to be matched and
|
||||
returned;
|
||||
|
||||
- raw_nick (defaulting to False) means that the nick has not been
|
||||
yet parsed, and so ident and host will be None, and nick will be
|
||||
a raw nick of the form nick!ident@host.
|
||||
|
||||
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 raw_nick:
|
||||
if ident is not None or host is not None:
|
||||
raise ValueError("ident and host need to be None if raw_nick is True")
|
||||
if ident is None and host is None and nick is not None:
|
||||
nick, ident, host = parse_rawnick(nick)
|
||||
|
||||
potential = []
|
||||
users = set(all_users)
|
||||
users = set(_users)
|
||||
if allow_bot:
|
||||
users.add(Bot)
|
||||
|
||||
@ -83,7 +80,7 @@ def get(nick=None, ident=None, host=None, realname=None, account=None, *, allow_
|
||||
|
||||
return potential[0]
|
||||
|
||||
def add(cli, *, nick, ident=None, host=None, realname=None, account=None, channels=None, raw_nick=False):
|
||||
def add(cli, *, nick, ident=None, host=None, realname=None, account=None, channels=None):
|
||||
"""Create a new user, add it to the user list and return it.
|
||||
|
||||
This function takes up to 6 keyword-only arguments (and no positional
|
||||
@ -91,15 +88,9 @@ def add(cli, *, nick, ident=None, host=None, realname=None, account=None, channe
|
||||
With the exception of the first one, any parameter can be omitted.
|
||||
If a matching user already exists, a ValueError will be raised.
|
||||
|
||||
The raw_nick keyword argument may be set if the nick has not yet
|
||||
been parsed. In that case, ident and host must both be None, and
|
||||
nick must be in the form nick!ident@host.
|
||||
|
||||
"""
|
||||
|
||||
if raw_nick:
|
||||
if ident is not None or host is not None:
|
||||
raise ValueError("ident and host need to be None if raw_nick is True")
|
||||
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):
|
||||
@ -115,10 +106,10 @@ def add(cli, *, nick, ident=None, host=None, realname=None, account=None, channe
|
||||
cls = FakeUser
|
||||
|
||||
new = cls(cli, nick, ident, host, realname, account, channels)
|
||||
all_users.add(new)
|
||||
_users.add(new)
|
||||
return new
|
||||
|
||||
def exists(*args, allow_none=False, **kwargs):
|
||||
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
|
||||
@ -127,29 +118,18 @@ def exists(*args, allow_none=False, **kwargs):
|
||||
|
||||
"""
|
||||
|
||||
if allow_none: # why would you even want to do that?
|
||||
raise RuntimeError("Cannot use allow_none=True with exists()")
|
||||
|
||||
try:
|
||||
get(*args, **kwargs)
|
||||
get(nick, ident, host, realname, account, allow_multiple=allow_multiple, allow_bot=allow_bot)
|
||||
except (KeyError, ValueError):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
_raw_nick_pattern = re.compile(
|
||||
def users():
|
||||
"""Iterate over the users in the registry."""
|
||||
yield from _users
|
||||
|
||||
r"""
|
||||
\A
|
||||
(?P<nick> [^!@\s]+ (?=!|$) )? !?
|
||||
(?P<ident> [^!@\s]+ )? @?
|
||||
(?P<host> \S+ )?
|
||||
\Z
|
||||
""",
|
||||
|
||||
re.VERBOSE
|
||||
|
||||
)
|
||||
_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."""
|
||||
@ -190,7 +170,7 @@ class User(IRCContext):
|
||||
|
||||
def is_owner(self):
|
||||
if self.is_fake:
|
||||
return False # fake nicks can't ever be owner
|
||||
return False
|
||||
|
||||
hosts = set(botconfig.OWNERS)
|
||||
accounts = set(botconfig.OWNERS_ACCOUNTS)
|
||||
@ -208,7 +188,7 @@ class User(IRCContext):
|
||||
|
||||
def is_admin(self):
|
||||
if self.is_fake:
|
||||
return False # they can't be admin, either
|
||||
return False
|
||||
|
||||
flags = var.FLAGS[self.rawnick] + var.FLAGS_ACCS[self.account]
|
||||
|
||||
@ -377,7 +357,7 @@ class User(IRCContext):
|
||||
max_targets = Features["TARGMAX"][send_type]
|
||||
while targets:
|
||||
using, targets = targets[:max_targets], targets[max_targets:]
|
||||
cls.raw_send(message, targets[0].client, send_type, ",".join([t.nick for t in using]))
|
||||
cls._send(message, targets[0].client, send_type, ",".join([t.nick for t in using]))
|
||||
|
||||
cls._messages.clear()
|
||||
|
||||
|
@ -28,7 +28,8 @@ if sys.version_info < (3, 3):
|
||||
sys.exit(1)
|
||||
|
||||
try: # need to manually add dependencies here
|
||||
import typing
|
||||
import typing # Python >= 3.5
|
||||
import enum # Python >= 3.4
|
||||
except ImportError:
|
||||
command = "python3"
|
||||
if os.name == "nt":
|
||||
|
Loading…
Reference in New Issue
Block a user