3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-12-25 12:12:53 +01:00

Merge branch 'wip/split-utils' into devel

This commit is contained in:
James Lu 2016-04-29 23:32:52 -07:00
commit bd0874a484
9 changed files with 405 additions and 355 deletions

View File

@ -19,6 +19,7 @@ from copy import deepcopy
from log import * from log import *
import world import world
import utils import utils
import structures
### Exceptions ### Exceptions
@ -113,7 +114,7 @@ class Irc():
internal=True, desc=self.serverdata.get('serverdesc') internal=True, desc=self.serverdata.get('serverdesc')
or self.botdata['serverdesc'])} or self.botdata['serverdesc'])}
self.users = {} self.users = {}
self.channels = utils.KeyedDefaultdict(IrcChannel) self.channels = structures.KeyedDefaultdict(IrcChannel)
# This sets the list of supported channel and user modes: the default # This sets the list of supported channel and user modes: the default
# RFC1459 modes are implied. Named modes are used here to make # RFC1459 modes are implied. Named modes are used here to make
@ -451,7 +452,7 @@ class Irc():
def __repr__(self): def __repr__(self):
return "<classes.Irc object for %r>" % self.name return "<classes.Irc object for %r>" % self.name
### Utility functions ### General utility functions
def callCommand(self, source, text): def callCommand(self, source, text):
""" """
Calls a PyLink bot command. source is the caller's UID, and text is the Calls a PyLink bot command. source is the caller's UID, and text is the
@ -490,6 +491,169 @@ class Irc():
"""Replies to the last caller in the right context (channel or PM).""" """Replies to the last caller in the right context (channel or PM)."""
self.msg(self.called_by, text, notice=notice, source=source) self.msg(self.called_by, text, notice=notice, source=source)
def parseModes(self, target, args):
"""Parses a modestring list into a list of (mode, argument) tuples.
['+mitl-o', '3', 'person'] => [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')]
"""
# http://www.irc.org/tech_docs/005.html
# A = Mode that adds or removes a nick or address to a list. Always has a parameter.
# B = Mode that changes a setting and always has a parameter.
# C = Mode that changes a setting and only has a parameter when set.
# D = Mode that changes a setting and never has a parameter.
assert args, 'No valid modes were supplied!'
usermodes = not utils.isChannel(target)
prefix = ''
modestring = args[0]
args = args[1:]
if usermodes:
log.debug('(%s) Using self.umodes for this query: %s', self.name, self.umodes)
if target not in self.users:
log.warning('(%s) Possible desync! Mode target %s is not in the users index.', self.name, target)
return [] # Return an empty mode list
supported_modes = self.umodes
oldmodes = self.users[target].modes
else:
log.debug('(%s) Using self.cmodes for this query: %s', self.name, self.cmodes)
if target not in self.channels:
log.warning('(%s) Possible desync! Mode target %s is not in the channels index.', self.name, target)
return []
supported_modes = self.cmodes
oldmodes = self.channels[target].modes
res = []
for mode in modestring:
if mode in '+-':
prefix = mode
else:
if not prefix:
prefix = '+'
arg = None
log.debug('Current mode: %s%s; args left: %s', prefix, mode, args)
try:
if mode in (supported_modes['*A'] + supported_modes['*B']):
# Must have parameter.
log.debug('Mode %s: This mode must have parameter.', mode)
arg = args.pop(0)
if prefix == '-' and mode in supported_modes['*B'] and arg == '*':
# Charybdis allows unsetting +k without actually
# knowing the key by faking the argument when unsetting
# as a single "*".
# We'd need to know the real argument of +k for us to
# be able to unset the mode.
oldargs = [m[1] for m in oldmodes if m[0] == mode]
if oldargs:
# Set the arg to the old one on the channel.
arg = oldargs[0]
log.debug("Mode %s: coersing argument of '*' to %r.", mode, arg)
elif mode in self.prefixmodes and not usermodes:
# We're setting a prefix mode on someone (e.g. +o user1)
log.debug('Mode %s: This mode is a prefix mode.', mode)
arg = args.pop(0)
# Convert nicks to UIDs implicitly; most IRCds will want
# this already.
arg = self.nickToUid(arg) or arg
if arg not in self.users: # Target doesn't exist, skip it.
log.debug('(%s) Skipping setting mode "%s %s"; the '
'target doesn\'t seem to exist!', self.name,
mode, arg)
continue
elif prefix == '+' and mode in supported_modes['*C']:
# Only has parameter when setting.
log.debug('Mode %s: Only has parameter when setting.', mode)
arg = args.pop(0)
except IndexError:
log.warning('(%s/%s) Error while parsing mode %r: mode requires an '
'argument but none was found. (modestring: %r)',
self.name, target, mode, modestring)
continue # Skip this mode; don't error out completely.
res.append((prefix + mode, arg))
return res
def applyModes(self, target, changedmodes):
"""Takes a list of parsed IRC modes, and applies them on the given target.
The target can be either a channel or a user; this is handled automatically."""
usermodes = not utils.isChannel(target)
log.debug('(%s) Using usermodes for this query? %s', self.name, usermodes)
try:
if usermodes:
old_modelist = self.users[target].modes
supported_modes = self.umodes
else:
old_modelist = self.channels[target].modes
supported_modes = self.cmodes
except KeyError:
log.warning('(%s) Possible desync? Mode target %s is unknown.', self.name, target)
return
modelist = set(old_modelist)
log.debug('(%s) Applying modes %r on %s (initial modelist: %s)', self.name, changedmodes, target, modelist)
for mode in changedmodes:
# Chop off the +/- part that parseModes gives; it's meaningless for a mode list.
try:
real_mode = (mode[0][1], mode[1])
except IndexError:
real_mode = mode
if not usermodes:
# We only handle +qaohv for now. Iterate over every supported mode:
# if the IRCd supports this mode and it is the one being set, add/remove
# the person from the corresponding prefix mode list (e.g. c.prefixmodes['op']
# for ops).
for pmode, pmodelist in self.channels[target].prefixmodes.items():
if pmode in self.cmodes and real_mode[0] == self.cmodes[pmode]:
log.debug('(%s) Initial prefixmodes list: %s', self.name, pmodelist)
if mode[0][0] == '+':
pmodelist.add(mode[1])
else:
pmodelist.discard(mode[1])
log.debug('(%s) Final prefixmodes list: %s', self.name, pmodelist)
if real_mode[0] in self.prefixmodes:
# Don't add prefix modes to IrcChannel.modes; they belong in the
# prefixmodes mapping handled above.
log.debug('(%s) Not adding mode %s to IrcChannel.modes because '
'it\'s a prefix mode.', self.name, str(mode))
continue
if mode[0][0] == '+':
# We're adding a mode
existing = [m for m in modelist if m[0] == real_mode[0] and m[1] != real_mode[1]]
if existing and real_mode[1] and real_mode[0] not in self.cmodes['*A']:
# The mode we're setting takes a parameter, but is not a list mode (like +beI).
# Therefore, only one version of it can exist at a time, and we must remove
# any old modepairs using the same letter. Otherwise, we'll get duplicates when,
# for example, someone sets mode "+l 30" on a channel already set "+l 25".
log.debug('(%s) Old modes for mode %r exist on %s, removing them: %s',
self.name, real_mode, target, str(existing))
[modelist.discard(oldmode) for oldmode in existing]
modelist.add(real_mode)
log.debug('(%s) Adding mode %r on %s', self.name, real_mode, target)
else:
log.debug('(%s) Removing mode %r on %s', self.name, real_mode, target)
# We're removing a mode
if real_mode[1] is None:
# We're removing a mode that only takes arguments when setting.
# Remove all mode entries that use the same letter as the one
# we're unsetting.
for oldmode in modelist.copy():
if oldmode[0] == real_mode[0]:
modelist.discard(oldmode)
else:
# Swap the - for a + and then remove it from the list.
modelist.discard(real_mode)
log.debug('(%s) Final modelist: %s', self.name, modelist)
if usermodes:
self.users[target].modes = modelist
else:
self.channels[target].modes = modelist
### State checking functions
def nickToUid(self, nick): def nickToUid(self, nick):
"""Looks up the UID of a user with the given nick, if one is present.""" """Looks up the UID of a user with the given nick, if one is present."""
nick = utils.toLower(self, nick) nick = utils.toLower(self, nick)

View File

@ -9,7 +9,7 @@ import utils
from log import log from log import log
from classes import * from classes import *
from ts6 import TS6Protocol from ts6 import *
class HybridProtocol(TS6Protocol): class HybridProtocol(TS6Protocol):
def __init__(self, irc): def __init__(self, irc):
@ -92,25 +92,26 @@ class HybridProtocol(TS6Protocol):
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None, server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None,
manipulatable=False): manipulatable=False):
"""Spawns a client with nick <nick> on the given IRC connection. """
Spawns a new client with the given options.
Note: No nick collision / valid nickname checks are done here; it is Note: No nick collision / valid nickname checks are done here; it is
up to plugins to make sure they don't introduce anything invalid.""" up to plugins to make sure they don't introduce anything invalid.
"""
server = server or self.irc.sid server = server or self.irc.sid
if not self.irc.isInternalServer(server): if not self.irc.isInternalServer(server):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server) raise ValueError('Server %r is not a PyLink server!' % server)
# Create an UIDGenerator instance for every SID, so that each gets
# distinct values. uid = self.uidgen[server].next_uid()
uid = self.uidgen.setdefault(server, utils.TS6UIDGenerator(server)).next_uid()
# EUID:
# parameters: nickname, hopcount, nickTS, umodes, username,
# visible hostname, IP address, UID, real hostname, account name, gecos
ts = ts or int(time.time()) ts = ts or int(time.time())
realname = realname or self.irc.botdata['realname'] realname = realname or self.irc.botdata['realname']
realhost = realhost or host realhost = realhost or host
raw_modes = utils.joinModes(modes) raw_modes = utils.joinModes(modes)
u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip, manipulatable=manipulatable) realhost=realhost, ip=ip, manipulatable=manipulatable)
utils.applyModes(self.irc, uid, modes) self.irc.applyModes(uid, modes)
self.irc.servers[server].users.add(uid) self.irc.servers[server].users.add(uid)
self._send(server, "UID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " self._send(server, "UID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
"* :{realname}".format(ts=ts, host=host, "* :{realname}".format(ts=ts, host=host,
@ -181,9 +182,9 @@ class HybridProtocol(TS6Protocol):
self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, host, ip) self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, host, ip)
parsedmodes = utils.parseModes(self.irc, uid, [modes]) parsedmodes = self.irc.parseModes(uid, [modes])
log.debug('(%s) handle_uid: Applying modes %s for %s', self.irc.name, parsedmodes, uid) log.debug('(%s) handle_uid: Applying modes %s for %s', self.irc.name, parsedmodes, uid)
utils.applyModes(self.irc, uid, parsedmodes) self.irc.applyModes(uid, parsedmodes)
self.irc.servers[numeric].users.add(uid) self.irc.servers[numeric].users.add(uid)
# Call the OPERED UP hook if +o is being added to the mode list. # Call the OPERED UP hook if +o is being added to the mode list.
@ -220,7 +221,7 @@ class HybridProtocol(TS6Protocol):
target = args[0] target = args[0]
ts = args[1] ts = args[1]
modes = args[2:] modes = args[2:]
parsedmodes = utils.parseModes(self.irc, target, modes) parsedmodes = self.irc.parseModes(target, modes)
for modepair in parsedmodes: for modepair in parsedmodes:
if modepair[0] == '+d': if modepair[0] == '+d':
@ -258,7 +259,7 @@ class HybridProtocol(TS6Protocol):
parsedmodes.remove(modepair) parsedmodes.remove(modepair)
if parsedmodes: if parsedmodes:
utils.applyModes(self.irc, target, parsedmodes) self.irc.applyModes(target, parsedmodes)
return {'target': target, 'modes': parsedmodes} return {'target': target, 'modes': parsedmodes}

View File

@ -15,7 +15,7 @@ import utils
from log import log from log import log
from classes import * from classes import *
from ts6_common import TS6BaseProtocol from ts6_common import *
class InspIRCdProtocol(TS6BaseProtocol): class InspIRCdProtocol(TS6BaseProtocol):
def __init__(self, irc): def __init__(self, irc):
@ -39,24 +39,29 @@ class InspIRCdProtocol(TS6BaseProtocol):
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator', server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator',
manipulatable=False): manipulatable=False):
"""Spawns a client with nick <nick> on the given IRC connection. """
Spawns a new client with the given options.
Note: No nick collision / valid nickname checks are done here; it is Note: No nick collision / valid nickname checks are done here; it is
up to plugins to make sure they don't introduce anything invalid.""" up to plugins to make sure they don't introduce anything invalid.
"""
server = server or self.irc.sid server = server or self.irc.sid
if not self.irc.isInternalServer(server): if not self.irc.isInternalServer(server):
raise ValueError('Server %r is not a PyLink server!' % server) raise ValueError('Server %r is not a PyLink server!' % server)
# Create an UIDGenerator instance for every SID, so that each gets
# distinct values. uid = self.uidgen[server].next_uid()
uid = self.uidgen.setdefault(server, utils.TS6UIDGenerator(server)).next_uid()
ts = ts or int(time.time()) ts = ts or int(time.time())
realname = realname or self.irc.botdata['realname'] realname = realname or self.irc.botdata['realname']
realhost = realhost or host realhost = realhost or host
raw_modes = utils.joinModes(modes) raw_modes = utils.joinModes(modes)
u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype) realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype)
utils.applyModes(self.irc, uid, modes)
self.irc.applyModes(uid, modes)
self.irc.servers[server].users.add(uid) self.irc.servers[server].users.add(uid)
self._send(server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}" self._send(server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}"
" {ts} {modes} + :{realname}".format(ts=ts, host=host, " {ts} {modes} + :{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid, nick=nick, ident=ident, uid=uid,
@ -127,7 +132,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user) log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user)
if ts <= orig_ts: if ts <= orig_ts:
# Only save our prefix modes in the channel state if our TS is lower than or equal to theirs. # Only save our prefix modes in the channel state if our TS is lower than or equal to theirs.
utils.applyModes(self.irc, channel, changedmodes) self.irc.applyModes(channel, changedmodes)
namelist = ' '.join(namelist) namelist = ' '.join(namelist)
self._send(server, "FJOIN {channel} {ts} {modes} :{users}".format( self._send(server, "FJOIN {channel} {ts} {modes} :{users}".format(
ts=ts, users=namelist, channel=channel, ts=ts, users=namelist, channel=channel,
@ -172,7 +177,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
# https://github.com/inspircd/inspircd/blob/master/src/modules/m_spanningtree/opertype.cpp#L26-L28 # https://github.com/inspircd/inspircd/blob/master/src/modules/m_spanningtree/opertype.cpp#L26-L28
# Servers need a special command to set umode +o on people. # Servers need a special command to set umode +o on people.
self._operUp(target) self._operUp(target)
utils.applyModes(self.irc, target, modes) self.irc.applyModes(target, modes)
joinedmodes = utils.joinModes(modes) joinedmodes = utils.joinModes(modes)
if utils.isChannel(target): if utils.isChannel(target):
ts = ts or self.irc.channels[utils.toLower(self.irc, target)].ts ts = ts or self.irc.channels[utils.toLower(self.irc, target)].ts
@ -486,8 +491,8 @@ class InspIRCdProtocol(TS6BaseProtocol):
self.updateTS(channel, their_ts) self.updateTS(channel, their_ts)
modestring = args[2:-1] or args[2] modestring = args[2:-1] or args[2]
parsedmodes = utils.parseModes(self.irc, channel, modestring) parsedmodes = self.irc.parseModes(channel, modestring)
utils.applyModes(self.irc, channel, parsedmodes) self.irc.applyModes(channel, parsedmodes)
namelist = [] namelist = []
for user in userlist: for user in userlist:
modeprefix, user = user.split(',', 1) modeprefix, user = user.split(',', 1)
@ -501,7 +506,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
namelist.append(user) namelist.append(user)
self.irc.users[user].channels.add(channel) self.irc.users[user].channels.add(channel)
if their_ts <= our_ts: if their_ts <= our_ts:
utils.applyModes(self.irc, channel, [('+%s' % mode, user) for mode in modeprefix]) self.irc.applyModes(channel, [('+%s' % mode, user) for mode in modeprefix])
self.irc.channels[channel].users.add(user) self.irc.channels[channel].users.add(user)
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts} return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts}
@ -511,9 +516,9 @@ class InspIRCdProtocol(TS6BaseProtocol):
uid, ts, nick, realhost, host, ident, ip = args[0:7] uid, ts, nick, realhost, host, ident, ip = args[0:7]
realname = args[-1] realname = args[-1]
self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip) self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip)
parsedmodes = utils.parseModes(self.irc, uid, [args[8], args[9]]) parsedmodes = self.irc.parseModes(uid, [args[8], args[9]])
log.debug('Applying modes %s for %s', parsedmodes, uid) log.debug('Applying modes %s for %s', parsedmodes, uid)
utils.applyModes(self.irc, uid, parsedmodes) self.irc.applyModes(uid, parsedmodes)
self.irc.servers[numeric].users.add(uid) self.irc.servers[numeric].users.add(uid)
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
@ -550,8 +555,8 @@ class InspIRCdProtocol(TS6BaseProtocol):
channel = utils.toLower(self.irc, args[0]) channel = utils.toLower(self.irc, args[0])
oldobj = self.irc.channels[channel].deepcopy() oldobj = self.irc.channels[channel].deepcopy()
modes = args[2:] modes = args[2:]
changedmodes = utils.parseModes(self.irc, channel, modes) changedmodes = self.irc.parseModes(channel, modes)
utils.applyModes(self.irc, channel, changedmodes) self.irc.applyModes(channel, changedmodes)
ts = int(args[1]) ts = int(args[1])
return {'target': channel, 'modes': changedmodes, 'ts': ts, return {'target': channel, 'modes': changedmodes, 'ts': ts,
'oldchan': oldobj} 'oldchan': oldobj}
@ -563,8 +568,8 @@ class InspIRCdProtocol(TS6BaseProtocol):
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc # <- :70MAAAAAA MODE 70MAAAAAA -i+xc
target = args[0] target = args[0]
modestrings = args[1:] modestrings = args[1:]
changedmodes = utils.parseModes(self.irc, target, modestrings) changedmodes = self.irc.parseModes(target, modestrings)
utils.applyModes(self.irc, target, changedmodes) self.irc.applyModes(target, changedmodes)
return {'target': target, 'modes': changedmodes} return {'target': target, 'modes': changedmodes}
def handle_idle(self, numeric, command, args): def handle_idle(self, numeric, command, args):
@ -631,7 +636,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
# <- :70MAAAAAB OPERTYPE Network_Owner # <- :70MAAAAAB OPERTYPE Network_Owner
omode = [('+o', None)] omode = [('+o', None)]
self.irc.users[numeric].opertype = opertype = args[0].replace("_", " ") self.irc.users[numeric].opertype = opertype = args[0].replace("_", " ")
utils.applyModes(self.irc, numeric, omode) self.irc.applyModes(numeric, omode)
# OPERTYPE is essentially umode +o and metadata in one command; # OPERTYPE is essentially umode +o and metadata in one command;
# we'll call that too. # we'll call that too.
self.irc.callHooks([numeric, 'CLIENT_OPERED', {'text': opertype}]) self.irc.callHooks([numeric, 'CLIENT_OPERED', {'text': opertype}])

View File

@ -16,6 +16,14 @@ import utils
from log import log from log import log
from classes import * from classes import *
class P10UIDGenerator(utils.IncrementalUIDGenerator):
"""Implements an incremental P10 UID Generator."""
def __init__(self, sid):
self.allowedchars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789[]'
self.length = 3
super().__init__(sid)
def p10b64encode(num, length=2): def p10b64encode(num, length=2):
""" """
Encodes a given numeric using P10 Base64 numeric nicks, as documented at Encodes a given numeric using P10 Base64 numeric nicks, as documented at
@ -60,8 +68,8 @@ class P10Protocol(Protocol):
def __init__(self, irc): def __init__(self, irc):
super().__init__(irc) super().__init__(irc)
# Dictionary of UID generators (one for each server) that the protocol module will fill in. # Dictionary of UID generators (one for each server).
self.uidgen = {} self.uidgen = structures.KeyedDefaultdict(P10UIDGenerator)
# SID generator for P10. # SID generator for P10.
self.sidgen = P10SIDGenerator(irc) self.sidgen = P10SIDGenerator(irc)
@ -224,6 +232,12 @@ class P10Protocol(Protocol):
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator', server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator',
manipulatable=False): manipulatable=False):
"""
Spawns a new client with the given options.
Note: No nick collision / valid nickname checks are done here; it is
up to plugins to make sure they don't introduce anything invalid.
"""
# {7N} *** NICK # {7N} *** NICK
# 1 <nickname> # 1 <nickname>
# 2 <hops> # 2 <hops>
@ -242,7 +256,7 @@ class P10Protocol(Protocol):
# Create an UIDGenerator instance for every SID, so that each gets # Create an UIDGenerator instance for every SID, so that each gets
# distinct values. # distinct values.
uid = self.uidgen.setdefault(server, utils.P10UIDGenerator(server)).next_uid() uid = self.uidgen.setdefault(server, P10UIDGenerator(server)).next_uid()
# Fill in all the values we need # Fill in all the values we need
ts = ts or int(time.time()) ts = ts or int(time.time())
@ -256,7 +270,7 @@ class P10Protocol(Protocol):
opertype=opertype) opertype=opertype)
# Fill in modes and add it to our users index # Fill in modes and add it to our users index
utils.applyModes(self.irc, uid, modes) self.irc.applyModes(uid, modes)
self.irc.servers[server].users.add(uid) self.irc.servers[server].users.add(uid)
# Encode IPs when sending # Encode IPs when sending
@ -363,7 +377,7 @@ class P10Protocol(Protocol):
(not self.irc.isInternalServer(numeric)): (not self.irc.isInternalServer(numeric)):
raise LookupError('No such PyLink client/server exists.') raise LookupError('No such PyLink client/server exists.')
utils.applyModes(self.irc, target, modes) self.irc.applyModes(target, modes)
modes = list(modes) modes = list(modes)
# According to the P10 specification: # According to the P10 specification:
@ -521,7 +535,7 @@ class P10Protocol(Protocol):
if ts <= orig_ts: if ts <= orig_ts:
# Only save our prefix modes in the channel state if our TS is lower than or equal to theirs. # Only save our prefix modes in the channel state if our TS is lower than or equal to theirs.
utils.applyModes(self.irc, channel, changedmodes) self.irc.applyModes(channel, changedmodes)
def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0): def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0):
""" """
@ -611,7 +625,7 @@ class P10Protocol(Protocol):
self._send(self.irc.sid, 'FA %s %s' % (target, text)) self._send(self.irc.sid, 'FA %s %s' % (target, text))
# Save the host change as a user mode (this is what P10 does), # Save the host change as a user mode (this is what P10 does),
# so further host checks work. # so further host checks work.
utils.applyModes(self.irc, target, [('+f', text)]) self.irc.applyModes(target, [('+f', text)])
# P10 cloaks aren't as simple as just replacing the displayed host with the one we're # P10 cloaks aren't as simple as just replacing the displayed host with the one we're
# sending. Check for cloak changes properly. # sending. Check for cloak changes properly.
@ -774,8 +788,8 @@ class P10Protocol(Protocol):
# parameters attached. # parameters attached.
if args[5].startswith('+'): if args[5].startswith('+'):
modes = args[5:-3] modes = args[5:-3]
parsedmodes = utils.parseModes(self.irc, uid, modes) parsedmodes = self.irc.parseModes(uid, modes)
utils.applyModes(self.irc, uid, parsedmodes) self.irc.applyModes(uid, parsedmodes)
for modepair in parsedmodes: for modepair in parsedmodes:
if modepair[0][-1] == 'r': if modepair[0][-1] == 'r':
@ -936,7 +950,7 @@ class P10Protocol(Protocol):
# If no modes are given, this will simply be empty. # If no modes are given, this will simply be empty.
modestring = args[2:-1] modestring = args[2:-1]
if modestring: if modestring:
parsedmodes = utils.parseModes(self.irc, channel, modestring) parsedmodes = self.irc.parseModes(channel, modestring)
else: else:
parsedmodes = [] parsedmodes = []
@ -944,7 +958,7 @@ class P10Protocol(Protocol):
parsedmodes.extend(bans) parsedmodes.extend(bans)
if parsedmodes: if parsedmodes:
utils.applyModes(self.irc, channel, parsedmodes) self.irc.applyModes(channel, parsedmodes)
namelist = [] namelist = []
log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel) log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel)
@ -975,7 +989,7 @@ class P10Protocol(Protocol):
self.irc.users[user].channels.add(channel) self.irc.users[user].channels.add(channel)
if their_ts <= our_ts: if their_ts <= our_ts:
utils.applyModes(self.irc, channel, [('+%s' % mode, user) for mode in prefixes]) self.irc.applyModes(channel, [('+%s' % mode, user) for mode in prefixes])
self.irc.channels[channel].users.add(user) self.irc.channels[channel].users.add(user)
@ -1044,8 +1058,8 @@ class P10Protocol(Protocol):
target = utils.toLower(self.irc, target) target = utils.toLower(self.irc, target)
modestrings = args[1:] modestrings = args[1:]
changedmodes = utils.parseModes(self.irc, target, modestrings) changedmodes = self.irc.parseModes(target, modestrings)
utils.applyModes(self.irc, target, changedmodes) self.irc.applyModes(target, changedmodes)
# Call the CLIENT_OPERED hook if +o is being set. # Call the CLIENT_OPERED hook if +o is being set.
if ('+o', None) in changedmodes and target in self.irc.users: if ('+o', None) in changedmodes and target in self.irc.users:
@ -1220,7 +1234,7 @@ class P10Protocol(Protocol):
# Mode does not take an argument when unsetting. # Mode does not take an argument when unsetting.
changedmodes.append(('-%s' % modechar, None)) changedmodes.append(('-%s' % modechar, None))
utils.applyModes(self.irc, channel, changedmodes) self.irc.applyModes(channel, changedmodes)
return {'target': channel, 'modes': changedmodes, 'oldchan': oldobj} return {'target': channel, 'modes': changedmodes, 'oldchan': oldobj}
def handle_account(self, numeric, command, args): def handle_account(self, numeric, command, args):
@ -1265,7 +1279,7 @@ class P10Protocol(Protocol):
text = args[1] text = args[1]
# Assume a usermode +f change, and then update the cloak checking. # Assume a usermode +f change, and then update the cloak checking.
utils.applyModes(self.irc, target, [('+f', text)]) self.irc.applyModes(target, [('+f', text)])
self.checkCloakChange(target) self.checkCloakChange(target)
# We don't need to send any hooks here, checkCloakChange does that for us. # We don't need to send any hooks here, checkCloakChange does that for us.

View File

@ -14,7 +14,7 @@ import utils
from log import log from log import log
from classes import * from classes import *
from ts6_common import TS6BaseProtocol from ts6_common import *
class TS6Protocol(TS6BaseProtocol): class TS6Protocol(TS6BaseProtocol):
def __init__(self, irc): def __init__(self, irc):
@ -31,16 +31,19 @@ class TS6Protocol(TS6BaseProtocol):
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator', server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator',
manipulatable=False): manipulatable=False):
"""Spawns a client with nick <nick> on the given IRC connection. """
Spawns a new client with the given options.
Note: No nick collision / valid nickname checks are done here; it is Note: No nick collision / valid nickname checks are done here; it is
up to plugins to make sure they don't introduce anything invalid.""" up to plugins to make sure they don't introduce anything invalid.
"""
server = server or self.irc.sid server = server or self.irc.sid
if not self.irc.isInternalServer(server): if not self.irc.isInternalServer(server):
raise ValueError('Server %r is not a PyLink server!' % server) raise ValueError('Server %r is not a PyLink server!' % server)
# Create an UIDGenerator instance for every SID, so that each gets
# distinct values. uid = self.uidgen[server].next_uid()
uid = self.uidgen.setdefault(server, utils.TS6UIDGenerator(server)).next_uid()
# EUID: # EUID:
# parameters: nickname, hopcount, nickTS, umodes, username, # parameters: nickname, hopcount, nickTS, umodes, username,
# visible hostname, IP address, UID, real hostname, account name, gecos # visible hostname, IP address, UID, real hostname, account name, gecos
@ -50,13 +53,16 @@ class TS6Protocol(TS6BaseProtocol):
raw_modes = utils.joinModes(modes) raw_modes = utils.joinModes(modes)
u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype) realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype)
utils.applyModes(self.irc, uid, modes)
self.irc.applyModes(uid, modes)
self.irc.servers[server].users.add(uid) self.irc.servers[server].users.add(uid)
self._send(server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " self._send(server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
"{realhost} * :{realname}".format(ts=ts, host=host, "{realhost} * :{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid, nick=nick, ident=ident, uid=uid,
modes=raw_modes, ip=ip, realname=realname, modes=raw_modes, ip=ip, realname=realname,
realhost=realhost)) realhost=realhost))
return u return u
def join(self, client, channel): def join(self, client, channel):
@ -133,7 +139,7 @@ class TS6Protocol(TS6BaseProtocol):
self.irc.channels[channel].users.update(uids) self.irc.channels[channel].users.update(uids)
if ts <= orig_ts: if ts <= orig_ts:
# Only save our prefix modes in the channel state if our TS is lower than or equal to theirs. # Only save our prefix modes in the channel state if our TS is lower than or equal to theirs.
utils.applyModes(self.irc, channel, changedmodes) self.irc.applyModes(channel, changedmodes)
def mode(self, numeric, target, modes, ts=None): def mode(self, numeric, target, modes, ts=None):
"""Sends mode changes from a PyLink client/server.""" """Sends mode changes from a PyLink client/server."""
@ -144,7 +150,7 @@ class TS6Protocol(TS6BaseProtocol):
(not self.irc.isInternalServer(numeric)): (not self.irc.isInternalServer(numeric)):
raise LookupError('No such PyLink client/server exists.') raise LookupError('No such PyLink client/server exists.')
utils.applyModes(self.irc, target, modes) self.irc.applyModes(target, modes)
modes = list(modes) modes = list(modes)
if utils.isChannel(target): if utils.isChannel(target):
@ -421,8 +427,8 @@ class TS6Protocol(TS6BaseProtocol):
self.updateTS(channel, their_ts) self.updateTS(channel, their_ts)
modestring = args[2:-1] or args[2] modestring = args[2:-1] or args[2]
parsedmodes = utils.parseModes(self.irc, channel, modestring) parsedmodes = self.irc.parseModes(channel, modestring)
utils.applyModes(self.irc, channel, parsedmodes) self.irc.applyModes(channel, parsedmodes)
namelist = [] namelist = []
log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel) log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel)
for userpair in userlist: for userpair in userlist:
@ -449,7 +455,7 @@ class TS6Protocol(TS6BaseProtocol):
namelist.append(user) namelist.append(user)
self.irc.users[user].channels.add(channel) self.irc.users[user].channels.add(channel)
if their_ts <= our_ts: if their_ts <= our_ts:
utils.applyModes(self.irc, channel, [('+%s' % mode, user) for mode in finalprefix]) self.irc.applyModes(channel, [('+%s' % mode, user) for mode in finalprefix])
self.irc.channels[channel].users.add(user) self.irc.channels[channel].users.add(user)
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts} return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts}
@ -496,9 +502,9 @@ class TS6Protocol(TS6BaseProtocol):
self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip) self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip)
parsedmodes = utils.parseModes(self.irc, uid, [modes]) parsedmodes = self.irc.parseModes(uid, [modes])
log.debug('Applying modes %s for %s', parsedmodes, uid) log.debug('Applying modes %s for %s', parsedmodes, uid)
utils.applyModes(self.irc, uid, parsedmodes) self.irc.applyModes(uid, parsedmodes)
self.irc.servers[numeric].users.add(uid) self.irc.servers[numeric].users.add(uid)
# Call the OPERED UP hook if +o is being added to the mode list. # Call the OPERED UP hook if +o is being added to the mode list.
@ -557,8 +563,8 @@ class TS6Protocol(TS6BaseProtocol):
channel = utils.toLower(self.irc, args[1]) channel = utils.toLower(self.irc, args[1])
oldobj = self.irc.channels[channel].deepcopy() oldobj = self.irc.channels[channel].deepcopy()
modes = args[2:] modes = args[2:]
changedmodes = utils.parseModes(self.irc, channel, modes) changedmodes = self.irc.parseModes(channel, modes)
utils.applyModes(self.irc, channel, changedmodes) self.irc.applyModes(channel, changedmodes)
ts = int(args[0]) ts = int(args[0])
return {'target': channel, 'modes': changedmodes, 'ts': ts, return {'target': channel, 'modes': changedmodes, 'ts': ts,
'oldchan': oldobj} 'oldchan': oldobj}
@ -568,8 +574,8 @@ class TS6Protocol(TS6BaseProtocol):
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc # <- :70MAAAAAA MODE 70MAAAAAA -i+xc
target = args[0] target = args[0]
modestrings = args[1:] modestrings = args[1:]
changedmodes = utils.parseModes(self.irc, target, modestrings) changedmodes = self.irc.parseModes(target, modestrings)
utils.applyModes(self.irc, target, changedmodes) self.irc.applyModes(target, changedmodes)
# Call the OPERED UP hook if +o is being set. # Call the OPERED UP hook if +o is being set.
if ('+o', None) in changedmodes: if ('+o', None) in changedmodes:
otype = 'Server Administrator' if ('a', None) in self.irc.users[target].modes else 'IRC Operator' otype = 'Server Administrator' if ('a', None) in self.irc.users[target].modes else 'IRC Operator'
@ -615,7 +621,7 @@ class TS6Protocol(TS6BaseProtocol):
modes = [] modes = []
for ban in args[-1].split(): for ban in args[-1].split():
modes.append(('+%s' % mode, ban)) modes.append(('+%s' % mode, ban))
utils.applyModes(self.irc, channel, modes) self.irc.applyModes(channel, modes)
return {'target': channel, 'modes': modes, 'ts': ts} return {'target': channel, 'modes': modes, 'ts': ts}
def handle_whois(self, numeric, command, args): def handle_whois(self, numeric, command, args):

View File

@ -4,6 +4,7 @@ ts6_common.py: Common base protocol class with functions shared by the UnrealIRC
import sys import sys
import os import os
import string
# Import hacks to access utils and classes... # Import hacks to access utils and classes...
curdir = os.path.dirname(__file__) curdir = os.path.dirname(__file__)
@ -12,17 +13,106 @@ sys.path += [curdir, os.path.dirname(curdir)]
import utils import utils
from log import log from log import log
from classes import * from classes import *
import structures
class TS6SIDGenerator():
"""
TS6 SID Generator. <query> is a 3 character string with any combination of
uppercase letters, digits, and #'s. it must contain at least one #,
which are used by the generator as a wildcard. On every next_sid() call,
the first available wildcard character (from the right) will be
incremented to generate the next SID.
When there are no more available SIDs left (SIDs are not reused, only
incremented), RuntimeError is raised.
Example queries:
"1#A" would give: 10A, 11A, 12A ... 19A, 1AA, 1BA ... 1ZA (36 total results)
"#BQ" would give: 0BQ, 1BQ, 2BQ ... 9BQ (10 total results)
"6##" would give: 600, 601, 602, ... 60Y, 60Z, 610, 611, ... 6ZZ (1296 total results)
"""
def __init__(self, irc):
self.irc = irc
try:
self.query = query = list(irc.serverdata["sidrange"])
except KeyError:
raise RuntimeError('(%s) "sidrange" is missing from your server configuration block!' % irc.name)
self.iters = self.query.copy()
self.output = self.query.copy()
self.allowedchars = {}
qlen = len(query)
assert qlen == 3, 'Incorrect length for a SID (must be 3, got %s)' % qlen
assert '#' in query, "Must be at least one wildcard (#) in query"
for idx, char in enumerate(query):
# Iterate over each character in the query string we got, along
# with its index in the string.
assert char in (string.digits+string.ascii_uppercase+"#"), \
"Invalid character %r found." % char
if char == '#':
if idx == 0: # The first char be only digits
self.allowedchars[idx] = string.digits
else:
self.allowedchars[idx] = string.digits+string.ascii_uppercase
self.iters[idx] = iter(self.allowedchars[idx])
self.output[idx] = self.allowedchars[idx][0]
next(self.iters[idx])
def increment(self, pos=2):
"""
Increments the SID generator to the next available SID.
"""
if pos < 0:
# Oh no, we've wrapped back to the start!
raise RuntimeError('No more available SIDs!')
it = self.iters[pos]
try:
self.output[pos] = next(it)
except TypeError: # This position is not an iterator, but a string.
self.increment(pos-1)
except StopIteration:
self.output[pos] = self.allowedchars[pos][0]
self.iters[pos] = iter(self.allowedchars[pos])
next(self.iters[pos])
self.increment(pos-1)
def next_sid(self):
"""
Returns the next unused TS6 SID for the server.
"""
while ''.join(self.output) in self.irc.servers:
# Increment until the SID we have doesn't already exist.
self.increment()
sid = ''.join(self.output)
return sid
class TS6UIDGenerator(utils.IncrementalUIDGenerator):
"""Implements an incremental TS6 UID Generator."""
def __init__(self, sid):
# Define the options for IncrementalUIDGenerator, and then
# initialize its functions.
# TS6 UIDs are 6 characters in length (9 including the SID).
# They go from ABCDEFGHIJKLMNOPQRSTUVWXYZ -> 0123456789 -> wrap around:
# e.g. AAAAAA, AAAAAB ..., AAAAA8, AAAAA9, AAAABA, etc.
self.allowedchars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456879'
self.length = 6
super().__init__(sid)
class TS6BaseProtocol(Protocol): class TS6BaseProtocol(Protocol):
def __init__(self, irc): def __init__(self, irc):
super().__init__(irc) super().__init__(irc)
# Dictionary of UID generators (one for each server) that the protocol module will fill in. # Dictionary of UID generators (one for each server).
self.uidgen = {} self.uidgen = structures.KeyedDefaultdict(TS6UIDGenerator)
# SID generator for TS6. # SID generator for TS6.
self.sidgen = utils.TS6SIDGenerator(irc) self.sidgen = TS6SIDGenerator(irc)
def _send(self, source, msg): def _send(self, source, msg):
"""Sends a TS6-style raw command from a source numeric to the self.irc connection given.""" """Sends a TS6-style raw command from a source numeric to the self.irc connection given."""

View File

@ -16,7 +16,7 @@ sys.path += [curdir, os.path.dirname(curdir)]
import utils import utils
from log import log from log import log
from classes import * from classes import *
from ts6_common import TS6BaseProtocol from ts6_common import *
class UnrealProtocol(TS6BaseProtocol): class UnrealProtocol(TS6BaseProtocol):
def __init__(self, irc): def __init__(self, irc):
@ -63,23 +63,27 @@ class UnrealProtocol(TS6BaseProtocol):
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator', server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator',
manipulatable=False): manipulatable=False):
"""Spawns a client with nick <nick> on the given IRC connection. """
Spawns a new client with the given options.
Note: No nick collision / valid nickname checks are done here; it is Note: No nick collision / valid nickname checks are done here; it is
up to plugins to make sure they don't introduce anything invalid.""" up to plugins to make sure they don't introduce anything invalid.
"""
server = server or self.irc.sid server = server or self.irc.sid
if not self.irc.isInternalServer(server): if not self.irc.isInternalServer(server):
raise ValueError('Server %r is not a PyLink server!' % server) raise ValueError('Server %r is not a PyLink server!' % server)
# Unreal 4.0 uses TS6-style UIDs. They don't start from AAAAAA like other IRCd's # Unreal 4.0 uses TS6-style UIDs. They don't start from AAAAAA like other IRCd's
# do, but that doesn't matter to us... # do, but that doesn't matter to us...
uid = self.uidgen.setdefault(server, utils.TS6UIDGenerator(server)).next_uid() uid = self.uidgen[server].next_uid()
ts = ts or int(time.time()) ts = ts or int(time.time())
realname = realname or self.irc.botdata['realname'] realname = realname or self.irc.botdata['realname']
realhost = realhost or host realhost = realhost or host
raw_modes = utils.joinModes(modes) raw_modes = utils.joinModes(modes)
u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype) realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype)
utils.applyModes(self.irc, uid, modes) self.irc.applyModes(uid, modes)
self.irc.servers[server].users.add(uid) self.irc.servers[server].users.add(uid)
# UnrealIRCd requires encoding the IP by first packing it into a binary format, # UnrealIRCd requires encoding the IP by first packing it into a binary format,
@ -176,7 +180,7 @@ class UnrealProtocol(TS6BaseProtocol):
self.irc.channels[channel].users.update(uids) self.irc.channels[channel].users.update(uids)
if ts <= orig_ts: if ts <= orig_ts:
# Only save our prefix modes in the channel state if our TS is lower than or equal to theirs. # Only save our prefix modes in the channel state if our TS is lower than or equal to theirs.
utils.applyModes(self.irc, channel, changedmodes) self.irc.applyModes(channel, changedmodes)
def ping(self, source=None, target=None): def ping(self, source=None, target=None):
"""Sends a PING to a target server. Periodic PINGs are sent to our uplink """Sends a PING to a target server. Periodic PINGs are sent to our uplink
@ -211,7 +215,7 @@ class UnrealProtocol(TS6BaseProtocol):
(not self.irc.isInternalServer(numeric)): (not self.irc.isInternalServer(numeric)):
raise LookupError('No such PyLink client/server exists.') raise LookupError('No such PyLink client/server exists.')
utils.applyModes(self.irc, target, modes) self.irc.applyModes(target, modes)
joinedmodes = utils.joinModes(modes) joinedmodes = utils.joinModes(modes)
if utils.isChannel(target): if utils.isChannel(target):
# The MODE command is used for channel mode changes only # The MODE command is used for channel mode changes only
@ -374,8 +378,8 @@ class UnrealProtocol(TS6BaseProtocol):
self.irc.servers[numeric].users.add(uid) self.irc.servers[numeric].users.add(uid)
# Handle user modes # Handle user modes
parsedmodes = utils.parseModes(self.irc, uid, [modestring]) parsedmodes = self.irc.parseModes(uid, [modestring])
utils.applyModes(self.irc, uid, parsedmodes) self.irc.applyModes(uid, parsedmodes)
# The cloaked (+x) host is completely separate from the displayed host # The cloaked (+x) host is completely separate from the displayed host
# and real host in that it is ONLY shown if the user is +x (cloak mode # and real host in that it is ONLY shown if the user is +x (cloak mode
@ -575,7 +579,7 @@ class UnrealProtocol(TS6BaseProtocol):
self.irc.users[user].channels.add(channel) self.irc.users[user].channels.add(channel)
# Only merge the remote's prefix modes if their TS is smaller or equal to ours. # Only merge the remote's prefix modes if their TS is smaller or equal to ours.
if their_ts <= our_ts: if their_ts <= our_ts:
utils.applyModes(self.irc, channel, [('+%s' % mode, user) for mode in finalprefix]) self.irc.applyModes(channel, [('+%s' % mode, user) for mode in finalprefix])
self.irc.channels[channel].users.add(user) self.irc.channels[channel].users.add(user)
return {'channel': channel, 'users': namelist, 'modes': self.irc.channels[channel].modes, 'ts': their_ts} return {'channel': channel, 'users': namelist, 'modes': self.irc.channels[channel].modes, 'ts': their_ts}
@ -637,9 +641,9 @@ class UnrealProtocol(TS6BaseProtocol):
channel = utils.toLower(self.irc, args[0]) channel = utils.toLower(self.irc, args[0])
oldobj = self.irc.channels[channel].deepcopy() oldobj = self.irc.channels[channel].deepcopy()
modes = list(filter(None, args[1:])) # normalize whitespace modes = list(filter(None, args[1:])) # normalize whitespace
parsedmodes = utils.parseModes(self.irc, channel, modes) parsedmodes = self.irc.parseModes(channel, modes)
if parsedmodes: if parsedmodes:
utils.applyModes(self.irc, channel, parsedmodes) self.irc.applyModes(channel, parsedmodes)
if numeric in self.irc.servers and args[-1].isdigit(): if numeric in self.irc.servers and args[-1].isdigit():
# Sender is a server AND last arg is number. Perform TS updates. # Sender is a server AND last arg is number. Perform TS updates.
their_ts = int(args[-1]) their_ts = int(args[-1])
@ -691,8 +695,8 @@ class UnrealProtocol(TS6BaseProtocol):
target = self._getUid(args[0]) target = self._getUid(args[0])
modes = args[1:] modes = args[1:]
parsedmodes = utils.parseModes(self.irc, target, modes) parsedmodes = self.irc.parseModes(target, modes)
utils.applyModes(self.irc, target, parsedmodes) self.irc.applyModes(target, parsedmodes)
# If +x/-x is being set, update cloaked host info. # If +x/-x is being set, update cloaked host info.
self.checkCloakChange(target, parsedmodes) self.checkCloakChange(target, parsedmodes)
@ -733,8 +737,8 @@ class UnrealProtocol(TS6BaseProtocol):
def handle_umode2(self, numeric, command, args): def handle_umode2(self, numeric, command, args):
"""Handles UMODE2, used to set user modes on oneself.""" """Handles UMODE2, used to set user modes on oneself."""
# <- :GL UMODE2 +W # <- :GL UMODE2 +W
parsedmodes = utils.parseModes(self.irc, numeric, args) parsedmodes = self.irc.parseModes(numeric, args)
utils.applyModes(self.irc, numeric, parsedmodes) self.irc.applyModes(numeric, parsedmodes)
if ('+o', None) in parsedmodes: if ('+o', None) in parsedmodes:
# If +o being set, call the CLIENT_OPERED internal hook. # If +o being set, call the CLIENT_OPERED internal hook.
@ -785,7 +789,7 @@ class UnrealProtocol(TS6BaseProtocol):
# When SETHOST or CHGHOST is used, modes +xt are implicitly set on the # When SETHOST or CHGHOST is used, modes +xt are implicitly set on the
# target. # target.
utils.applyModes(self.irc, numeric, [('+x', None), ('+t', None)]) self.irc.applyModes(numeric, [('+x', None), ('+t', None)])
return {'target': numeric, 'newhost': newhost} return {'target': numeric, 'newhost': newhost}
@ -810,7 +814,7 @@ class UnrealProtocol(TS6BaseProtocol):
# When SETHOST or CHGHOST is used, modes +xt are implicitly set on the # When SETHOST or CHGHOST is used, modes +xt are implicitly set on the
# target. # target.
utils.applyModes(self.irc, target, [('+x', None), ('+t', None)]) self.irc.applyModes(target, [('+x', None), ('+t', None)])
return {'target': target, 'newhost': newhost} return {'target': target, 'newhost': newhost}

19
structures.py Normal file
View File

@ -0,0 +1,19 @@
"""
structures.py - PyLink data structures module.
This module contains custom data structures that may be useful in various situations.
"""
import collections
class KeyedDefaultdict(collections.defaultdict):
"""
Subclass of defaultdict allowing the key to be passed to the default factory.
"""
def __missing__(self, key):
if self.default_factory is None:
# If there is no default factory, just let defaultdict handle it
super().__missing__(self, key)
else:
value = self[key] = self.default_factory(key)
return value

271
utils.py
View File

@ -10,25 +10,11 @@ import re
import inspect import inspect
import importlib import importlib
import os import os
import collections
from log import log from log import log
import world import world
import conf import conf
class KeyedDefaultdict(collections.defaultdict):
"""
Subclass of defaultdict allowing the key to be passed to the default factory.
"""
def __missing__(self, key):
if self.default_factory is None:
# If there is no default factory, just let defaultdict handle it
super().__missing__(self, key)
else:
value = self[key] = self.default_factory(key)
return value
class NotAuthenticatedError(Exception): class NotAuthenticatedError(Exception):
""" """
Exception raised by checkAuthenticated() when a user fails authentication Exception raised by checkAuthenticated() when a user fails authentication
@ -43,9 +29,6 @@ class IncrementalUIDGenerator():
""" """
def __init__(self, sid): def __init__(self, sid):
# TS6 UIDs are 6 characters in length (9 including the SID).
# They wrap from ABCDEFGHIJKLMNOPQRSTUVWXYZ -> 0123456789 -> wrap around:
# (e.g. AAAAAA, AAAAAB ..., AAAAA8, AAAAA9, AAAABA)
if not (hasattr(self, 'allowedchars') and hasattr(self, 'length')): if not (hasattr(self, 'allowedchars') and hasattr(self, 'length')):
raise RuntimeError("Allowed characters list not defined. Subclass " raise RuntimeError("Allowed characters list not defined. Subclass "
"%s by defining self.allowedchars and self.length " "%s by defining self.allowedchars and self.length "
@ -80,99 +63,6 @@ class IncrementalUIDGenerator():
self.increment() self.increment()
return uid return uid
class TS6UIDGenerator(IncrementalUIDGenerator):
"""Implements an incremental TS6 UID Generator."""
def __init__(self, sid):
# Define the options for IncrementalUIDGenerator, and then
# initialize its functions.
self.allowedchars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456879'
self.length = 6
super().__init__(sid)
class P10UIDGenerator(IncrementalUIDGenerator):
"""Implements an incremental P10 UID Generator."""
def __init__(self, sid):
self.allowedchars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789[]'
self.length = 3
super().__init__(sid)
class TS6SIDGenerator():
"""
TS6 SID Generator. <query> is a 3 character string with any combination of
uppercase letters, digits, and #'s. it must contain at least one #,
which are used by the generator as a wildcard. On every next_sid() call,
the first available wildcard character (from the right) will be
incremented to generate the next SID.
When there are no more available SIDs left (SIDs are not reused, only
incremented), RuntimeError is raised.
Example queries:
"1#A" would give: 10A, 11A, 12A ... 19A, 1AA, 1BA ... 1ZA (36 total results)
"#BQ" would give: 0BQ, 1BQ, 2BQ ... 9BQ (10 total results)
"6##" would give: 600, 601, 602, ... 60Y, 60Z, 610, 611, ... 6ZZ (1296 total results)
"""
def __init__(self, irc):
self.irc = irc
try:
self.query = query = list(irc.serverdata["sidrange"])
except KeyError:
raise RuntimeError('(%s) "sidrange" is missing from your server configuration block!' % irc.name)
self.iters = self.query.copy()
self.output = self.query.copy()
self.allowedchars = {}
qlen = len(query)
assert qlen == 3, 'Incorrect length for a SID (must be 3, got %s)' % qlen
assert '#' in query, "Must be at least one wildcard (#) in query"
for idx, char in enumerate(query):
# Iterate over each character in the query string we got, along
# with its index in the string.
assert char in (string.digits+string.ascii_uppercase+"#"), \
"Invalid character %r found." % char
if char == '#':
if idx == 0: # The first char be only digits
self.allowedchars[idx] = string.digits
else:
self.allowedchars[idx] = string.digits+string.ascii_uppercase
self.iters[idx] = iter(self.allowedchars[idx])
self.output[idx] = self.allowedchars[idx][0]
next(self.iters[idx])
def increment(self, pos=2):
"""
Increments the SID generator to the next available SID.
"""
if pos < 0:
# Oh no, we've wrapped back to the start!
raise RuntimeError('No more available SIDs!')
it = self.iters[pos]
try:
self.output[pos] = next(it)
except TypeError: # This position is not an iterator, but a string.
self.increment(pos-1)
except StopIteration:
self.output[pos] = self.allowedchars[pos][0]
self.iters[pos] = iter(self.allowedchars[pos])
next(self.iters[pos])
self.increment(pos-1)
def next_sid(self):
"""
Returns the next unused TS6 SID for the server.
"""
while ''.join(self.output) in self.irc.servers:
# Increment until the SID we have doesn't already exist.
self.increment()
sid = ''.join(self.output)
return sid
def add_cmd(func, name=None): def add_cmd(func, name=None):
"""Binds an IRC command function to the given command name.""" """Binds an IRC command function to the given command name."""
if name is None: if name is None:
@ -226,164 +116,21 @@ def isHostmask(text):
def parseModes(irc, target, args): def parseModes(irc, target, args):
"""Parses a modestring list into a list of (mode, argument) tuples. """Parses a modestring list into a list of (mode, argument) tuples.
['+mitl-o', '3', 'person'] => [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')] ['+mitl-o', '3', 'person'] => [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')]
This method is deprecated. Use irc.parseModes() instead.
""" """
# http://www.irc.org/tech_docs/005.html log.warning("(%s) utils.parseModes is deprecated. Use irc.parseModes() instead!", irc.name)
# A = Mode that adds or removes a nick or address to a list. Always has a parameter. return irc.parseModes(target, args)
# B = Mode that changes a setting and always has a parameter.
# C = Mode that changes a setting and only has a parameter when set.
# D = Mode that changes a setting and never has a parameter.
assert args, 'No valid modes were supplied!'
usermodes = not isChannel(target)
prefix = ''
modestring = args[0]
args = args[1:]
if usermodes:
log.debug('(%s) Using irc.umodes for this query: %s', irc.name, irc.umodes)
if target not in irc.users:
log.warning('(%s) Possible desync! Mode target %s is not in the users index.', irc.name, target)
return [] # Return an empty mode list
supported_modes = irc.umodes
oldmodes = irc.users[target].modes
else:
log.debug('(%s) Using irc.cmodes for this query: %s', irc.name, irc.cmodes)
if target not in irc.channels:
log.warning('(%s) Possible desync! Mode target %s is not in the channels index.', irc.name, target)
return []
supported_modes = irc.cmodes
oldmodes = irc.channels[target].modes
res = []
for mode in modestring:
if mode in '+-':
prefix = mode
else:
if not prefix:
prefix = '+'
arg = None
log.debug('Current mode: %s%s; args left: %s', prefix, mode, args)
try:
if mode in (supported_modes['*A'] + supported_modes['*B']):
# Must have parameter.
log.debug('Mode %s: This mode must have parameter.', mode)
arg = args.pop(0)
if prefix == '-' and mode in supported_modes['*B'] and arg == '*':
# Charybdis allows unsetting +k without actually
# knowing the key by faking the argument when unsetting
# as a single "*".
# We'd need to know the real argument of +k for us to
# be able to unset the mode.
oldargs = [m[1] for m in oldmodes if m[0] == mode]
if oldargs:
# Set the arg to the old one on the channel.
arg = oldargs[0]
log.debug("Mode %s: coersing argument of '*' to %r.", mode, arg)
elif mode in irc.prefixmodes and not usermodes:
# We're setting a prefix mode on someone (e.g. +o user1)
log.debug('Mode %s: This mode is a prefix mode.', mode)
arg = args.pop(0)
# Convert nicks to UIDs implicitly; most IRCds will want
# this already.
arg = irc.nickToUid(arg) or arg
if arg not in irc.users: # Target doesn't exist, skip it.
log.debug('(%s) Skipping setting mode "%s %s"; the '
'target doesn\'t seem to exist!', irc.name,
mode, arg)
continue
elif prefix == '+' and mode in supported_modes['*C']:
# Only has parameter when setting.
log.debug('Mode %s: Only has parameter when setting.', mode)
arg = args.pop(0)
except IndexError:
log.warning('(%s/%s) Error while parsing mode %r: mode requires an '
'argument but none was found. (modestring: %r)',
irc.name, target, mode, modestring)
continue # Skip this mode; don't error out completely.
res.append((prefix + mode, arg))
return res
def applyModes(irc, target, changedmodes): def applyModes(irc, target, changedmodes):
"""Takes a list of parsed IRC modes, and applies them on the given target. """Takes a list of parsed IRC modes, and applies them on the given target.
The target can be either a channel or a user; this is handled automatically.""" The target can be either a channel or a user; this is handled automatically.
usermodes = not isChannel(target)
log.debug('(%s) Using usermodes for this query? %s', irc.name, usermodes)
try: This method is deprecated. Use irc.applyModes() instead.
if usermodes: """
old_modelist = irc.users[target].modes log.warning("(%s) utils.applyModes is deprecated. Use irc.applyModes() instead!", irc.name)
supported_modes = irc.umodes return irc.applyModes(target, changedmodes)
else:
old_modelist = irc.channels[target].modes
supported_modes = irc.cmodes
except KeyError:
log.warning('(%s) Possible desync? Mode target %s is unknown.', irc.name, target)
return
modelist = set(old_modelist)
log.debug('(%s) Applying modes %r on %s (initial modelist: %s)', irc.name, changedmodes, target, modelist)
for mode in changedmodes:
# Chop off the +/- part that parseModes gives; it's meaningless for a mode list.
try:
real_mode = (mode[0][1], mode[1])
except IndexError:
real_mode = mode
if not usermodes:
# We only handle +qaohv for now. Iterate over every supported mode:
# if the IRCd supports this mode and it is the one being set, add/remove
# the person from the corresponding prefix mode list (e.g. c.prefixmodes['op']
# for ops).
for pmode, pmodelist in irc.channels[target].prefixmodes.items():
if pmode in irc.cmodes and real_mode[0] == irc.cmodes[pmode]:
log.debug('(%s) Initial prefixmodes list: %s', irc.name, pmodelist)
if mode[0][0] == '+':
pmodelist.add(mode[1])
else:
pmodelist.discard(mode[1])
log.debug('(%s) Final prefixmodes list: %s', irc.name, pmodelist)
if real_mode[0] in irc.prefixmodes:
# Don't add prefix modes to IrcChannel.modes; they belong in the
# prefixmodes mapping handled above.
log.debug('(%s) Not adding mode %s to IrcChannel.modes because '
'it\'s a prefix mode.', irc.name, str(mode))
continue
if mode[0][0] == '+':
# We're adding a mode
existing = [m for m in modelist if m[0] == real_mode[0] and m[1] != real_mode[1]]
if existing and real_mode[1] and real_mode[0] not in irc.cmodes['*A']:
# The mode we're setting takes a parameter, but is not a list mode (like +beI).
# Therefore, only one version of it can exist at a time, and we must remove
# any old modepairs using the same letter. Otherwise, we'll get duplicates when,
# for example, someone sets mode "+l 30" on a channel already set "+l 25".
log.debug('(%s) Old modes for mode %r exist on %s, removing them: %s',
irc.name, real_mode, target, str(existing))
[modelist.discard(oldmode) for oldmode in existing]
modelist.add(real_mode)
log.debug('(%s) Adding mode %r on %s', irc.name, real_mode, target)
else:
log.debug('(%s) Removing mode %r on %s', irc.name, real_mode, target)
# We're removing a mode
if real_mode[1] is None:
# We're removing a mode that only takes arguments when setting.
# Remove all mode entries that use the same letter as the one
# we're unsetting.
for oldmode in modelist.copy():
if oldmode[0] == real_mode[0]:
modelist.discard(oldmode)
else:
# Swap the - for a + and then remove it from the list.
modelist.discard(real_mode)
log.debug('(%s) Final modelist: %s', irc.name, modelist)
if usermodes:
irc.users[target].modes = modelist
else:
irc.channels[target].modes = modelist
def joinModes(modes): def joinModes(modes):
"""Takes a list of (mode, arg) tuples in parseModes() format, and """Takes a list of (mode, arg) tuples in parseModes() format, and