3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-12-25 20:22:45 +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
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 ## 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! **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 ### Dependencies
@ -22,10 +22,10 @@ Dependencies currently include:
* charybdis (3.5.x / git master) - module `ts6` * charybdis (3.5.x / git master) - module `ts6`
* Elemental-IRCd (6.6.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??? 3) Profit???

View File

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

View File

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

View File

@ -20,6 +20,22 @@ login:
user: admin user: admin
password: changeme 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: servers:
yournet: yournet:
# Server IP, port, and passwords # Server IP, port, and passwords
@ -28,6 +44,9 @@ servers:
recvpass: "abcd" recvpass: "abcd"
sendpass: "abcd" sendpass: "abcd"
# The full network name, used by plugins.
netname: "yournet"
# Hostname we will use to connect to the remote server # Hostname we will use to connect to the remote server
hostname: "pylink.yournet" hostname: "pylink.yournet"
@ -81,6 +100,7 @@ servers:
sendpass: "abcd" sendpass: "abcd"
hostname: "pylink.example.com" hostname: "pylink.example.com"
sid: "8PY" sid: "8PY"
netname: "some network"
# Leave this as an empty list if you don't want to join any channels. # Leave this as an empty list if you don't want to join any channels.
channels: [] channels: []

View File

@ -4,22 +4,22 @@ import utils
from log import log from log import log
import world import world
# Handle KILLs sent to the PyLink client and respawn
def handle_kill(irc, source, command, args): def handle_kill(irc, source, command, args):
"""Handle KILLs to the main PyLink client, respawning it as needed."""
if args['target'] == irc.pseudoclient.uid: if args['target'] == irc.pseudoclient.uid:
irc.spawnMain() irc.spawnMain()
utils.add_hook(handle_kill, 'KILL') utils.add_hook(handle_kill, 'KILL')
# Handle KICKs to the PyLink client, rejoining the channels
def handle_kick(irc, source, command, args): def handle_kick(irc, source, command, args):
"""Handle KICKs to the main PyLink client, rejoining channels as needed."""
kicked = args['target'] kicked = args['target']
channel = args['channel'] channel = args['channel']
if kicked == irc.pseudoclient.uid: if kicked == irc.pseudoclient.uid:
irc.proto.joinClient(irc.pseudoclient.uid, channel) irc.proto.joinClient(irc.pseudoclient.uid, channel)
utils.add_hook(handle_kick, 'KICK') utils.add_hook(handle_kick, 'KICK')
# Handle commands sent to the PyLink client (PRIVMSG)
def handle_commands(irc, source, command, args): def handle_commands(irc, source, command, args):
"""Handle commands sent to the PyLink client (PRIVMSG)."""
if args['target'] == irc.pseudoclient.uid: if args['target'] == irc.pseudoclient.uid:
text = args['text'].strip() text = args['text'].strip()
cmd_args = text.split(' ') 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))) irc.msg(source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
utils.add_hook(handle_commands, 'PRIVMSG') 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): def handle_whois(irc, source, command, args):
"""Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd)."""
target = args['target'] target = args['target']
user = irc.users.get(target) user = irc.users.get(target)
if user is None: 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 f = irc.proto.numericServer
server = utils.clientToServer(irc, target) or irc.sid server = utils.clientToServer(irc, target) or irc.sid
nick = user.nick nick = user.nick
@ -72,12 +74,12 @@ def handle_whois(irc, source, command, args):
f(server, 319, source, '%s :%s' % (nick, ' '.join(public_chans))) f(server, 319, source, '%s :%s' % (nick, ' '.join(public_chans)))
# 312: sends the server the target is on, and its server description. # 312: sends the server the target is on, and its server description.
f(server, 312, source, "%s %s :%s" % (nick, irc.servers[server].name, 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, # 313: sends a string denoting the target's operator privilege,
# only if they have umode +o. # only if they have umode +o.
if ('o', None) in user.modes: if ('o', None) in user.modes:
if hasattr(user, 'opertype'): if hasattr(user, 'opertype'):
opertype = user.opertype.replace("_", " ") opertype = user.opertype
else: else:
opertype = "IRC Operator" opertype = "IRC Operator"
# Let's be gramatically correct. # Let's be gramatically correct.
@ -86,6 +88,7 @@ def handle_whois(irc, source, command, args):
# 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd. # 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd.
# Only show this to opers! # Only show this to opers!
if sourceisOper: 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))) 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 # 317: shows idle and signon time. However, we don't track the user's real
# idle time, so we simply return 0. # idle time, so we simply return 0.
@ -106,3 +109,14 @@ def handle_whois(irc, source, command, args):
# 318: End of WHOIS. # 318: End of WHOIS.
f(server, 318, source, "%s :End of /WHOIS list" % nick) f(server, 318, source, "%s :End of /WHOIS list" % nick)
utils.add_hook(handle_whois, 'WHOIS') 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: except ValueError:
irc.msg(source, "Error: Not enough arguments. Needs 3: nick, user, host.") irc.msg(source, "Error: Not enough arguments. Needs 3: nick, user, host.")
return return
irc.proto.spawnClient(nick, ident, host) irc.proto.spawnClient(nick, ident, host, manipulatable=True)
@utils.add_cmd @utils.add_cmd
def quit(irc, source, args): def quit(irc, source, args):
@ -40,6 +40,9 @@ def quit(irc, source, args):
return return
u = utils.nickToUid(irc, nick) u = utils.nickToUid(irc, nick)
quitmsg = ' '.join(args[1:]) or 'Client Quit' 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.proto.quitClient(u, quitmsg)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}]) 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.") irc.msg(source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
return return
u = utils.nickToUid(irc, nick) 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: for channel in clist:
if not utils.isChannel(channel): if not utils.isChannel(channel):
irc.msg(source, "Error: Invalid channel name %r." % channel) irc.msg(source, "Error: Invalid channel name %r." % channel)
@ -85,6 +91,9 @@ def nick(irc, source, args):
elif not utils.isNick(newnick): elif not utils.isNick(newnick):
irc.msg(source, 'Error: Invalid nickname %r.' % newnick) irc.msg(source, 'Error: Invalid nickname %r.' % newnick)
return 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.proto.nickClient(u, newnick)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}]) 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.") irc.msg(source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
return return
u = utils.nickToUid(irc, nick) 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: for channel in clist:
if not utils.isChannel(channel): if not utils.isChannel(channel):
irc.msg(source, "Error: Invalid channel name %r." % channel) irc.msg(source, "Error: Invalid channel name %r." % channel)
@ -138,30 +150,35 @@ def kick(irc, source, args):
def mode(irc, source, args): def mode(irc, source, args):
"""<source> <target> <modes> """<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) utils.checkAuthenticated(irc, source, allowOper=False)
try: try:
modesource, target, modes = args[0], args[1], args[2:] modesource, target, modes = args[0], args[1], args[2:]
except IndexError: except IndexError:
irc.msg(source, 'Error: Not enough arguments. Needs 3: source nick, target, modes to set.') irc.msg(source, 'Error: Not enough arguments. Needs 3: source nick, target, modes to set.')
return return
if not modes: target = utils.nickToUid(irc, target) or target
irc.msg(source, "Error: No modes given to set!") extclient = target in irc.users and not utils.isInternalClient(irc, target)
return
parsedmodes = utils.parseModes(irc, target, modes) parsedmodes = utils.parseModes(irc, target, modes)
targetuid = utils.nickToUid(irc, target) ischannel = target in irc.channels
if targetuid: if not (target in irc.users or ischannel):
target = targetuid
elif not utils.isChannel(target):
irc.msg(source, "Error: Invalid channel or nick %r." % target) irc.msg(source, "Error: Invalid channel or nick %r." % target)
return 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): if utils.isInternalServer(irc, modesource):
# Setting modes from a server.
irc.proto.modeServer(modesource, target, parsedmodes) irc.proto.modeServer(modesource, target, parsedmodes)
irc.callHooks([modesource, 'PYLINK_BOTSPLUGIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}])
else: else:
sourceuid = utils.nickToUid(irc, modesource) # Setting modes from a client.
irc.proto.modeClient(sourceuid, target, parsedmodes) modesource = utils.nickToUid(irc, modesource)
irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}]) irc.proto.modeClient(modesource, target, parsedmodes)
irc.callHooks([modesource, 'PYLINK_BOTSPLUGIN_MODE',
{'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}])
@utils.add_cmd @utils.add_cmd
def msg(irc, source, args): 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')) (userobj.realhost, userobj.ip, userobj.away or '\x1D(not set)\x1D'))
f('\x02Channels\x02: %s' % (' '.join(userobj.channels).strip() or '\x1D(none)\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 @utils.add_cmd
def shutdown(irc, source, args): def shutdown(irc, source, args):
"""takes no arguments. """takes no arguments.
@ -139,3 +185,11 @@ def shutdown(irc, source, args):
# Disable auto-connect first by setting the time to negative. # Disable auto-connect first by setting the time to negative.
ircobj.serverdata['autoconnect'] = -1 ircobj.serverdata['autoconnect'] = -1
ircobj.aborted.set() 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)) log.info('(%s) Executing %r for %s', irc.name, args, utils.getHostmask(irc, source))
exec(args, globals(), locals()) exec(args, globals(), locals())
utils.add_cmd(_exec, 'exec') 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', self.hook_map = {'FJOIN': 'JOIN', 'RSQUIT': 'SQUIT', 'FMODE': 'MODE',
'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST', 'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST',
'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME'} 'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME'}
self.sidgen = utils.TS6SIDGenerator(self.irc)
self.uidgen = {}
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):
"""Spawns a client with nick <nick> on the given IRC connection. """Spawns a client with nick <nick> on the given IRC connection.
Note: No nick collision / valid nickname checks are done here; it is 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 server = server or self.irc.sid
if not utils.isInternalServer(self.irc, server): if not utils.isInternalServer(self.irc, server):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server) raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server)
# We need a separate UID generator instance for every PseudoServer # Create an UIDGenerator instance for every SID, so that each gets
# we spawn. Otherwise, things won't wrap around properly. # distinct values.
if server not in self.irc.uidgen: uid = self.uidgen.setdefault(server, utils.TS6UIDGenerator(server)).next_uid()
self.irc.uidgen[server] = utils.TS6UIDGenerator(server)
uid = self.irc.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) realhost=realhost, ip=ip, manipulatable=manipulatable)
utils.applyModes(self.irc, uid, modes) utils.applyModes(self.irc, 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}"
@ -53,7 +54,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
modes=raw_modes, ip=ip, realname=realname, modes=raw_modes, ip=ip, realname=realname,
realhost=realhost)) realhost=realhost))
if ('o', None) in modes or ('+o', None) in modes: 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 return u
def joinClient(self, client, channel): def joinClient(self, client, channel):
@ -142,26 +143,26 @@ class InspIRCdProtocol(TS6BaseProtocol):
and the change will be reflected here.""" and the change will be reflected here."""
userobj = self.irc.users[target] userobj = self.irc.users[target]
try: try:
otype = opertype or userobj.opertype otype = opertype or userobj.opertype or 'IRC Operator'
except AttributeError: except AttributeError:
log.debug('(%s) opertype field for %s (%s) isn\'t filled yet!', log.debug('(%s) opertype field for %s (%s) isn\'t filled yet!',
self.irc.name, target, userobj.nick) self.irc.name, target, userobj.nick)
# whatever, this is non-standard anyways. # 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.', log.debug('(%s) Sending OPERTYPE from %s to oper them up.',
self.irc.name, target) self.irc.name, target)
userobj.opertype = otype userobj.opertype = otype
self._send(target, 'OPERTYPE %s' % otype) self._send(target, 'OPERTYPE %s' % otype.replace(" ", "_"))
def _sendModes(self, numeric, target, modes, ts=None): def _sendModes(self, numeric, target, modes, ts=None):
"""Internal function to send mode changes from a PyLink client/server.""" """Internal function to send mode changes from a PyLink client/server."""
# -> :9PYAAAAAA FMODE #pylink 1433653951 +os 9PYAAAAAA # -> :9PYAAAAAA FMODE #pylink 1433653951 +os 9PYAAAAAA
# -> :9PYAAAAAA MODE 9PYAAAAAA -i+w # -> :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): 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 # 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. # Servers need a special command to set umode +o on people.
# Why isn't this documented anywhere, InspIRCd?
self._operUp(target) self._operUp(target)
utils.applyModes(self.irc, target, modes) utils.applyModes(self.irc, target, modes)
joinedmodes = utils.joinModes(modes) joinedmodes = utils.joinModes(modes)
@ -272,8 +273,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
# "desc" defaults to the configured server description. # "desc" defaults to the configured server description.
desc = desc or self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc'] desc = desc or self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc']
if sid is None: # No sid given; generate one! if sid is None: # No sid given; generate one!
self.irc.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) sid = self.sidgen.next_sid()
sid = self.irc.sidgen.next_sid()
assert len(sid) == 3, "Incorrect SID length" assert len(sid) == 3, "Incorrect SID length"
if sid in self.irc.servers: if sid in self.irc.servers:
raise ValueError('A server with SID %r already exists!' % sid) raise ValueError('A server with SID %r already exists!' % sid)
@ -285,7 +285,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
if not utils.isServerName(name): if not utils.isServerName(name):
raise ValueError('Invalid server name %r' % name) raise ValueError('Invalid server name %r' % name)
self._send(uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc)) 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') self._send(sid, 'ENDBURST')
return sid return sid
@ -329,7 +329,8 @@ class InspIRCdProtocol(TS6BaseProtocol):
if args[2] != self.irc.serverdata['recvpass']: if args[2] != self.irc.serverdata['recvpass']:
# Check if recvpass is correct # Check if recvpass is correct
raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername) 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 self.irc.uplink = numeric
return return
elif args[0] == 'CAPAB': elif args[0] == 'CAPAB':
@ -459,18 +460,20 @@ class InspIRCdProtocol(TS6BaseProtocol):
servername = args[0].lower() servername = args[0].lower()
sid = args[3] sid = args[3]
sdesc = args[-1] 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} return {'name': servername, 'sid': args[3], 'text': sdesc}
def handle_fmode(self, numeric, command, args): def handle_fmode(self, numeric, command, args):
"""Handles the FMODE command, used for channel mode changes.""" """Handles the FMODE command, used for channel mode changes."""
# <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD # <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD
channel = utils.toLower(self.irc, args[0]) channel = utils.toLower(self.irc, args[0])
oldobj = self.irc.channels[channel].deepcopy()
modes = args[2:] modes = args[2:]
changedmodes = utils.parseModes(self.irc, channel, modes) changedmodes = utils.parseModes(self.irc, channel, modes)
utils.applyModes(self.irc, channel, changedmodes) utils.applyModes(self.irc, 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}
def handle_mode(self, numeric, command, args): def handle_mode(self, numeric, command, args):
"""Handles incoming user mode changes.""" """Handles incoming user mode changes."""
@ -543,7 +546,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
# command sent for it. # command sent for it.
# <- :70MAAAAAB OPERTYPE Network_Owner # <- :70MAAAAAB OPERTYPE Network_Owner
omode = [('+o', None)] 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) utils.applyModes(self.irc, 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.

View File

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

View File

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

View File

@ -18,7 +18,7 @@ class UnrealProtocol(TS6BaseProtocol):
self.casemapping = 'ascii' self.casemapping = 'ascii'
self.proto_ver = 2351 self.proto_ver = 2351
self.hook_map = {} self.hook_map = {}
self.uidgen = {}
self.caps = {} self.caps = {}
self._unrealCmodes = {'l': 'limit', 'c': 'blockcolor', 'G': 'censor', 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) 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 # Unreal 3.4 uses TS6-style UIDs. They don't start from AAAAAA like other IRCd's
# do, but we can do that fine... # do, but we can do that fine...
if server not in self.irc.uidgen: uid = self.uidgen.setdefault(server, utils.TS6UIDGenerator(server)).next_uid()
self.irc.uidgen[server] = utils.TS6UIDGenerator(server)
uid = self.irc.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

4
pylink
View File

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

View File

@ -11,7 +11,10 @@ suites = []
# Yay, import hacks! # Yay, import hacks!
sys.path.append(os.path.join(os.getcwd(), 'tests')) 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 # Strip the tests/ and .py extension: tests/test_whatever.py => test_whatever
module = testfile.replace('.py', '').replace('tests/', '') module = testfile.replace('.py', '').replace('tests/', '')
module = __import__(module) module = __import__(module)

View File

@ -139,25 +139,12 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase):
# Default channels start with +nt # Default channels start with +nt
self.irc.run(':70M FMODE #pylink 1423790412 -nt') self.irc.run(':70M FMODE #pylink 1423790412 -nt')
self.assertEqual(set(), self.irc.channels['#pylink'].modes) self.assertEqual(set(), self.irc.channels['#pylink'].modes)
self.irc.takeHooks()
self.irc.run(':70M FMODE #pylink 1423790412 +ikl herebedragons 100') self.irc.run(':70M FMODE #pylink 1423790412 +ikl herebedragons 100')
self.assertEqual({('i', None), ('k', 'herebedragons'), ('l', '100')}, self.irc.channels['#pylink'].modes) self.assertEqual({('i', None), ('k', 'herebedragons'), ('l', '100')}, self.irc.channels['#pylink'].modes)
self.irc.run(':70M FMODE #pylink 1423790413 -ilk+m herebedragons') self.irc.run(':70M FMODE #pylink 1423790413 -ilk+m herebedragons')
self.assertEqual({('m', None)}, self.irc.channels['#pylink'].modes) 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): def testHandleFModeWithPrefixes(self):
self.irc.run(':70M FJOIN #pylink 123 +n :o,10XAAAAAA ,10XAAAAAB') self.irc.run(':70M FJOIN #pylink 123 +n :o,10XAAAAAA ,10XAAAAAB')
# Prefix modes are stored separately, so they should never show up in .modes # 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.irc.run(':70M FMODE #pylink 123 -o %s' % self.u)
self.assertEqual(modes, self.irc.channels['#pylink'].modes) self.assertEqual(modes, self.irc.channels['#pylink'].modes)
self.assertNotIn(self.u, self.irc.channels['#pylink'].prefixmodes['ops']) 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): def testHandleFModeRemovesOldParams(self):
self.irc.run(':70M FMODE #pylink 1423790412 +l 50') 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.irc.run(':70M FMODE #pylink 1423790412 +l 30')
self.assertIn(('l', '30'), self.irc.channels['#pylink'].modes) self.assertIn(('l', '30'), self.irc.channels['#pylink'].modes)
self.assertNotIn(('l', '50'), 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): def testHandleFJoinResetsTS(self):
curr_ts = self.irc.channels['#pylink'].ts curr_ts = self.irc.channels['#pylink'].ts
@ -238,24 +214,24 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase):
self.assertIn('00C', self.irc.servers) self.assertIn('00C', self.irc.servers)
def testHandleNick(self): 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] hookdata = self.irc.takeHooks()[0][-1]
expected = {'newnick': 'PyLink-devel', 'oldnick': 'PyLink', 'ts': 1434744242} expected = {'newnick': 'PyLink-devel', 'oldnick': 'PyLink', 'ts': 1434744242}
self.assertEqual(hookdata, expected) 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): def testHandleSave(self):
self.irc.run(':9PYAAAAAA NICK Derp_ 1433728673') self.irc.run(':%s NICK Derp_ 1433728673' % self.u)
self.irc.run(':70M SAVE 9PYAAAAAA 1433728673') self.irc.run(':70M SAVE %s 1433728673' % self.u)
hookdata = self.irc.takeHooks()[-1][-1] hookdata = self.irc.takeHooks()[-1][-1]
self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'ts': 1433728673, 'oldnick': 'Derp_'}) self.assertEqual(hookdata, {'target': self.u, 'ts': 1433728673, 'oldnick': 'Derp_'})
self.assertEqual('9PYAAAAAA', self.irc.users['9PYAAAAAA'].nick) self.assertEqual(self.u, self.irc.users[self.u].nick)
def testHandleInvite(self): 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] hookdata = self.irc.takeHooks()[-1][-1]
del hookdata['ts'] del hookdata['ts']
self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'channel': '#blah'}) self.assertEqual(hookdata, {'target': self.u, 'channel': '#blah'})
def testHandleOpertype(self): def testHandleOpertype(self):
self.irc.run('SERVER whatever. abcd 0 10X :Whatever Server - Hellas Planitia, Mars') 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')]) ('+b', '*!*@*.badisp.net')])
self.assertEqual(res, '-o+l-nm+kb 9PYAAAAAA 50 hello *!*@*.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): 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. # Strings.
self.assertEqual(f("+nt-lk"), "-nt+lk") self._reverseModes("+mk-t test", "-mk+t test")
self.assertEqual(f("nt-k"), "-nt+k") self._reverseModes("ml-n 111", "-ml+n")
# Lists. # Lists.
self.assertEqual(f([('+m', None), ('+t', None), ('+l', '3'), ('-o', 'person')]), self._reverseModes([('+m', None), ('+r', None), ('+l', '3')],
[('-m', None), ('-t', None), ('-l', '3'), ('+o', 'person')]) {('-m', None), ('-r', None), ('-l', None)})
# Sets. # 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 # 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__': if __name__ == '__main__':
unittest.main() unittest.main()

212
utils.py
View File

@ -40,10 +40,9 @@ class TS6UIDGenerator():
return uid return uid
class TS6SIDGenerator(): class TS6SIDGenerator():
"""<query> """
TS6 SID Generator. <query> is a 3 character string with any combination of 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, which are used by the generator as a wildcard. On every next_sid() call,
the first available wildcard character (from the right) will be the first available wildcard character (from the right) will be
incremented to generate the next SID. 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) "6##" would give: 600, 601, 602, ... 60Y, 60Z, 610, 611, ... 6ZZ (1296 total results)
""" """
def __init__(self, query): def __init__(self, irc):
self.query = list(query) 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.iters = self.query.copy()
self.output = self.query.copy() self.output = self.query.copy()
self.allowedchars = {} self.allowedchars = {}
@ -94,8 +97,9 @@ class TS6SIDGenerator():
self.increment(pos-1) self.increment(pos-1)
def next_sid(self): def next_sid(self):
while ''.join(self.output) in self.irc.servers:
self.increment()
sid = ''.join(self.output) sid = ''.join(self.output)
self.increment()
return sid return sid
def add_cmd(func, name=None): def add_cmd(func, name=None):
@ -110,10 +114,8 @@ def add_hook(func, command):
world.command_hooks[command].append(func) world.command_hooks[command].append(func)
def toLower(irc, text): def toLower(irc, text):
"""<irc object> <text> """Returns a lowercase representation of text based on the IRC object's
casemapping (rfc1459 or ascii)."""
Returns a lowercase representation of <text> based on <irc object>'s
casemapping (rfc1459 vs ascii)."""
if irc.proto.casemapping == 'rfc1459': if irc.proto.casemapping == 'rfc1459':
text = text.replace('{', '[') text = text.replace('{', '[')
text = text.replace('}', ']') text = text.replace('}', ']')
@ -122,39 +124,42 @@ def toLower(irc, text):
return text.lower() return text.lower()
def nickToUid(irc, nick): 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) nick = toLower(irc, nick)
for k, v in irc.users.items(): for k, v in irc.users.items():
if toLower(irc, v.nick) == nick: if toLower(irc, v.nick) == nick:
return k return k
def clientToServer(irc, numeric): def clientToServer(irc, numeric):
"""<irc object> <numeric> """Finds the SID of the server a user is on."""
Finds the server SID of user <numeric> and returns it."""
for server in irc.servers: for server in irc.servers:
if numeric in irc.servers[server].users: if numeric in irc.servers[server].users:
return server return server
# A+ regex
_nickregex = r'^[A-Za-z\|\\_\[\]\{\}\^\`][A-Z0-9a-z\-\|\\_\[\]\{\}\^\`]*$' _nickregex = r'^[A-Za-z\|\\_\[\]\{\}\^\`][A-Z0-9a-z\-\|\\_\[\]\{\}\^\`]*$'
def isNick(s, nicklen=None): def isNick(s, nicklen=None):
"""Checks whether the string given is a valid nick."""
if nicklen and len(s) > nicklen: if nicklen and len(s) > nicklen:
return False return False
return bool(re.match(_nickregex, s)) return bool(re.match(_nickregex, s))
def isChannel(s): def isChannel(s):
return s.startswith('#') """Checks whether the string given is a valid channel name."""
return str(s).startswith('#')
def _isASCII(s): def _isASCII(s):
chars = string.ascii_letters + string.digits + string.punctuation chars = string.ascii_letters + string.digits + string.punctuation
return all(char in chars for char in s) return all(char in chars for char in s)
def isServerName(s): def isServerName(s):
"""Checks whether the string given is a server name."""
return _isASCII(s) and '.' in s and not s.startswith('.') 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): 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')]
@ -164,11 +169,10 @@ def parseModes(irc, target, args):
# B = Mode that changes a setting and 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. # C = Mode that changes a setting and only has a parameter when set.
# D = Mode that changes a setting and never has a parameter. # D = Mode that changes a setting and never has a parameter.
assert args, 'No valid modes were supplied!'
usermodes = not isChannel(target) usermodes = not isChannel(target)
prefix = '' prefix = ''
modestring = args[0] modestring = args[0]
if not modestring:
return ValueError('No modes supplied in parseModes query: %r' % modes)
args = args[1:] args = args[1:]
if usermodes: if usermodes:
log.debug('(%s) Using irc.umodes for this query: %s', irc.name, irc.umodes) 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) # We're setting a prefix mode on someone (e.g. +o user1)
log.debug('Mode %s: This mode is a prefix mode.', mode) log.debug('Mode %s: This mode is a prefix mode.', mode)
arg = args.pop(0) 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']: elif prefix == '+' and mode in supported_modes['*C']:
# Only has parameter when setting. # Only has parameter when setting.
log.debug('Mode %s: Only has parameter when setting.', mode) log.debug('Mode %s: Only has parameter when setting.', mode)
@ -220,10 +231,9 @@ def parseModes(irc, target, args):
return res return res
def applyModes(irc, target, changedmodes): 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>. The target can be either a channel or a user; this is handled automatically."""
<target> can be either a channel or a user; this is handled automatically."""
usermodes = not isChannel(target) usermodes = not isChannel(target)
log.debug('(%s) Using usermodes for this query? %s', irc.name, usermodes) log.debug('(%s) Using usermodes for this query? %s', irc.name, usermodes)
if usermodes: if usermodes:
@ -292,11 +302,10 @@ def applyModes(irc, target, changedmodes):
irc.channels[target].modes = modelist irc.channels[target].modes = modelist
def joinModes(modes): 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 See testJoinModes in tests/test_utils.py for some examples."""
joins them into a string. See testJoinModes in tests/test_utils.py
for some examples."""
prefix = '+' # Assume we're adding modes unless told otherwise prefix = '+' # Assume we're adding modes unless told otherwise
modelist = '' modelist = ''
args = [] args = []
@ -328,55 +337,112 @@ def joinModes(modes):
modelist += ' %s' % ' '.join(args) modelist += ' %s' % ' '.join(args)
return modelist return modelist
def reverseModes(irc, target, modes): def _flip(mode):
"""<mode string/mode list> """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" Optionally, an oldobj argument can be given to look at an earlier state of
"nt-k" => "-nt+k" a channel/user object, e.g. for checking the op status of a mode setter
[('+m', None), ('+t', None), ('+l', '3'), ('-o', 'person')] => before their modes are processed and added to the channel state.
[('-m', None), ('-t', None), ('-l', '3'), ('+o', 'person')]
[('s', None), ('+n', None)] => [('-s', None), ('-n', None)] 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) origtype = type(modes)
# Operate on joined modestrings only; it's easier. # If the query is a string, we have to parse it first.
if origtype != str: if origtype == str:
modes = joinModes(modes) modes = parseModes(irc, target, modes.split(" "))
# Swap the +'s and -'s by replacing one with a dummy character, and then changing it back. # Get the current mode list first.
assert '\x00' not in modes, 'NUL cannot be in the mode list (it is a reserved character)!' if isChannel(target):
if not modes.startswith(('+', '-')): c = oldobj or irc.channels[target]
modes = '+' + modes oldmodes = c.modes.copy()
newmodes = modes.replace('+', '\x00') possible_modes = irc.cmodes.copy()
newmodes = newmodes.replace('-', '+') # For channels, this also includes the list of prefix modes.
newmodes = newmodes.replace('\x00', '-') possible_modes['*A'] += ''.join(irc.prefixmodes)
if origtype != str: for name, userlist in c.prefixmodes.items():
# If the original query isn't a string, send back the parseModes() output. try:
return parseModes(irc, target, newmodes.split(" ")) oldmodes.update([(irc.cmodes[name[:-1]], u) for u in userlist])
except KeyError:
continue
else: 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): def isInternalClient(irc, numeric):
"""<irc object> <client numeric> """
Checks whether the given numeric is a PyLink Client,
Checks whether <client numeric> is a PyLink PseudoClient, returning the SID of the server it's on if so.
returning the SID of the PseudoClient's server if True.
""" """
for sid in irc.servers: for sid in irc.servers:
if irc.servers[sid].internal and numeric in irc.servers[sid].users: if irc.servers[sid].internal and numeric in irc.servers[sid].users:
return sid return sid
def isInternalServer(irc, sid): def isInternalServer(irc, sid):
"""<irc object> <sid> """Returns whether the given SID is an internal PyLink server."""
Returns whether <sid> is an internal PyLink PseudoServer.
"""
return (sid in irc.servers and irc.servers[sid].internal) return (sid in irc.servers and irc.servers[sid].internal)
def isOper(irc, uid, allowAuthed=True, allowOper=True): def isOper(irc, uid, allowAuthed=True, allowOper=True):
"""<irc object> <UID> """
Returns whether the given user has operator status on PyLink. This can be achieved
Returns whether <UID> has operator status on PyLink. This can be achieved
by either identifying to PyLink as admin (if allowAuthed is True), 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 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 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 return False
def checkAuthenticated(irc, uid, allowAuthed=True, allowOper=True): def checkAuthenticated(irc, uid, allowAuthed=True, allowOper=True):
"""<irc object> <UID> """
Checks whetherthe given user has operator status on PyLink, raising
Checks whether user <UID> has operator status on PyLink, raising NotAuthenticatedError and logging the access denial if not.
NotAuthenticatedError and logging the access denial if not.""" """
lastfunc = inspect.stack()[1][3] lastfunc = inspect.stack()[1][3]
if not isOper(irc, uid, allowAuthed=allowAuthed, allowOper=allowOper): if not isOper(irc, uid, allowAuthed=allowAuthed, allowOper=allowOper):
log.warning('(%s) Access denied for %s calling %r', irc.name, 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!") raise NotAuthenticatedError("You are not authenticated!")
return True return True
def getHostmask(irc, user): def isManipulatableClient(irc, uid):
"""<irc object> <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) userobj = irc.users.get(user)
if userobj is None: if userobj is None:
return '<user object not found>' return '<user object not found>'
@ -421,8 +494,3 @@ def getHostmask(irc, user):
except AttributeError: except AttributeError:
host = '<unknown host>' host = '<unknown host>'
return '%s!%s@%s' % (nick, ident, 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 from collections import defaultdict
import threading import threading
import subprocess
# Global variable to indicate whether we're being ran directly, or imported # Global variable to indicate whether we're being ran directly, or imported
# for a testcase. # for a testcase.
@ -16,3 +17,14 @@ schedulers = {}
plugins = [] plugins = []
whois_handlers = [] whois_handlers = []
started = threading.Event() 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))