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:
commit
886994475d
10
README.md
10
README.md
@ -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???
|
||||
|
45
classes.py
45
classes.py
@ -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,12 +207,16 @@ class Irc():
|
||||
line = line.strip(b'\r')
|
||||
# FIXME: respect other encodings?
|
||||
line = line.decode("utf-8", "replace")
|
||||
self.runline(line)
|
||||
|
||||
def runline(self, line):
|
||||
"""Sends a command to the protocol module."""
|
||||
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)
|
||||
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
|
||||
@ -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
|
||||
|
3
conf.py
3
conf.py
@ -27,7 +27,8 @@ testconf = {'bot':
|
||||
'hostname': "pylink.unittest",
|
||||
'sid': "9PY",
|
||||
'channels': ["#pylink"],
|
||||
'maxnicklen': 20
|
||||
'maxnicklen': 20,
|
||||
'sidrange': '8##'
|
||||
})
|
||||
}
|
||||
if world.testing:
|
||||
|
@ -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: []
|
||||
|
@ -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')
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
1001
plugins/relay.py
1001
plugins/relay.py
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||
|
@ -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."""
|
||||
|
@ -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."""
|
||||
|
@ -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
4
pylink
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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
212
utils.py
@ -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):
|
||||
sid = ''.join(self.output)
|
||||
while ''.join(self.output) in self.irc.servers:
|
||||
self.increment()
|
||||
sid = ''.join(self.output)
|
||||
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))
|
||||
|
12
world.py
12
world.py
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user