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

Merge branch 'master' into wip/unrealircd

Conflicts:
	classes.py
This commit is contained in:
James Lu 2015-09-23 18:59:17 -07:00
commit 886994475d
19 changed files with 1070 additions and 654 deletions

View File

@ -1,12 +1,12 @@
# PyLink
PyLink is an extensible, plugin-based IRC PseudoService written in Python. It aims to be a replacement for the now-defunct Janus.
PyLink is an extensible, plugin-based IRC Services framework written in Python. It aims to be a replacement for the now-defunct Janus.
## Usage
**PyLink is a work in progress and thus may be very unstable**! No warranty is provided if this completely wrecks your network and causes widespread rioting amongst your users!
That said, please report any bugs you find to the [issue tracker](https://github.com/GLolol/PyLink/issues). Pull requests are open if you'd like to contribute.
That said, please report any bugs you find to the [issue tracker](https://github.com/GLolol/PyLink/issues). Pull requests are open if you'd like to contribute: note that **master** is bugfix only; new stuff goes to the **devel** branch.
### Dependencies
@ -22,10 +22,10 @@ Dependencies currently include:
* charybdis (3.5.x / git master) - module `ts6`
* Elemental-IRCd (6.6.x / git master) - module `ts6`
### Installation
### Setup
1) Rename `config.yml.example` to `config.yml` and configure your instance there. Not all options are properly implemented yet, and the configuration schema isn't finalized yet - this means that your configuration may break in an update!
1) Rename `config.yml.example` to `config.yml` and configure your instance there. Note that the configuration format isn't finalized yet - this means that your configuration may break in an update!
2) Run `main.py` from the command line.
2) Run `./pylink` from the command line.
3) Profit???

View File

@ -6,6 +6,7 @@ import threading
import ssl
from collections import defaultdict
import hashlib
from copy import deepcopy
from log import log
from conf import conf
@ -26,7 +27,9 @@ class Irc():
self.lastping = time.time()
# Server, channel, and user indexes to be populated by our protocol module
self.servers = {self.sid: IrcServer(None, self.serverdata['hostname'], internal=True)}
self.servers = {self.sid: IrcServer(None, self.serverdata['hostname'],
internal=True, desc=self.serverdata.get('serverdesc')
or self.botdata['serverdesc'])}
self.users = {}
self.channels = defaultdict(IrcChannel)
# Sets flags such as whether to use halfops, etc. The default RFC1459
@ -55,13 +58,6 @@ class Irc():
self.uplink = None
self.start_ts = int(time.time())
# UID generators, for servers that need it
self.uidgen = {}
# Local data for the IRC object, which protocol modules may use for
# initialization, etc.
self.protodata = {}
def __init__(self, netname, proto):
# Initialize some variables
self.name = netname.lower()
@ -211,20 +207,24 @@ class Irc():
line = line.strip(b'\r')
# FIXME: respect other encodings?
line = line.decode("utf-8", "replace")
log.debug("(%s) <- %s", self.name, line)
hook_args = None
try:
hook_args = self.proto.handle_events(line)
except Exception:
log.exception('(%s) Caught error in handle_events, disconnecting!', self.name)
return
# Only call our hooks if there's data to process. Handlers that support
# hooks will return a dict of parsed arguments, which can be passed on
# to plugins and the like. For example, the JOIN handler will return
# something like: {'channel': '#whatever', 'users': ['UID1', 'UID2',
# 'UID3']}, etc.
if hook_args is not None:
self.callHooks(hook_args)
self.runline(line)
def runline(self, line):
"""Sends a command to the protocol module."""
log.debug("(%s) <- %s", self.name, line)
try:
hook_args = self.proto.handle_events(line)
except Exception:
log.exception('(%s) Caught error in handle_events, disconnecting!', self.name)
log.error('(%s) The offending line was: <- %s', self.name, line)
return
# Only call our hooks if there's data to process. Handlers that support
# hooks will return a dict of parsed arguments, which can be passed on
# to plugins and the like. For example, the JOIN handler will return
# something like: {'channel': '#whatever', 'users': ['UID1', 'UID2',
# 'UID3']}, etc.
if hook_args is not None:
self.callHooks(hook_args)
def callHooks(self, hook_args):
numeric, command, parsed_args = hook_args
@ -239,15 +239,17 @@ class Irc():
if command in hook_map:
hook_cmd = hook_map[command]
hook_cmd = parsed_args.get('parse_as') or hook_cmd
log.debug('Parsed args %r received from %s handler (calling hook %s)', parsed_args, command, hook_cmd)
log.debug('(%s) Parsed args %r received from %s handler (calling hook %s)',
self.name, parsed_args, command, hook_cmd)
# Iterate over hooked functions, catching errors accordingly
for hook_func in world.command_hooks[hook_cmd]:
try:
log.debug('Calling function %s', hook_func)
log.debug('(%s) Calling function %s', self.name, hook_func)
hook_func(self, numeric, command, parsed_args)
except Exception:
# We don't want plugins to crash our servers...
log.exception('Unhandled exception caught in %r' % hook_func)
log.exception('(%s) Unhandled exception caught in %r',
self.name, hook_func)
continue
def send(self, data):
@ -275,7 +277,9 @@ class Irc():
host = self.serverdata["hostname"]
log.info('(%s) Connected! Spawning main client %s.', self.name, nick)
olduserobj = self.pseudoclient
self.pseudoclient = self.proto.spawnClient(nick, ident, host, modes={("+o", None)})
self.pseudoclient = self.proto.spawnClient(nick, ident, host,
modes={("+o", None)},
manipulatable=True)
for chan in self.serverdata['channels']:
self.proto.joinClient(self.pseudoclient.uid, chan)
# PyLink internal hook called when spawnMain is called and the
@ -285,7 +289,7 @@ class Irc():
class IrcUser():
def __init__(self, nick, ts, uid, ident='null', host='null',
realname='PyLink dummy client', realhost='null',
ip='0.0.0.0'):
ip='0.0.0.0', manipulatable=False):
self.nick = nick
self.ts = ts
self.uid = uid
@ -300,6 +304,11 @@ class IrcUser():
self.channels = set()
self.away = ''
# Whether the client should be marked as manipulatable
# (i.e. we are allowed to play with it using bots.py's commands).
# For internal services clients, this should always be False.
self.manipulatable = manipulatable
def __repr__(self):
return repr(self.__dict__)
@ -311,11 +320,12 @@ class IrcServer():
name: The name of the server.
internal: Whether the server is an internal PyLink PseudoServer.
"""
def __init__(self, uplink, name, internal=False):
def __init__(self, uplink, name, internal=False, desc="(None given)"):
self.uplink = uplink
self.users = set()
self.internal = internal
self.name = name.lower()
self.desc = desc
def __repr__(self):
return repr(self.__dict__)
@ -337,6 +347,9 @@ class IrcChannel():
s.discard(target)
self.users.discard(target)
def deepcopy(self):
return deepcopy(self)
### FakeIRC classes, used for test cases
class FakeIRC(Irc):
@ -404,7 +417,7 @@ class FakeProto(Protocol):
pass
def spawnClient(self, nick, *args, **kwargs):
uid = randint(1, 10000000000)
uid = str(randint(1, 10000000000))
ts = int(time.time())
self.irc.users[uid] = user = IrcUser(nick, ts, uid)
return user

View File

@ -27,7 +27,8 @@ testconf = {'bot':
'hostname': "pylink.unittest",
'sid': "9PY",
'channels': ["#pylink"],
'maxnicklen': 20
'maxnicklen': 20,
'sidrange': '8##'
})
}
if world.testing:

View File

@ -20,6 +20,22 @@ login:
user: admin
password: changeme
relay:
# This block defines various options for the Relay plugin. You don't need this
# if you aren't using it.
# Determines whether remote opers will have user mode +H (hideoper) set on them.
# This has the benefit of lowering the oper count in /lusers and /stats (P|p),
# but only on IRCds that supported the mode.
# It defaults to true if not set.
hideoper: true
# Determines whether real IPs should be sent across the relay. You should
# generally have a consensus with your linked networks whether this should
# be turned on. You will see other networks' user IP addresses, and they
# will see yours.
show_ips: false
servers:
yournet:
# Server IP, port, and passwords
@ -28,6 +44,9 @@ servers:
recvpass: "abcd"
sendpass: "abcd"
# The full network name, used by plugins.
netname: "yournet"
# Hostname we will use to connect to the remote server
hostname: "pylink.yournet"
@ -81,6 +100,7 @@ servers:
sendpass: "abcd"
hostname: "pylink.example.com"
sid: "8PY"
netname: "some network"
# Leave this as an empty list if you don't want to join any channels.
channels: []

View File

@ -4,22 +4,22 @@ import utils
from log import log
import world
# Handle KILLs sent to the PyLink client and respawn
def handle_kill(irc, source, command, args):
"""Handle KILLs to the main PyLink client, respawning it as needed."""
if args['target'] == irc.pseudoclient.uid:
irc.spawnMain()
utils.add_hook(handle_kill, 'KILL')
# Handle KICKs to the PyLink client, rejoining the channels
def handle_kick(irc, source, command, args):
"""Handle KICKs to the main PyLink client, rejoining channels as needed."""
kicked = args['target']
channel = args['channel']
if kicked == irc.pseudoclient.uid:
irc.proto.joinClient(irc.pseudoclient.uid, channel)
utils.add_hook(handle_kick, 'KICK')
# Handle commands sent to the PyLink client (PRIVMSG)
def handle_commands(irc, source, command, args):
"""Handle commands sent to the PyLink client (PRIVMSG)."""
if args['target'] == irc.pseudoclient.uid:
text = args['text'].strip()
cmd_args = text.split(' ')
@ -39,12 +39,14 @@ def handle_commands(irc, source, command, args):
irc.msg(source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
utils.add_hook(handle_commands, 'PRIVMSG')
# Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd).
def handle_whois(irc, source, command, args):
"""Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd)."""
target = args['target']
user = irc.users.get(target)
if user is None:
log.warning('(%s) Got a WHOIS request for %r from %r, but the target doesn\'t exist in irc.users!', irc.name, target, source)
log.warning('(%s) Got a WHOIS request for %r from %r, but the target '
'doesn\'t exist in irc.users!', irc.name, target, source)
return
f = irc.proto.numericServer
server = utils.clientToServer(irc, target) or irc.sid
nick = user.nick
@ -72,12 +74,12 @@ def handle_whois(irc, source, command, args):
f(server, 319, source, '%s :%s' % (nick, ' '.join(public_chans)))
# 312: sends the server the target is on, and its server description.
f(server, 312, source, "%s %s :%s" % (nick, irc.servers[server].name,
irc.serverdata.get('serverdesc') or irc.botdata['serverdesc']))
irc.servers[server].desc))
# 313: sends a string denoting the target's operator privilege,
# only if they have umode +o.
if ('o', None) in user.modes:
if hasattr(user, 'opertype'):
opertype = user.opertype.replace("_", " ")
opertype = user.opertype
else:
opertype = "IRC Operator"
# Let's be gramatically correct.
@ -86,6 +88,7 @@ def handle_whois(irc, source, command, args):
# 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd.
# Only show this to opers!
if sourceisOper:
f(server, 378, source, "%s :is connecting from %s@%s %s" % (nick, user.ident, user.realhost, user.ip))
f(server, 379, source, '%s :is using modes %s' % (nick, utils.joinModes(user.modes)))
# 317: shows idle and signon time. However, we don't track the user's real
# idle time, so we simply return 0.
@ -106,3 +109,14 @@ def handle_whois(irc, source, command, args):
# 318: End of WHOIS.
f(server, 318, source, "%s :End of /WHOIS list" % nick)
utils.add_hook(handle_whois, 'WHOIS')
def handle_mode(irc, source, command, args):
"""Protect against forced deoper attempts."""
target = args['target']
modes = args['modes']
# If the sender is not a PyLink client, and the target IS a protected
# client, revert any forced deoper attempts.
if utils.isInternalClient(irc, target) and not utils.isInternalClient(irc, source):
if ('-o', None) in modes and (target == irc.pseudoclient.uid or not utils.isManipulatableClient(irc, target)):
irc.proto.modeServer(irc.sid, target, {('+o', None)})
utils.add_hook(handle_mode, 'MODE')

View File

@ -22,7 +22,7 @@ def spawnclient(irc, source, args):
except ValueError:
irc.msg(source, "Error: Not enough arguments. Needs 3: nick, user, host.")
return
irc.proto.spawnClient(nick, ident, host)
irc.proto.spawnClient(nick, ident, host, manipulatable=True)
@utils.add_cmd
def quit(irc, source, args):
@ -40,6 +40,9 @@ def quit(irc, source, args):
return
u = utils.nickToUid(irc, nick)
quitmsg = ' '.join(args[1:]) or 'Client Quit'
if not utils.isManipulatableClient(irc, u):
irc.msg(source, "Error: Cannot force quit a protected PyLink services client.")
return
irc.proto.quitClient(u, quitmsg)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}])
@ -57,6 +60,9 @@ def joinclient(irc, source, args):
irc.msg(source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
return
u = utils.nickToUid(irc, nick)
if not utils.isManipulatableClient(irc, u):
irc.msg(source, "Error: Cannot force join a protected PyLink services client.")
return
for channel in clist:
if not utils.isChannel(channel):
irc.msg(source, "Error: Invalid channel name %r." % channel)
@ -85,6 +91,9 @@ def nick(irc, source, args):
elif not utils.isNick(newnick):
irc.msg(source, 'Error: Invalid nickname %r.' % newnick)
return
elif not utils.isManipulatableClient(irc, u):
irc.msg(source, "Error: Cannot force nick changes for a protected PyLink services client.")
return
irc.proto.nickClient(u, newnick)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}])
@ -102,6 +111,9 @@ def part(irc, source, args):
irc.msg(source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
return
u = utils.nickToUid(irc, nick)
if not utils.isManipulatableClient(irc, u):
irc.msg(source, "Error: Cannot force part a protected PyLink services client.")
return
for channel in clist:
if not utils.isChannel(channel):
irc.msg(source, "Error: Invalid channel name %r." % channel)
@ -138,30 +150,35 @@ def kick(irc, source, args):
def mode(irc, source, args):
"""<source> <target> <modes>
Admin-only. Sets modes <modes> on <target> from <source>, where <source> is either the nick of a PyLink client, or the SID of a PyLink server."""
Admin-only. Sets modes <modes> on <target> from <source>, where <source> is either the nick of a PyLink client, or the SID of a PyLink server. <target> can be either a nick or a channel."""
utils.checkAuthenticated(irc, source, allowOper=False)
try:
modesource, target, modes = args[0], args[1], args[2:]
except IndexError:
irc.msg(source, 'Error: Not enough arguments. Needs 3: source nick, target, modes to set.')
return
if not modes:
irc.msg(source, "Error: No modes given to set!")
return
target = utils.nickToUid(irc, target) or target
extclient = target in irc.users and not utils.isInternalClient(irc, target)
parsedmodes = utils.parseModes(irc, target, modes)
targetuid = utils.nickToUid(irc, target)
if targetuid:
target = targetuid
elif not utils.isChannel(target):
ischannel = target in irc.channels
if not (target in irc.users or ischannel):
irc.msg(source, "Error: Invalid channel or nick %r." % target)
return
elif not parsedmodes:
irc.msg(source, "Error: No valid modes were given.")
return
elif not (ischannel or utils.isManipulatableClient(irc, target)):
irc.msg(source, "Error: Can only set modes on channels or non-protected PyLink clients.")
return
if utils.isInternalServer(irc, modesource):
# Setting modes from a server.
irc.proto.modeServer(modesource, target, parsedmodes)
irc.callHooks([modesource, 'PYLINK_BOTSPLUGIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}])
else:
sourceuid = utils.nickToUid(irc, modesource)
irc.proto.modeClient(sourceuid, target, parsedmodes)
irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}])
# Setting modes from a client.
modesource = utils.nickToUid(irc, modesource)
irc.proto.modeClient(modesource, target, parsedmodes)
irc.callHooks([modesource, 'PYLINK_BOTSPLUGIN_MODE',
{'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}])
@utils.add_cmd
def msg(irc, source, args):

View File

@ -126,6 +126,52 @@ def showuser(irc, source, args):
(userobj.realhost, userobj.ip, userobj.away or '\x1D(not set)\x1D'))
f('\x02Channels\x02: %s' % (' '.join(userobj.channels).strip() or '\x1D(none)\x1D'))
@utils.add_cmd
def showchan(irc, source, args):
"""<channel>
Shows information about <channel>."""
try:
channel = utils.toLower(irc, args[0])
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 1: channel.")
return
if channel not in irc.channels:
irc.msg(source, 'Error: Unknown channel %r.' % channel)
return
f = lambda s: irc.msg(source, s)
c = irc.channels[channel]
# Only show verbose info if caller is oper or is in the target channel.
verbose = source in c.users or utils.isOper(irc, source)
secret = ('s', None) in c.modes
if secret and not verbose:
# Hide secret channels from normal users.
irc.msg(source, 'Error: Unknown channel %r.' % channel)
return
nicks = [irc.users[u].nick for u in c.users]
pmodes = ('owner', 'admin', 'op', 'halfop', 'voice')
f('Information on channel \x02%s\x02:' % channel)
f('\x02Channel topic\x02: %s' % c.topic)
f('\x02Channel creation time\x02: %s (%s)' % (ctime(c.ts), c.ts))
# Show only modes that aren't list-style modes.
modes = utils.joinModes([m for m in c.modes if m[0] not in irc.cmodes['*A']])
f('\x02Channel modes\x02: %s' % modes)
if verbose:
nicklist = []
# Iterate over the user list, sorted by nick.
for user, nick in sorted(zip(c.users, nicks),
key=lambda userpair: userpair[1].lower()):
prefixmodes = [irc.prefixmodes.get(irc.cmodes.get(pmode, ''), '')
for pmode in pmodes if user in c.prefixmodes[pmode+'s']]
nicklist.append(''.join(prefixmodes) + nick)
while nicklist[:20]: # 20 nicks per line to prevent message cutoff.
f('\x02User list\x02: %s' % ' '.join(nicklist[:20]))
nicklist = nicklist[20:]
@utils.add_cmd
def shutdown(irc, source, args):
"""takes no arguments.
@ -139,3 +185,11 @@ def shutdown(irc, source, args):
# Disable auto-connect first by setting the time to negative.
ircobj.serverdata['autoconnect'] = -1
ircobj.aborted.set()
@utils.add_cmd
def version(irc, source, args):
"""takes no arguments.
Returns the version of the currently running PyLink instance."""
irc.msg(source, "PyLink version \x02%s\x02, released under the Mozilla Public License version 2.0." % world.version)
irc.msg(source, "The source of this program is available at \x02%s\x02." % world.source)

View File

@ -19,3 +19,17 @@ def _exec(irc, source, args):
log.info('(%s) Executing %r for %s', irc.name, args, utils.getHostmask(irc, source))
exec(args, globals(), locals())
utils.add_cmd(_exec, 'exec')
def _eval(irc, source, args):
"""<Python expression>
Admin-only. Evaluates the given Python expression and returns the result.
\x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02"""
utils.checkAuthenticated(irc, source, allowOper=False)
args = ' '.join(args)
if not args.strip():
irc.msg(source, 'No code entered!')
return
log.info('(%s) Evaluating %r for %s', irc.name, args, utils.getHostmask(irc, source))
irc.msg(source, eval(args))
utils.add_cmd(_eval, 'eval')

File diff suppressed because it is too large Load Diff

View File

@ -24,9 +24,12 @@ class InspIRCdProtocol(TS6BaseProtocol):
self.hook_map = {'FJOIN': 'JOIN', 'RSQUIT': 'SQUIT', 'FMODE': 'MODE',
'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST',
'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME'}
self.sidgen = utils.TS6SIDGenerator(self.irc)
self.uidgen = {}
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):
"""Spawns a client with nick <nick> on the given IRC connection.
Note: No nick collision / valid nickname checks are done here; it is
@ -34,17 +37,15 @@ class InspIRCdProtocol(TS6BaseProtocol):
server = server or self.irc.sid
if not utils.isInternalServer(self.irc, server):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server)
# We need a separate UID generator instance for every PseudoServer
# we spawn. Otherwise, things won't wrap around properly.
if server not in self.irc.uidgen:
self.irc.uidgen[server] = utils.TS6UIDGenerator(server)
uid = self.irc.uidgen[server].next_uid()
# Create an UIDGenerator instance for every SID, so that each gets
# distinct values.
uid = self.uidgen.setdefault(server, utils.TS6UIDGenerator(server)).next_uid()
ts = ts or int(time.time())
realname = realname or self.irc.botdata['realname']
realhost = realhost or host
raw_modes = utils.joinModes(modes)
u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip)
realhost=realhost, ip=ip, manipulatable=manipulatable)
utils.applyModes(self.irc, uid, modes)
self.irc.servers[server].users.add(uid)
self._send(server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}"
@ -53,7 +54,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
modes=raw_modes, ip=ip, realname=realname,
realhost=realhost))
if ('o', None) in modes or ('+o', None) in modes:
self._operUp(uid, opertype=opertype or 'IRC_Operator')
self._operUp(uid, opertype=opertype or 'IRC Operator')
return u
def joinClient(self, client, channel):
@ -142,26 +143,26 @@ class InspIRCdProtocol(TS6BaseProtocol):
and the change will be reflected here."""
userobj = self.irc.users[target]
try:
otype = opertype or userobj.opertype
otype = opertype or userobj.opertype or 'IRC Operator'
except AttributeError:
log.debug('(%s) opertype field for %s (%s) isn\'t filled yet!',
self.irc.name, target, userobj.nick)
# whatever, this is non-standard anyways.
otype = 'IRC_Operator'
otype = 'IRC Operator'
assert otype, "Tried to send an empty OPERTYPE!"
log.debug('(%s) Sending OPERTYPE from %s to oper them up.',
self.irc.name, target)
userobj.opertype = otype
self._send(target, 'OPERTYPE %s' % otype)
self._send(target, 'OPERTYPE %s' % otype.replace(" ", "_"))
def _sendModes(self, numeric, target, modes, ts=None):
"""Internal function to send mode changes from a PyLink client/server."""
# -> :9PYAAAAAA FMODE #pylink 1433653951 +os 9PYAAAAAA
# -> :9PYAAAAAA MODE 9PYAAAAAA -i+w
log.debug('(%s) inspself.ircd._sendModes: received %r for mode list', self.irc.name, modes)
log.debug('(%s) inspircd._sendModes: received %r for mode list', self.irc.name, modes)
if ('+o', None) in modes and not utils.isChannel(target):
# https://github.com/inspself.ircd/inspself.ircd/blob/master/src/modules/m_spanningtree/opertype.cpp#L26-L28
# Servers need a special command to set umode +o on people.
# Why isn't this documented anywhere, InspIRCd?
self._operUp(target)
utils.applyModes(self.irc, target, modes)
joinedmodes = utils.joinModes(modes)
@ -272,8 +273,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
# "desc" defaults to the configured server description.
desc = desc or self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc']
if sid is None: # No sid given; generate one!
self.irc.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"])
sid = self.irc.sidgen.next_sid()
sid = self.sidgen.next_sid()
assert len(sid) == 3, "Incorrect SID length"
if sid in self.irc.servers:
raise ValueError('A server with SID %r already exists!' % sid)
@ -285,7 +285,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
if not utils.isServerName(name):
raise ValueError('Invalid server name %r' % name)
self._send(uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc))
self.irc.servers[sid] = IrcServer(uplink, name, internal=True)
self.irc.servers[sid] = IrcServer(uplink, name, internal=True, desc=desc)
self._send(sid, 'ENDBURST')
return sid
@ -329,7 +329,8 @@ class InspIRCdProtocol(TS6BaseProtocol):
if args[2] != self.irc.serverdata['recvpass']:
# Check if recvpass is correct
raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername)
self.irc.servers[numeric] = IrcServer(None, servername)
sdesc = ' '.join(args).split(':', 1)[1]
self.irc.servers[numeric] = IrcServer(None, servername, desc=sdesc)
self.irc.uplink = numeric
return
elif args[0] == 'CAPAB':
@ -459,18 +460,20 @@ class InspIRCdProtocol(TS6BaseProtocol):
servername = args[0].lower()
sid = args[3]
sdesc = args[-1]
self.irc.servers[sid] = IrcServer(numeric, servername)
self.irc.servers[sid] = IrcServer(numeric, servername, desc=sdesc)
return {'name': servername, 'sid': args[3], 'text': sdesc}
def handle_fmode(self, numeric, command, args):
"""Handles the FMODE command, used for channel mode changes."""
# <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD
channel = utils.toLower(self.irc, args[0])
oldobj = self.irc.channels[channel].deepcopy()
modes = args[2:]
changedmodes = utils.parseModes(self.irc, channel, modes)
utils.applyModes(self.irc, channel, changedmodes)
ts = int(args[1])
return {'target': channel, 'modes': changedmodes, 'ts': ts}
return {'target': channel, 'modes': changedmodes, 'ts': ts,
'oldchan': oldobj}
def handle_mode(self, numeric, command, args):
"""Handles incoming user mode changes."""
@ -543,7 +546,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
# command sent for it.
# <- :70MAAAAAB OPERTYPE Network_Owner
omode = [('+o', None)]
self.irc.users[numeric].opertype = opertype = args[0]
self.irc.users[numeric].opertype = opertype = args[0].replace("_", " ")
utils.applyModes(self.irc, numeric, omode)
# OPERTYPE is essentially umode +o and metadata in one command;
# we'll call that too.

View File

@ -17,9 +17,12 @@ class TS6Protocol(TS6BaseProtocol):
super(TS6Protocol, self).__init__(irc)
self.casemapping = 'rfc1459'
self.hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE'}
self.sidgen = utils.TS6SIDGenerator(self.irc)
self.uidgen = {}
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):
"""Spawns a client with nick <nick> on the given IRC connection.
Note: No nick collision / valid nickname checks are done here; it is
@ -27,11 +30,9 @@ class TS6Protocol(TS6BaseProtocol):
server = server or self.irc.sid
if not utils.isInternalServer(self.irc, server):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server)
# We need a separate UID generator instance for every PseudoServer
# we spawn. Otherwise, things won't wrap around properly.
if server not in self.irc.uidgen:
self.irc.uidgen[server] = utils.TS6UIDGenerator(server)
uid = self.irc.uidgen[server].next_uid()
# Create an UIDGenerator instance for every SID, so that each gets
# distinct values.
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
@ -40,7 +41,7 @@ class TS6Protocol(TS6BaseProtocol):
realhost = realhost or host
raw_modes = utils.joinModes(modes)
u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip)
realhost=realhost, ip=ip, manipulatable=manipulatable)
utils.applyModes(self.irc, uid, modes)
self.irc.servers[server].users.add(uid)
self._send(server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
@ -99,7 +100,7 @@ class TS6Protocol(TS6BaseProtocol):
self.irc.channels[channel].modes.clear()
for p in self.irc.channels[channel].prefixmodes.values():
p.clear()
log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, self.irc.name, ts,
log.debug("(%s) sending SJOIN to %s with ts %s (that's %r)", self.irc.name, channel, ts,
time.strftime("%c", time.localtime(ts)))
modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']]
changedmodes = []
@ -135,6 +136,7 @@ class TS6Protocol(TS6BaseProtocol):
def _sendModes(self, numeric, target, modes, ts=None):
"""Internal function to send mode changes from a PyLink client/server."""
utils.applyModes(self.irc, target, modes)
modes = list(modes)
if utils.isChannel(target):
ts = ts or self.irc.channels[utils.toLower(self.irc, target)].ts
# TMODE:
@ -263,8 +265,7 @@ class TS6Protocol(TS6BaseProtocol):
name = name.lower()
desc = desc or self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc']
if sid is None: # No sid given; generate one!
self.irc.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"])
sid = self.irc.sidgen.next_sid()
sid = self.sidgen.next_sid()
assert len(sid) == 3, "Incorrect SID length"
if sid in self.irc.servers:
raise ValueError('A server with SID %r already exists!' % sid)
@ -276,7 +277,7 @@ class TS6Protocol(TS6BaseProtocol):
if not utils.isServerName(name):
raise ValueError('Invalid server name %r' % name)
self._send(uplink, 'SID %s 1 %s :%s' % (name, sid, desc))
self.irc.servers[sid] = IrcServer(uplink, name, internal=True)
self.irc.servers[sid] = IrcServer(uplink, name, internal=True, desc=desc)
return sid
def squitServer(self, source, target, text='No reason given'):
@ -407,6 +408,7 @@ class TS6Protocol(TS6BaseProtocol):
sname = args[1].lower()
log.debug('(%s) Found uplink server name as %r', self.irc.name, sname)
self.irc.servers[self.irc.uplink].name = sname
self.irc.servers[self.irc.uplink].desc = ' '.join(args).split(':', 1)[1]
# According to the TS6 protocol documentation, we should send SVINFO
# when we get our uplink's SERVER command.
self.irc.send('SVINFO 6 6 0 :%s' % int(time.time()))
@ -593,7 +595,7 @@ class TS6Protocol(TS6BaseProtocol):
# XXX: don't just save these by their server names; that's ugly!
sid = servername
sdesc = args[-1]
self.irc.servers[sid] = IrcServer(numeric, servername)
self.irc.servers[sid] = IrcServer(numeric, servername, desc=sdesc)
return {'name': servername, 'sid': sid, 'text': sdesc}
handle_sid = handle_server
@ -602,11 +604,13 @@ class TS6Protocol(TS6BaseProtocol):
"""Handles incoming TMODE commands (channel mode change)."""
# <- :42XAAAAAB TMODE 1437450768 #endlessvoid -c+lkC 3 agte4
channel = utils.toLower(self.irc, args[1])
oldobj = self.irc.channels[channel].deepcopy()
modes = args[2:]
changedmodes = utils.parseModes(self.irc, channel, modes)
utils.applyModes(self.irc, channel, changedmodes)
ts = int(args[0])
return {'target': channel, 'modes': changedmodes, 'ts': ts}
return {'target': channel, 'modes': changedmodes, 'ts': ts,
'oldchan': oldobj}
def handle_mode(self, numeric, command, args):
"""Handles incoming user mode changes."""

View File

@ -81,11 +81,12 @@ class TS6BaseProtocol(Protocol):
# Clear empty non-permanent channels.
if not (self.irc.channels[c].users or ((self.irc.cmodes.get('permanent'), None) in self.irc.channels[c].modes)):
del self.irc.channels[c]
assert numeric not in v.users, "IrcChannel's removeuser() is broken!"
sid = numeric[:3]
log.debug('Removing client %s from self.irc.users', numeric)
del self.irc.users[numeric]
log.debug('Removing client %s from self.irc.servers[%s]', numeric, sid)
log.debug('Removing client %s from self.irc.servers[%s].users', numeric, sid)
self.irc.servers[sid].users.discard(numeric)
def partClient(self, client, channel, reason=None):
@ -200,6 +201,7 @@ class TS6BaseProtocol(Protocol):
split_server = args[0]
affected_users = []
log.info('(%s) Netsplit on server %s', self.irc.name, split_server)
assert split_server in self.irc.servers, "Tried to split a server (%s) that didn't exist!" % split_server
# Prevent RuntimeError: dictionary changed size during iteration
old_servers = self.irc.servers.copy()
for sid, data in old_servers.items():
@ -211,9 +213,10 @@ class TS6BaseProtocol(Protocol):
affected_users.append(user)
log.debug('Removing client %s (%s)', user, self.irc.users[user].nick)
self.removeClient(user)
sname = self.irc.servers[split_server].name
del self.irc.servers[split_server]
log.debug('(%s) Netsplit affected users: %s', self.irc.name, affected_users)
return {'target': split_server, 'users': affected_users}
return {'target': split_server, 'users': affected_users, 'name': sname}
def handle_topic(self, numeric, command, args):
"""Handles incoming TOPIC changes from clients. For topic bursts,
@ -222,9 +225,11 @@ class TS6BaseProtocol(Protocol):
channel = utils.toLower(self.irc, args[0])
topic = args[1]
ts = int(time.time())
oldtopic = self.irc.channels[channel].topic
self.irc.channels[channel].topic = topic
self.irc.channels[channel].topicset = True
return {'channel': channel, 'setter': numeric, 'ts': ts, 'topic': topic}
return {'channel': channel, 'setter': numeric, 'ts': ts, 'topic': topic,
'oldtopic': oldtopic}
def handle_part(self, source, command, args):
"""Handles incoming PART commands."""

View File

@ -18,7 +18,7 @@ class UnrealProtocol(TS6BaseProtocol):
self.casemapping = 'ascii'
self.proto_ver = 2351
self.hook_map = {}
self.uidgen = {}
self.caps = {}
self._unrealCmodes = {'l': 'limit', 'c': 'blockcolor', 'G': 'censor',
@ -40,9 +40,7 @@ class UnrealProtocol(TS6BaseProtocol):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server)
# Unreal 3.4 uses TS6-style UIDs. They don't start from AAAAAA like other IRCd's
# do, but we can do that fine...
if server not in self.irc.uidgen:
self.irc.uidgen[server] = utils.TS6UIDGenerator(server)
uid = self.irc.uidgen[server].next_uid()
uid = self.uidgen.setdefault(server, utils.TS6UIDGenerator(server)).next_uid()
ts = ts or int(time.time())
realname = realname or self.irc.botdata['realname']
realhost = realhost or host

4
pylink
View File

@ -5,6 +5,7 @@ import os
import sys
# This must be done before conf imports, so we get the real conf instead of testing one.
os.chdir(os.path.dirname(__file__))
import world
world.testing = False
@ -14,7 +15,7 @@ import classes
import coreplugin
if __name__ == '__main__':
log.info('PyLink starting...')
log.info('PyLink %s starting...', world.version)
if conf.conf['login']['password'] == 'changeme':
log.critical("You have not set the login details correctly! Exiting...")
sys.exit(2)
@ -61,4 +62,3 @@ if __name__ == '__main__':
world.networkobjects[network] = classes.Irc(network, proto)
world.started.set()
log.info("loaded plugins: %s", world.plugins)

View File

@ -11,7 +11,10 @@ suites = []
# Yay, import hacks!
sys.path.append(os.path.join(os.getcwd(), 'tests'))
for testfile in glob.glob('tests/test_*.py'):
query = sys.argv[1:] or glob.glob('tests/test_*.py')
for testfile in query:
# Strip the tests/ and .py extension: tests/test_whatever.py => test_whatever
module = testfile.replace('.py', '').replace('tests/', '')
module = __import__(module)

View File

@ -139,25 +139,12 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase):
# Default channels start with +nt
self.irc.run(':70M FMODE #pylink 1423790412 -nt')
self.assertEqual(set(), self.irc.channels['#pylink'].modes)
self.irc.takeHooks()
self.irc.run(':70M FMODE #pylink 1423790412 +ikl herebedragons 100')
self.assertEqual({('i', None), ('k', 'herebedragons'), ('l', '100')}, self.irc.channels['#pylink'].modes)
self.irc.run(':70M FMODE #pylink 1423790413 -ilk+m herebedragons')
self.assertEqual({('m', None)}, self.irc.channels['#pylink'].modes)
hookdata = self.irc.takeHooks()
expected = [['70M', 'FMODE', {'target': '#pylink', 'modes':
[('+i', None), ('+k', 'herebedragons'),
('+l', '100')], 'ts': 1423790412}
],
['70M', 'FMODE', {'target': '#pylink', 'modes':
[('-i', None), ('-l', None),
('-k', 'herebedragons'), ('+m', None)],
'ts': 1423790413}]
]
self.assertEqual(expected, hookdata)
def testHandleFModeWithPrefixes(self):
self.irc.run(':70M FJOIN #pylink 123 +n :o,10XAAAAAA ,10XAAAAAB')
# Prefix modes are stored separately, so they should never show up in .modes
@ -170,13 +157,6 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase):
self.irc.run(':70M FMODE #pylink 123 -o %s' % self.u)
self.assertEqual(modes, self.irc.channels['#pylink'].modes)
self.assertNotIn(self.u, self.irc.channels['#pylink'].prefixmodes['ops'])
# Test hooks
hookdata = self.irc.takeHooks()
expected = [['70M', 'FJOIN', {'channel': '#pylink', 'ts': 123, 'modes': [('+n', None)],
'users': ['10XAAAAAA', '10XAAAAAB']}],
['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '50'), ('+o', '9PYAAAAAA'), ('+t', None)], 'ts': 123}],
['70M', 'FMODE', {'target': '#pylink', 'modes': [('-o', '9PYAAAAAA')], 'ts': 123}]]
self.assertEqual(expected, hookdata)
def testHandleFModeRemovesOldParams(self):
self.irc.run(':70M FMODE #pylink 1423790412 +l 50')
@ -184,10 +164,6 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase):
self.irc.run(':70M FMODE #pylink 1423790412 +l 30')
self.assertIn(('l', '30'), self.irc.channels['#pylink'].modes)
self.assertNotIn(('l', '50'), self.irc.channels['#pylink'].modes)
hookdata = self.irc.takeHooks()
expected = [['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '50')], 'ts': 1423790412}],
['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '30')], 'ts': 1423790412}]]
self.assertEqual(expected, hookdata)
def testHandleFJoinResetsTS(self):
curr_ts = self.irc.channels['#pylink'].ts
@ -238,24 +214,24 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase):
self.assertIn('00C', self.irc.servers)
def testHandleNick(self):
self.irc.run(':9PYAAAAAA NICK PyLink-devel 1434744242')
self.irc.run(':%s NICK PyLink-devel 1434744242' % self.u)
hookdata = self.irc.takeHooks()[0][-1]
expected = {'newnick': 'PyLink-devel', 'oldnick': 'PyLink', 'ts': 1434744242}
self.assertEqual(hookdata, expected)
self.assertEqual('PyLink-devel', self.irc.users['9PYAAAAAA'].nick)
self.assertEqual('PyLink-devel', self.irc.users[self.u].nick)
def testHandleSave(self):
self.irc.run(':9PYAAAAAA NICK Derp_ 1433728673')
self.irc.run(':70M SAVE 9PYAAAAAA 1433728673')
self.irc.run(':%s NICK Derp_ 1433728673' % self.u)
self.irc.run(':70M SAVE %s 1433728673' % self.u)
hookdata = self.irc.takeHooks()[-1][-1]
self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'ts': 1433728673, 'oldnick': 'Derp_'})
self.assertEqual('9PYAAAAAA', self.irc.users['9PYAAAAAA'].nick)
self.assertEqual(hookdata, {'target': self.u, 'ts': 1433728673, 'oldnick': 'Derp_'})
self.assertEqual(self.u, self.irc.users[self.u].nick)
def testHandleInvite(self):
self.irc.run(':10XAAAAAA INVITE 9PYAAAAAA #blah 0')
self.irc.run(':10XAAAAAA INVITE %s #blah 0' % self.u)
hookdata = self.irc.takeHooks()[-1][-1]
del hookdata['ts']
self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'channel': '#blah'})
self.assertEqual(hookdata, {'target': self.u, 'channel': '#blah'})
def testHandleOpertype(self):
self.irc.run('SERVER whatever. abcd 0 10X :Whatever Server - Hellas Planitia, Mars')

View File

@ -101,19 +101,50 @@ class TestUtils(unittest.TestCase):
('+b', '*!*@*.badisp.net')])
self.assertEqual(res, '-o+l-nm+kb 9PYAAAAAA 50 hello *!*@*.badisp.net')
@unittest.skip('Wait, we need to work out the kinks first! (reversing changes of modes with arguments)')
def _reverseModes(self, query, expected, target='#test'):
res = utils.reverseModes(self.irc, target, query)
self.assertEqual(res, expected)
def testReverseModes(self):
f = lambda x: utils.reverseModes(self.irc, '#test', x)
test = lambda x, y: self.assertEqual(utils.reverseModes(self.irc, '#test', x), y)
# Strings.
self.assertEqual(f("+nt-lk"), "-nt+lk")
self.assertEqual(f("nt-k"), "-nt+k")
self._reverseModes("+mk-t test", "-mk+t test")
self._reverseModes("ml-n 111", "-ml+n")
# Lists.
self.assertEqual(f([('+m', None), ('+t', None), ('+l', '3'), ('-o', 'person')]),
[('-m', None), ('-t', None), ('-l', '3'), ('+o', 'person')])
self._reverseModes([('+m', None), ('+r', None), ('+l', '3')],
{('-m', None), ('-r', None), ('-l', None)})
# Sets.
self.assertEqual(f({('s', None), ('+o', 'whoever')}), {('-s', None), ('-o', 'whoever')})
self._reverseModes({('s', None)}, {('-s', None)})
# Combining modes with an initial + and those without
self.assertEqual(f({('s', None), ('+n', None)}), {('-s', None), ('-n', None)})
self._reverseModes({('s', None), ('+R', None)}, {('-s', None), ('-R', None)})
def testReverseModesUser(self):
self._reverseModes({('+i', None), ('l', 'asfasd')}, {('-i', None), ('-l', 'asfasd')},
target=self.irc.pseudoclient.uid)
def testReverseModesExisting(self):
utils.applyModes(self.irc, '#test', [('+m', None), ('+l', '50'), ('+k', 'supersecret'),
('+o', '9PYAAAAAA')])
self._reverseModes({('+i', None), ('+l', '3')}, {('-i', None), ('+l', '50')})
self._reverseModes('-n', '+n')
self._reverseModes('-l', '+l 50')
self._reverseModes('+k derp', '+k supersecret')
self._reverseModes('-mk *', '+mk supersecret')
# Existing modes are ignored.
self._reverseModes([('+t', None)], set())
self._reverseModes('+n', '+')
self._reverseModes('+oo GLolol 9PYAAAAAA', '-o GLolol')
self._reverseModes('+o 9PYAAAAAA', '+')
self._reverseModes('+vvvvM test abcde atat abcd', '-vvvvM test abcde atat abcd')
# Ignore unsetting prefixmodes/list modes that were never set.
self._reverseModes([('-v', '10XAAAAAA')], set())
self._reverseModes('-ob 10XAAAAAA derp!*@*', '+')
utils.applyModes(self.irc, '#test', [('+o', 'GLolol'), ('+b', '*!user@badisp.tk')])
self._reverseModes('-voo GLolol GLolol 10XAAAAAA', '+o GLolol')
self._reverseModes('-bb *!*@* *!user@badisp.tk', '+b *!user@badisp.tk')
if __name__ == '__main__':
unittest.main()

212
utils.py
View File

@ -40,10 +40,9 @@ class TS6UIDGenerator():
return uid
class TS6SIDGenerator():
"""<query>
"""
TS6 SID Generator. <query> is a 3 character string with any combination of
uppercase letters, digits, and #'s. <query> must contain at least one #,
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.
@ -57,8 +56,12 @@ class TS6SIDGenerator():
"6##" would give: 600, 601, 602, ... 60Y, 60Z, 610, 611, ... 6ZZ (1296 total results)
"""
def __init__(self, query):
self.query = list(query)
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 = {}
@ -94,8 +97,9 @@ class TS6SIDGenerator():
self.increment(pos-1)
def next_sid(self):
while ''.join(self.output) in self.irc.servers:
self.increment()
sid = ''.join(self.output)
self.increment()
return sid
def add_cmd(func, name=None):
@ -110,10 +114,8 @@ def add_hook(func, command):
world.command_hooks[command].append(func)
def toLower(irc, text):
"""<irc object> <text>
Returns a lowercase representation of <text> based on <irc object>'s
casemapping (rfc1459 vs ascii)."""
"""Returns a lowercase representation of text based on the IRC object's
casemapping (rfc1459 or ascii)."""
if irc.proto.casemapping == 'rfc1459':
text = text.replace('{', '[')
text = text.replace('}', ']')
@ -122,39 +124,42 @@ def toLower(irc, text):
return text.lower()
def nickToUid(irc, nick):
"""<irc object> <nick>
Returns the UID of a user named <nick>, if present."""
"""Returns the UID of a user named nick, if present."""
nick = toLower(irc, nick)
for k, v in irc.users.items():
if toLower(irc, v.nick) == nick:
return k
def clientToServer(irc, numeric):
"""<irc object> <numeric>
Finds the server SID of user <numeric> and returns it."""
"""Finds the SID of the server a user is on."""
for server in irc.servers:
if numeric in irc.servers[server].users:
return server
# A+ regex
_nickregex = r'^[A-Za-z\|\\_\[\]\{\}\^\`][A-Z0-9a-z\-\|\\_\[\]\{\}\^\`]*$'
def isNick(s, nicklen=None):
"""Checks whether the string given is a valid nick."""
if nicklen and len(s) > nicklen:
return False
return bool(re.match(_nickregex, s))
def isChannel(s):
return s.startswith('#')
"""Checks whether the string given is a valid channel name."""
return str(s).startswith('#')
def _isASCII(s):
chars = string.ascii_letters + string.digits + string.punctuation
return all(char in chars for char in s)
def isServerName(s):
"""Checks whether the string given is a server name."""
return _isASCII(s) and '.' in s and not s.startswith('.')
hostmaskRe = re.compile(r'^\S+!\S+@\S+$')
def isHostmask(text):
"""Returns whether the given text is a valid hostmask."""
return bool(hostmaskRe.match(text))
def parseModes(irc, 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')]
@ -164,11 +169,10 @@ def parseModes(irc, 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]
if not modestring:
return ValueError('No modes supplied in parseModes query: %r' % modes)
args = args[1:]
if usermodes:
log.debug('(%s) Using irc.umodes for this query: %s', irc.name, irc.umodes)
@ -207,6 +211,13 @@ def parseModes(irc, target, args):
# 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 = nickToUid(irc, 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!')
continue
elif prefix == '+' and mode in supported_modes['*C']:
# Only has parameter when setting.
log.debug('Mode %s: Only has parameter when setting.', mode)
@ -220,10 +231,9 @@ def parseModes(irc, target, args):
return res
def applyModes(irc, target, changedmodes):
"""<target> <changedmodes>
"""Takes a list of parsed IRC modes, and applies them on the given target.
Takes a list of parsed IRC modes (<changedmodes>, in the format of parseModes()), and applies them on <target>.
<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)
if usermodes:
@ -292,11 +302,10 @@ def applyModes(irc, target, changedmodes):
irc.channels[target].modes = modelist
def joinModes(modes):
"""<mode list>
"""Takes a list of (mode, arg) tuples in parseModes() format, and
joins them into a string.
Takes a list of (mode, arg) tuples in parseModes() format, and
joins them into a string. See testJoinModes in tests/test_utils.py
for some examples."""
See testJoinModes in tests/test_utils.py for some examples."""
prefix = '+' # Assume we're adding modes unless told otherwise
modelist = ''
args = []
@ -328,55 +337,112 @@ def joinModes(modes):
modelist += ' %s' % ' '.join(args)
return modelist
def reverseModes(irc, target, modes):
"""<mode string/mode list>
def _flip(mode):
"""Flips a mode character."""
# Make it a list first, strings don't support item assignment
mode = list(mode)
if mode[0] == '-': # Query is something like "-n"
mode[0] = '+' # Change it to "+n"
elif mode[0] == '+':
mode[0] = '-'
else: # No prefix given, assume +
mode.insert(0, '-')
return ''.join(mode)
Reverses/Inverts the mode string or mode list given.
def reverseModes(irc, target, modes, oldobj=None):
"""Reverses/Inverts the mode string or mode list given.
"+nt-lk" => "-nt+lk"
"nt-k" => "-nt+k"
[('+m', None), ('+t', None), ('+l', '3'), ('-o', 'person')] =>
[('-m', None), ('-t', None), ('-l', '3'), ('+o', 'person')]
[('s', None), ('+n', None)] => [('-s', None), ('-n', None)]
Optionally, an oldobj argument can be given to look at an earlier state of
a channel/user object, e.g. for checking the op status of a mode setter
before their modes are processed and added to the channel state.
This function allows both mode strings or mode lists. Example uses:
"+mi-lk test => "-mi+lk test"
"mi-k test => "-mi+k test"
[('+m', None), ('+r', None), ('+l', '3'), ('-o', 'person')
=> {('-m', None), ('-r', None), ('-l', None), ('+o', 'person')})
{('s', None), ('+o', 'whoever') => {('-s', None), ('-o', 'whoever')})
"""
origtype = type(modes)
# Operate on joined modestrings only; it's easier.
if origtype != str:
modes = joinModes(modes)
# Swap the +'s and -'s by replacing one with a dummy character, and then changing it back.
assert '\x00' not in modes, 'NUL cannot be in the mode list (it is a reserved character)!'
if not modes.startswith(('+', '-')):
modes = '+' + modes
newmodes = modes.replace('+', '\x00')
newmodes = newmodes.replace('-', '+')
newmodes = newmodes.replace('\x00', '-')
if origtype != str:
# If the original query isn't a string, send back the parseModes() output.
return parseModes(irc, target, newmodes.split(" "))
# If the query is a string, we have to parse it first.
if origtype == str:
modes = parseModes(irc, target, modes.split(" "))
# Get the current mode list first.
if isChannel(target):
c = oldobj or irc.channels[target]
oldmodes = c.modes.copy()
possible_modes = irc.cmodes.copy()
# For channels, this also includes the list of prefix modes.
possible_modes['*A'] += ''.join(irc.prefixmodes)
for name, userlist in c.prefixmodes.items():
try:
oldmodes.update([(irc.cmodes[name[:-1]], u) for u in userlist])
except KeyError:
continue
else:
return newmodes
oldmodes = irc.users[target].modes
possible_modes = irc.umodes
newmodes = []
log.debug('(%s) reverseModes: old/current mode list for %s is: %s', irc.name,
target, oldmodes)
for char, arg in modes:
# Mode types:
# 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.
mchar = char[-1]
if mchar in possible_modes['*B'] + possible_modes['*C']:
# We need to find the current mode list, so we can reset arguments
# for modes that have arguments. For example, setting +l 30 on a channel
# that had +l 50 set should give "+l 30", not "-l".
oldarg = [m for m in oldmodes if m[0] == mchar]
if oldarg: # Old mode argument for this mode existed, use that.
oldarg = oldarg[0]
mpair = ('+%s' % oldarg[0], oldarg[1])
else: # Not found, flip the mode then.
# Mode takes no arguments when unsetting.
if mchar in possible_modes['*C'] and char[0] != '-':
arg = None
mpair = (_flip(char), arg)
else:
mpair = (_flip(char), arg)
if char[0] != '-' and (mchar, arg) in oldmodes:
# Mode is already set.
log.debug("(%s) reverseModes: skipping reversing '%s %s' with %s since we're "
"setting a mode that's already set.", irc.name, char, arg, mpair)
continue
elif char[0] == '-' and (mchar, arg) not in oldmodes and mchar in possible_modes['*A']:
# We're unsetting a prefixmode that was never set - don't set it in response!
# Charybdis lacks verification for this server-side.
log.debug("(%s) reverseModes: skipping reversing '%s %s' with %s since it "
"wasn't previously set.", irc.name, char, arg, mpair)
continue
newmodes.append(mpair)
log.debug('(%s) reverseModes: new modes: %s', irc.name, newmodes)
if origtype == str:
# If the original query is a string, send it back as a string.
return joinModes(newmodes)
else:
return set(newmodes)
def isInternalClient(irc, numeric):
"""<irc object> <client numeric>
Checks whether <client numeric> is a PyLink PseudoClient,
returning the SID of the PseudoClient's server if True.
"""
Checks whether the given numeric is a PyLink Client,
returning the SID of the server it's on if so.
"""
for sid in irc.servers:
if irc.servers[sid].internal and numeric in irc.servers[sid].users:
return sid
def isInternalServer(irc, sid):
"""<irc object> <sid>
Returns whether <sid> is an internal PyLink PseudoServer.
"""
"""Returns whether the given SID is an internal PyLink server."""
return (sid in irc.servers and irc.servers[sid].internal)
def isOper(irc, uid, allowAuthed=True, allowOper=True):
"""<irc object> <UID>
Returns whether <UID> has operator status on PyLink. This can be achieved
"""
Returns whether the given user has operator status on PyLink. This can be achieved
by either identifying to PyLink as admin (if allowAuthed is True),
or having user mode +o set (if allowOper is True). At least one of
allowAuthed or allowOper must be True for this to give any meaningful
@ -390,10 +456,10 @@ def isOper(irc, uid, allowAuthed=True, allowOper=True):
return False
def checkAuthenticated(irc, uid, allowAuthed=True, allowOper=True):
"""<irc object> <UID>
Checks whether user <UID> has operator status on PyLink, raising
NotAuthenticatedError and logging the access denial if not."""
"""
Checks whetherthe given user has operator status on PyLink, raising
NotAuthenticatedError and logging the access denial if not.
"""
lastfunc = inspect.stack()[1][3]
if not isOper(irc, uid, allowAuthed=allowAuthed, allowOper=allowOper):
log.warning('(%s) Access denied for %s calling %r', irc.name,
@ -401,10 +467,17 @@ def checkAuthenticated(irc, uid, allowAuthed=True, allowOper=True):
raise NotAuthenticatedError("You are not authenticated!")
return True
def getHostmask(irc, user):
"""<irc object> <UID>
def isManipulatableClient(irc, uid):
"""
Returns whether the given user is marked as an internal, manipulatable
client. Usually, automatically spawned services clients should have this
set True to prevent interactions with opers (like mode changes) from
causing desyncs.
"""
return isInternalClient(irc, uid) and irc.users[uid].manipulatable
Gets the hostmask of user <UID>, if present."""
def getHostmask(irc, user):
"""Gets the hostmask of the given user, if present."""
userobj = irc.users.get(user)
if userobj is None:
return '<user object not found>'
@ -421,8 +494,3 @@ def getHostmask(irc, user):
except AttributeError:
host = '<unknown host>'
return '%s!%s@%s' % (nick, ident, host)
hostmaskRe = re.compile(r'^\S+!\S+@\S+$')
def isHostmask(text):
"""Returns whether <text> is a valid hostmask."""
return bool(hostmaskRe.match(text))

View File

@ -2,6 +2,7 @@
from collections import defaultdict
import threading
import subprocess
# Global variable to indicate whether we're being ran directly, or imported
# for a testcase.
@ -16,3 +17,14 @@ schedulers = {}
plugins = []
whois_handlers = []
started = threading.Event()
version = "<unknown>"
source = "https://github.com/GLolol/PyLink" # CHANGE THIS IF YOU'RE FORKING!!
# Only run this once.
if version == "<unknown>":
# Get version from Git tags.
try:
version = 'v' + subprocess.check_output(['git', 'describe', '--tags']).decode('utf-8').strip()
except Exception as e:
print('ERROR: Failed to get version from "git describe --tags": %s: %s' % (type(e).__name__, e))