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

Merge commit '320de2079a78202e99c7b6aeb53c28c13f43ba47'

Many things here, including:
- New 'exec' plugin
- INVITE, umode +H (hideoper) support for relay
- New and improved 'showuser' command, now with internals that support multiple binds to one command name.
- relay: bug fixes, like not sending empty user mode changes.
This commit is contained in:
James Lu 2015-09-12 09:11:52 -07:00
commit d6cb9d45c7
19 changed files with 1008 additions and 717 deletions

View File

@ -1,9 +1,263 @@
import threading
from random import randint
import time
import socket
import threading
import ssl
from collections import defaultdict
import hashlib
from log import log
import main
import time
from conf import conf
import world
### Exceptions
class ProtocolError(Exception):
pass
### Internal classes (users, servers, channels)
class Irc():
def initVars(self):
self.pseudoclient = None
self.connected = threading.Event()
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.users = {}
self.channels = defaultdict(IrcChannel)
# Sets flags such as whether to use halfops, etc. The default RFC1459
# modes are implied.
self.cmodes = {'op': 'o', 'secret': 's', 'private': 'p',
'noextmsg': 'n', 'moderated': 'm', 'inviteonly': 'i',
'topiclock': 't', 'limit': 'l', 'ban': 'b',
'voice': 'v', 'key': 'k',
# Type A, B, and C modes
'*A': 'b',
'*B': 'k',
'*C': 'l',
'*D': 'imnpstr'}
self.umodes = {'invisible': 'i', 'snomask': 's', 'wallops': 'w',
'oper': 'o',
'*A': '', '*B': '', '*C': 's', '*D': 'iow'}
# This max nick length starts off as the config value, but may be
# overwritten later by the protocol module if such information is
# received. Note that only some IRCds (InspIRCd) give us nick length
# during link, so it is still required that the config value be set!
self.maxnicklen = self.serverdata['maxnicklen']
self.prefixmodes = {'o': '@', 'v': '+'}
# Uplink SID (filled in by protocol module)
self.uplink = None
self.start_ts = int(time.time())
# UID generators, for servers that need it
self.uidgen = {}
def __init__(self, netname, proto):
# Initialize some variables
self.name = netname.lower()
self.conf = conf
self.serverdata = conf['servers'][netname]
self.sid = self.serverdata["sid"]
self.botdata = conf['bot']
self.proto = proto
self.pingfreq = self.serverdata.get('pingfreq') or 30
self.pingtimeout = self.pingfreq * 2
self.initVars()
if world.testing:
# HACK: Don't thread if we're running tests.
self.connect()
else:
self.connection_thread = threading.Thread(target = self.connect)
self.connection_thread.start()
self.pingTimer = None
def connect(self):
ip = self.serverdata["ip"]
port = self.serverdata["port"]
while True:
self.initVars()
checks_ok = True
try:
self.socket = socket.socket()
self.socket.setblocking(0)
# Initial connection timeout is a lot smaller than the timeout after
# we've connected; this is intentional.
self.socket.settimeout(self.pingfreq)
self.ssl = self.serverdata.get('ssl')
if self.ssl:
log.info('(%s) Attempting SSL for this connection...', self.name)
certfile = self.serverdata.get('ssl_certfile')
keyfile = self.serverdata.get('ssl_keyfile')
if certfile and keyfile:
try:
self.socket = ssl.wrap_socket(self.socket,
certfile=certfile,
keyfile=keyfile)
except OSError:
log.exception('(%s) Caught OSError trying to '
'initialize the SSL connection; '
'are "ssl_certfile" and '
'"ssl_keyfile" set correctly?',
self.name)
checks_ok = False
else:
log.error('(%s) SSL certfile/keyfile was not set '
'correctly, aborting... ', self.name)
checks_ok = False
log.info("Connecting to network %r on %s:%s", self.name, ip, port)
self.socket.connect((ip, port))
self.socket.settimeout(self.pingtimeout)
if self.ssl and checks_ok:
peercert = self.socket.getpeercert(binary_form=True)
sha1fp = hashlib.sha1(peercert).hexdigest()
expected_fp = self.serverdata.get('ssl_fingerprint')
if expected_fp:
if sha1fp != expected_fp:
log.error('(%s) Uplink\'s SSL certificate '
'fingerprint (SHA1) does not match the '
'one configured: expected %r, got %r; '
'disconnecting...', self.name,
expected_fp, sha1fp)
checks_ok = False
else:
log.info('(%s) Uplink SSL certificate fingerprint '
'(SHA1) verified: %r', self.name, sha1fp)
else:
log.info('(%s) Uplink\'s SSL certificate fingerprint '
'is %r. You can enhance the security of your '
'link by specifying this in a "ssl_fingerprint"'
' option in your server block.', self.name,
sha1fp)
if checks_ok:
self.proto.connect(self)
self.spawnMain()
log.info('(%s) Starting ping schedulers....', self.name)
self.schedulePing()
log.info('(%s) Server ready; listening for data.', self.name)
self.run()
else:
log.error('(%s) A configuration error was encountered '
'trying to set up this connection. Please check'
' your configuration file and try again.',
self.name)
except (socket.error, ProtocolError, ConnectionError) as e:
log.warning('(%s) Disconnected from IRC: %s: %s',
self.name, type(e).__name__, str(e))
self.disconnect()
autoconnect = self.serverdata.get('autoconnect')
log.debug('(%s) Autoconnect delay set to %s seconds.', self.name, autoconnect)
if autoconnect is not None and autoconnect >= 0:
log.info('(%s) Going to auto-reconnect in %s seconds.', self.name, autoconnect)
time.sleep(autoconnect)
else:
return
def disconnect(self):
log.debug('(%s) Canceling pingTimer at %s due to disconnect() call', self.name, time.time())
self.connected.clear()
try:
self.socket.close()
self.pingTimer.cancel()
except: # Socket timed out during creation; ignore
pass
# Internal hook signifying that a network has disconnected.
self.callHooks([None, 'PYLINK_DISCONNECT', {}])
def run(self):
buf = b""
data = b""
while True:
data = self.socket.recv(2048)
buf += data
if self.connected.is_set() and not data:
log.warning('(%s) No data received and self.connected is set; disconnecting!', self.name)
return
elif (time.time() - self.lastping) > self.pingtimeout:
log.warning('(%s) Connection timed out.', self.name)
return
while b'\n' in buf:
line, buf = buf.split(b'\n', 1)
line = line.strip(b'\r')
# TODO: respect other encodings?
line = line.decode("utf-8", "replace")
log.debug("(%s) <- %s", self.name, line)
hook_args = None
try:
hook_args = self.proto.handle_events(self, line)
except Exception:
log.exception('(%s) Caught error in handle_events, disconnecting!', self.name)
return
# Only call our hooks if there's data to process. Handlers that support
# hooks will return a dict of parsed arguments, which can be passed on
# to plugins and the like. For example, the JOIN handler will return
# something like: {'channel': '#whatever', 'users': ['UID1', 'UID2',
# 'UID3']}, etc.
if hook_args is not None:
self.callHooks(hook_args)
def callHooks(self, hook_args):
numeric, command, parsed_args = hook_args
# Always make sure TS is sent.
if 'ts' not in parsed_args:
parsed_args['ts'] = int(time.time())
hook_cmd = command
hook_map = self.proto.hook_map
# Handlers can return a 'parse_as' key to send their payload to a
# different hook. An example of this is "/join 0" being interpreted
# as leaving all channels (PART).
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)
# Iterate over hooked functions, catching errors accordingly
for hook_func in world.command_hooks[hook_cmd]:
try:
log.debug('Calling function %s', 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)
continue
def send(self, data):
# Safeguard against newlines in input!! Otherwise, each line gets
# treated as a separate command, which is particularly nasty.
data = data.replace('\n', ' ')
data = data.encode("utf-8") + b"\n"
stripped_data = data.decode("utf-8").strip("\n")
log.debug("(%s) -> %s", self.name, stripped_data)
try:
self.socket.send(data)
except (OSError, AttributeError):
log.debug("(%s) Dropping message %r; network isn't connected!", self.name, stripped_data)
def schedulePing(self):
self.proto.pingServer(self)
self.pingTimer = threading.Timer(self.pingfreq, self.schedulePing)
self.pingTimer.daemon = True
self.pingTimer.start()
log.debug('(%s) Ping scheduled at %s', self.name, time.time())
def spawnMain(self):
nick = self.botdata.get('nick') or 'PyLink'
ident = self.botdata.get('ident') or 'pylink'
host = self.serverdata["hostname"]
log.info('(%s) Connected! Spawning main client %s.', self.name, nick)
olduserobj = self.pseudoclient
self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("+o", None)})
for chan in self.serverdata['channels']:
self.proto.joinClient(self, self.pseudoclient.uid, chan)
# PyLink internal hook called when spawnMain is called and the
# contents of Irc().pseudoclient change.
self.callHooks([self.sid, 'PYLINK_SPAWNMAIN', {'olduser': olduserobj}])
class IrcUser():
def __init__(self, nick, ts, uid, ident='null', host='null',
@ -36,7 +290,7 @@ class IrcServer():
"""
def __init__(self, uplink, name, internal=False):
self.uplink = uplink
self.users = []
self.users = set()
self.internal = internal
self.name = name.lower()
def __repr__(self):
@ -45,7 +299,7 @@ class IrcServer():
class IrcChannel():
def __init__(self):
self.users = set()
self.modes = set()
self.modes = {('n', None), ('t', None)}
self.topic = ''
self.ts = int(time.time())
self.topicset = False
@ -60,33 +314,9 @@ class IrcChannel():
s.discard(target)
self.users.discard(target)
class ProtocolError(Exception):
pass
### FakeIRC classes, used for test cases
global testconf
testconf = {'bot':
{
'nick': 'PyLink',
'user': 'pylink',
'realname': 'PyLink Service Client',
'loglevel': 'DEBUG',
},
'servers':
{'unittest':
{
'ip': '0.0.0.0',
'port': 7000,
'recvpass': "abcd",
'sendpass': "abcd",
'protocol': "null",
'hostname': "pylink.unittest",
'sid': "9PY",
'channels': ["#pylink"],
},
},
}
class FakeIRC(main.Irc):
class FakeIRC(Irc):
def connect(self):
self.messages = []
self.hookargs = []
@ -100,7 +330,10 @@ class FakeIRC(main.Irc):
def run(self, data):
"""Queues a message to the fake IRC server."""
log.debug('<- ' + data)
self.proto.handle_events(self, data)
hook_args = self.proto.handle_events(self, data)
if hook_args is not None:
self.hookmsgs.append(hook_args)
self.callHooks(hook_args)
def send(self, data):
self.messages.append(data)
@ -132,13 +365,13 @@ class FakeIRC(main.Irc):
self.hookmsgs = []
return hookmsgs
@staticmethod
def dummyhook(irc, source, command, parsed_args):
"""Dummy function to bind to hooks."""
irc.hookmsgs.append(parsed_args)
class FakeProto():
"""Dummy protocol module for testing purposes."""
def __init__(self):
self.hook_map = {}
self.casemapping = 'rfc1459'
self.__name__ = 'FakeProto'
@staticmethod
def handle_events(irc, data):
pass

46
conf.py
View File

@ -1,19 +1,53 @@
import yaml
import sys
from collections import defaultdict
global confname
try:
import world
global testconf
testconf = {'bot':
{
'nick': 'PyLink',
'user': 'pylink',
'realname': 'PyLink Service Client',
# Suppress logging in the test output for the most part.
'loglevel': 'CRITICAL',
'serverdesc': 'PyLink unit tests'
},
'servers':
# Wildcard defaultdict! This means that
# any network name you try will work and return
# this basic template:
defaultdict(lambda: {
'ip': '0.0.0.0',
'port': 7000,
'recvpass': "abcd",
'sendpass': "chucknorris",
'protocol': "null",
'hostname': "pylink.unittest",
'sid': "9PY",
'channels': ["#pylink"],
'maxnicklen': 20
})
}
if world.testing:
conf = testconf
confname = 'testconf'
else:
try:
# Get the config name from the command line, falling back to config.yml
# if not given.
fname = sys.argv[1]
confname = fname.split('.', 1)[0]
except IndexError:
except IndexError:
# confname is used for logging and PID writing, so that each
# instance uses its own files. fname is the actual name of the file
# we load.
confname = 'pylink'
fname = 'config.yml'
with open(fname, 'r') as f:
global conf
with open(fname, 'r') as f:
try:
conf = yaml.load(f)
except Exception as e:
print('ERROR: Failed to load config from %r: %s: %s' % (fname, type(e).__name__, e))
sys.exit(4)

View File

@ -2,6 +2,7 @@
import utils
from log import log
import world
# Handle KILLs sent to the PyLink client and respawn
def handle_kill(irc, source, command, args):
@ -24,13 +25,12 @@ def handle_commands(irc, source, command, args):
cmd_args = text.split(' ')
cmd = cmd_args[0].lower()
cmd_args = cmd_args[1:]
try:
func = utils.bot_commands[cmd]
except KeyError:
utils.msg(irc, source, 'Unknown command %r.' % cmd)
if cmd not in world.bot_commands:
utils.msg(irc, source, 'Error: Unknown command %r.' % cmd)
return
try:
log.info('(%s) Calling command %r for %s', irc.name, cmd, utils.getHostmask(irc, source))
for func in world.bot_commands[cmd]:
try:
func(irc, source, cmd_args)
except Exception as e:
log.exception('Unhandled exception caught in command %r', cmd)
@ -75,7 +75,13 @@ def handle_whois(irc, source, command, args):
# 313: sends a string denoting the target's operator privilege,
# only if they have umode +o.
if ('o', None) in user.modes:
f(irc, server, 313, source, "%s :is an IRC Operator" % nick)
if hasattr(user, 'opertype'):
opertype = user.opertype.replace("_", " ")
else:
opertype = "IRC Operator"
# Let's be gramatically correct.
n = 'n' if opertype[0].lower() in 'aeiou' else ''
f(irc, server, 313, source, "%s :is a%s %s" % (nick, n, opertype))
# 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd.
# Only show this to opers!
if sourceisOper:
@ -84,7 +90,7 @@ def handle_whois(irc, source, command, args):
# idle time, so we simply return 0.
# <- 317 GL GL 15 1437632859 :seconds idle, signon time
f(irc, server, 317, source, "%s 0 %s :seconds idle, signon time" % (nick, user.ts))
for func in utils.whois_handlers:
for func in world.whois_handlers:
# Iterate over custom plugin WHOIS handlers. They return a tuple
# or list with two arguments: the numeric, and the text to send.
try:

261
main.py
View File

@ -2,258 +2,17 @@
import imp
import os
import socket
import time
import sys
from collections import defaultdict
import threading
import ssl
import hashlib
from log import log
# This must be done before conf imports, so we get the real conf instead of testing one.
import world
world.testing = False
import conf
from log import log
import classes
import utils
import coreplugin
class Irc():
def initVars(self):
self.pseudoclient = None
self.connected = threading.Event()
self.lastping = time.time()
# Server, channel, and user indexes to be populated by our protocol module
self.servers = {self.sid: classes.IrcServer(None, self.serverdata['hostname'], internal=True)}
self.users = {}
self.channels = defaultdict(classes.IrcChannel)
# Sets flags such as whether to use halfops, etc. The default RFC1459
# modes are implied.
self.cmodes = {'op': 'o', 'secret': 's', 'private': 'p',
'noextmsg': 'n', 'moderated': 'm', 'inviteonly': 'i',
'topiclock': 't', 'limit': 'l', 'ban': 'b',
'voice': 'v', 'key': 'k',
# Type A, B, and C modes
'*A': 'b',
'*B': 'k',
'*C': 'l',
'*D': 'imnpstr'}
self.umodes = {'invisible': 'i', 'snomask': 's', 'wallops': 'w',
'oper': 'o',
'*A': '', '*B': '', '*C': 's', '*D': 'iow'}
# This max nick length starts off as the config value, but may be
# overwritten later by the protocol module if such information is
# received. Note that only some IRCds (InspIRCd) give us nick length
# during link, so it is still required that the config value be set!
self.maxnicklen = self.serverdata['maxnicklen']
self.prefixmodes = {'o': '@', 'v': '+'}
# Uplink SID (filled in by protocol module)
self.uplink = None
self.start_ts = int(time.time())
# UID generators, for servers that need it
self.uidgen = {}
def __init__(self, netname, proto, conf):
# Initialize some variables
self.name = netname.lower()
self.conf = conf
self.serverdata = conf['servers'][netname]
self.sid = self.serverdata["sid"]
self.botdata = conf['bot']
self.proto = proto
self.pingfreq = self.serverdata.get('pingfreq') or 30
self.pingtimeout = self.pingfreq * 2
self.initVars()
self.connection_thread = threading.Thread(target = self.connect)
self.connection_thread.start()
self.pingTimer = None
def connect(self):
ip = self.serverdata["ip"]
port = self.serverdata["port"]
while True:
self.initVars()
checks_ok = True
try:
self.socket = socket.socket()
self.socket.setblocking(0)
# Initial connection timeout is a lot smaller than the timeout after
# we've connected; this is intentional.
self.socket.settimeout(self.pingfreq)
self.ssl = self.serverdata.get('ssl')
if self.ssl:
log.info('(%s) Attempting SSL for this connection...', self.name)
certfile = self.serverdata.get('ssl_certfile')
keyfile = self.serverdata.get('ssl_keyfile')
if certfile and keyfile:
try:
self.socket = ssl.wrap_socket(self.socket,
certfile=certfile,
keyfile=keyfile)
except OSError:
log.exception('(%s) Caught OSError trying to '
'initialize the SSL connection; '
'are "ssl_certfile" and '
'"ssl_keyfile" set correctly?',
self.name)
checks_ok = False
else:
log.error('(%s) SSL certfile/keyfile was not set '
'correctly, aborting... ', self.name)
checks_ok = False
log.info("Connecting to network %r on %s:%s", self.name, ip, port)
self.socket.connect((ip, port))
self.socket.settimeout(self.pingtimeout)
if self.ssl and checks_ok:
peercert = self.socket.getpeercert(binary_form=True)
sha1fp = hashlib.sha1(peercert).hexdigest()
expected_fp = self.serverdata.get('ssl_fingerprint')
if expected_fp:
if sha1fp != expected_fp:
log.error('(%s) Uplink\'s SSL certificate '
'fingerprint (SHA1) does not match the '
'one configured: expected %r, got %r; '
'disconnecting...', self.name,
expected_fp, sha1fp)
checks_ok = False
else:
log.info('(%s) Uplink SSL certificate fingerprint '
'(SHA1) verified: %r', self.name, sha1fp)
else:
log.info('(%s) Uplink\'s SSL certificate fingerprint '
'is %r. You can enhance the security of your '
'link by specifying this in a "ssl_fingerprint"'
' option in your server block.', self.name,
sha1fp)
if checks_ok:
self.proto.connect(self)
self.spawnMain()
log.info('(%s) Starting ping schedulers....', self.name)
self.schedulePing()
log.info('(%s) Server ready; listening for data.', self.name)
self.run()
else:
log.error('(%s) A configuration error was encountered '
'trying to set up this connection. Please check'
' your configuration file and try again.',
self.name)
except (socket.error, classes.ProtocolError, ConnectionError) as e:
log.warning('(%s) Disconnected from IRC: %s: %s',
self.name, type(e).__name__, str(e))
self.disconnect()
autoconnect = self.serverdata.get('autoconnect')
log.debug('(%s) Autoconnect delay set to %s seconds.', self.name, autoconnect)
if autoconnect is not None and autoconnect >= 0:
log.info('(%s) Going to auto-reconnect in %s seconds.', self.name, autoconnect)
time.sleep(autoconnect)
else:
return
def disconnect(self):
log.debug('(%s) Canceling pingTimer at %s due to disconnect() call', self.name, time.time())
self.connected.clear()
try:
self.socket.close()
self.pingTimer.cancel()
except: # Socket timed out during creation; ignore
pass
# Internal hook signifying that a network has disconnected.
self.callHooks([None, 'PYLINK_DISCONNECT', {}])
def run(self):
buf = b""
data = b""
while True:
data = self.socket.recv(2048)
buf += data
if self.connected.is_set() and not data:
log.warning('(%s) No data received and self.connected is set; disconnecting!', self.name)
return
elif (time.time() - self.lastping) > self.pingtimeout:
log.warning('(%s) Connection timed out.', self.name)
return
while b'\n' in buf:
line, buf = buf.split(b'\n', 1)
line = line.strip(b'\r')
# TODO: respect other encodings?
line = line.decode("utf-8", "replace")
log.debug("(%s) <- %s", self.name, line)
hook_args = None
try:
hook_args = self.proto.handle_events(self, line)
except Exception:
log.exception('(%s) Caught error in handle_events, disconnecting!', self.name)
return
# Only call our hooks if there's data to process. Handlers that support
# hooks will return a dict of parsed arguments, which can be passed on
# to plugins and the like. For example, the JOIN handler will return
# something like: {'channel': '#whatever', 'users': ['UID1', 'UID2',
# 'UID3']}, etc.
if hook_args is not None:
self.callHooks(hook_args)
def callHooks(self, hook_args):
numeric, command, parsed_args = hook_args
# Always make sure TS is sent.
if 'ts' not in parsed_args:
parsed_args['ts'] = int(time.time())
hook_cmd = command
hook_map = self.proto.hook_map
# Handlers can return a 'parse_as' key to send their payload to a
# different hook. An example of this is "/join 0" being interpreted
# as leaving all channels (PART).
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)
# Iterate over hooked functions, catching errors accordingly
for hook_func in utils.command_hooks[hook_cmd]:
try:
log.debug('Calling function %s', 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)
continue
def send(self, data):
# Safeguard against newlines in input!! Otherwise, each line gets
# treated as a separate command, which is particularly nasty.
data = data.replace('\n', ' ')
data = data.encode("utf-8") + b"\n"
stripped_data = data.decode("utf-8").strip("\n")
log.debug("(%s) -> %s", self.name, stripped_data)
try:
self.socket.send(data)
except (OSError, AttributeError):
log.debug("(%s) Dropping message %r; network isn't connected!", self.name, stripped_data)
def schedulePing(self):
self.proto.pingServer(self)
self.pingTimer = threading.Timer(self.pingfreq, self.schedulePing)
self.pingTimer.daemon = True
self.pingTimer.start()
log.debug('(%s) Ping scheduled at %s', self.name, time.time())
def spawnMain(self):
nick = self.botdata.get('nick') or 'PyLink'
ident = self.botdata.get('ident') or 'pylink'
host = self.serverdata["hostname"]
log.info('(%s) Connected! Spawning main client %s.', self.name, nick)
olduserobj = self.pseudoclient
self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("+o", None)})
for chan in self.serverdata['channels']:
self.proto.joinClient(self, self.pseudoclient.uid, chan)
# PyLink internal hook called when spawnMain is called and the
# contents of Irc().pseudoclient change.
self.callHooks([self.sid, 'PYLINK_SPAWNMAIN', {'olduser': olduserobj}])
if __name__ == '__main__':
log.info('PyLink starting...')
if conf.conf['login']['password'] == 'changeme':
@ -267,7 +26,7 @@ if __name__ == '__main__':
# Import plugins first globally, because they can listen for events
# that happen before the connection phase.
utils.plugins.append(coreplugin)
world.plugins.append(coreplugin)
to_load = conf.conf['plugins']
plugins_folder = [os.path.join(os.getcwd(), 'plugins')]
# Here, we override the module lookup and import the plugins
@ -276,7 +35,7 @@ if __name__ == '__main__':
try:
moduleinfo = imp.find_module(plugin, plugins_folder)
pl = imp.load_source(plugin, moduleinfo[1])
utils.plugins.append(pl)
world.plugins.append(pl)
except ImportError as e:
if str(e) == ('No module named %r' % plugin):
log.error('Failed to load plugin %r: The plugin could not be found.', plugin)
@ -299,7 +58,7 @@ if __name__ == '__main__':
log.critical('Failed to load protocol module: ImportError: %s', protoname, str(e))
sys.exit(2)
else:
utils.networkobjects[network] = Irc(network, proto, conf.conf)
utils.started.set()
log.info("loaded plugins: %s", utils.plugins)
world.networkobjects[network] = classes.Irc(network, proto)
world.started.set()
log.info("loaded plugins: %s", world.plugins)

View File

@ -1,47 +1,22 @@
# admin.py: PyLink administrative commands
import sys
import os
import inspect
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils
from log import log
class NotAuthenticatedError(Exception):
pass
def checkauthenticated(irc, source):
lastfunc = inspect.stack()[1][3]
if not irc.users[source].identified:
log.warning('(%s) Access denied for %s calling %r', irc.name,
utils.getHostmask(irc, source), lastfunc)
raise NotAuthenticatedError("You are not authenticated!")
def _exec(irc, source, args):
"""<code>
Admin-only. Executes <code> in the current PyLink instance.
\x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02"""
checkauthenticated(irc, source)
args = ' '.join(args)
if not args.strip():
utils.msg(irc, source, 'No code entered!')
return
log.info('(%s) Executing %r for %s', irc.name, args, utils.getHostmask(irc, source))
exec(args, globals(), locals())
utils.add_cmd(_exec, 'exec')
@utils.add_cmd
def spawnclient(irc, source, args):
"""<nick> <ident> <host>
Admin-only. Spawns the specified PseudoClient on the PyLink server.
Note: this doesn't check the validity of any fields you give it!"""
checkauthenticated(irc, source)
utils.checkAuthenticated(irc, source, allowOper=False)
try:
nick, ident, host = args[:3]
except ValueError:
utils.msg(irc, source, "Error: not enough arguments. Needs 3: nick, user, host.")
utils.msg(irc, source, "Error: Not enough arguments. Needs 3: nick, user, host.")
return
irc.proto.spawnClient(irc, nick, ident, host)
@ -50,17 +25,17 @@ def quit(irc, source, args):
"""<target> [<reason>]
Admin-only. Quits the PyLink client with nick <target>, if one exists."""
checkauthenticated(irc, source)
utils.checkAuthenticated(irc, source, allowOper=False)
try:
nick = args[0]
except IndexError:
utils.msg(irc, source, "Error: not enough arguments. Needs 1-2: nick, reason (optional).")
utils.msg(irc, source, "Error: Not enough arguments. Needs 1-2: nick, reason (optional).")
return
if irc.pseudoclient.uid == utils.nickToUid(irc, nick):
utils.msg(irc, source, "Error: cannot quit the main PyLink PseudoClient!")
utils.msg(irc, source, "Error: Cannot quit the main PyLink PseudoClient!")
return
u = utils.nickToUid(irc, nick)
quitmsg = ' '.join(args[1:]) or 'Client quit'
quitmsg = ' '.join(args[1:]) or 'Client Quit'
irc.proto.quitClient(irc, u, quitmsg)
irc.callHooks([u, 'PYLINK_ADMIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}])
@ -68,14 +43,14 @@ def joinclient(irc, source, args):
"""<target> <channel1>,[<channel2>], etc.
Admin-only. Joins <target>, the nick of a PyLink client, to a comma-separated list of channels."""
checkauthenticated(irc, source)
utils.checkAuthenticated(irc, source, allowOper=False)
try:
nick = args[0]
clist = args[1].split(',')
if not clist:
raise IndexError
except IndexError:
utils.msg(irc, source, "Error: not enough arguments. Needs 2: nick, comma separated list of channels.")
utils.msg(irc, source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
return
u = utils.nickToUid(irc, nick)
for channel in clist:
@ -93,12 +68,12 @@ def nick(irc, source, args):
"""<target> <newnick>
Admin-only. Changes the nick of <target>, a PyLink client, to <newnick>."""
checkauthenticated(irc, source)
utils.checkAuthenticated(irc, source, allowOper=False)
try:
nick = args[0]
newnick = args[1]
except IndexError:
utils.msg(irc, source, "Error: not enough arguments. Needs 2: nick, newnick.")
utils.msg(irc, source, "Error: Not enough arguments. Needs 2: nick, newnick.")
return
u = utils.nickToUid(irc, nick)
if newnick in ('0', u):
@ -114,13 +89,13 @@ def part(irc, source, args):
"""<target> <channel1>,[<channel2>],... [<reason>]
Admin-only. Parts <target>, the nick of a PyLink client, from a comma-separated list of channels."""
checkauthenticated(irc, source)
utils.checkAuthenticated(irc, source, allowOper=False)
try:
nick = args[0]
clist = args[1].split(',')
reason = ' '.join(args[2:])
except IndexError:
utils.msg(irc, source, "Error: not enough arguments. Needs 2: nick, comma separated list of channels.")
utils.msg(irc, source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
return
u = utils.nickToUid(irc, nick)
for channel in clist:
@ -135,14 +110,14 @@ def kick(irc, source, args):
"""<source> <channel> <user> [<reason>]
Admin-only. Kicks <user> from <channel> via <source>, where <source> is the nick of a PyLink client."""
checkauthenticated(irc, source)
utils.checkAuthenticated(irc, source, allowOper=False)
try:
nick = args[0]
channel = args[1]
target = args[2]
reason = ' '.join(args[3:])
except IndexError:
utils.msg(irc, source, "Error: not enough arguments. Needs 3-4: source nick, channel, target, reason (optional).")
utils.msg(irc, source, "Error: Not enough arguments. Needs 3-4: source nick, channel, target, reason (optional).")
return
u = utils.nickToUid(irc, nick) or nick
targetu = utils.nickToUid(irc, target)
@ -155,38 +130,19 @@ def kick(irc, source, args):
irc.proto.kickClient(irc, u, channel, targetu, reason)
irc.callHooks([u, 'PYLINK_ADMIN_KICK', {'channel': channel, 'target': targetu, 'text': reason, 'parse_as': 'KICK'}])
@utils.add_cmd
def showuser(irc, source, args):
"""<user>
Admin-only. Shows information about <user>."""
checkauthenticated(irc, source)
try:
target = args[0]
except IndexError:
utils.msg(irc, source, "Error: not enough arguments. Needs 1: nick.")
return
u = utils.nickToUid(irc, target)
if u is None:
utils.msg(irc, source, 'Error: unknown user %r' % target)
return
s = ['\x02%s\x02: %s' % (k, v) for k, v in sorted(irc.users[u].__dict__.items())]
s = 'Information on user \x02%s\x02: %s' % (target, '; '.join(s))
utils.msg(irc, source, s)
@utils.add_cmd
def showchan(irc, source, args):
"""<channel>
Admin-only. Shows information about <channel>."""
checkauthenticated(irc, source)
utils.checkAuthenticated(irc, source, allowOper=False)
try:
channel = args[0].lower()
except IndexError:
utils.msg(irc, source, "Error: not enough arguments. Needs 1: channel.")
utils.msg(irc, source, "Error: Not enough arguments. Needs 1: channel.")
return
if channel not in irc.channels:
utils.msg(irc, source, 'Error: unknown channel %r' % channel)
utils.msg(irc, source, 'Error: Unknown channel %r.' % channel)
return
s = ['\x02%s\x02: %s' % (k, v) for k, v in sorted(irc.channels[channel].__dict__.items())]
s = 'Information on channel \x02%s\x02: %s' % (channel, '; '.join(s))
@ -197,14 +153,14 @@ 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."""
checkauthenticated(irc, source)
utils.checkAuthenticated(irc, source, allowOper=False)
try:
modesource, target, modes = args[0], args[1], args[2:]
except IndexError:
utils.msg(irc, source, 'Error: not enough arguments. Needs 3: source nick, target, modes to set.')
utils.msg(irc, source, 'Error: Not enough arguments. Needs 3: source nick, target, modes to set.')
return
if not modes:
utils.msg(irc, source, "Error: no modes given to set!")
utils.msg(irc, source, "Error: No modes given to set!")
return
parsedmodes = utils.parseModes(irc, target, modes)
targetuid = utils.nickToUid(irc, target)
@ -226,25 +182,25 @@ def msg(irc, source, args):
"""<source> <target> <text>
Admin-only. Sends message <text> from <source>, where <source> is the nick of a PyLink client."""
checkauthenticated(irc, source)
utils.checkAuthenticated(irc, source, allowOper=False)
try:
msgsource, target, text = args[0], args[1], ' '.join(args[2:])
except IndexError:
utils.msg(irc, source, 'Error: not enough arguments. Needs 3: source nick, target, text.')
utils.msg(irc, source, 'Error: Not enough arguments. Needs 3: source nick, target, text.')
return
sourceuid = utils.nickToUid(irc, msgsource)
if not sourceuid:
utils.msg(irc, source, 'Error: unknown user %r' % msgsource)
utils.msg(irc, source, 'Error: Unknown user %r.' % msgsource)
return
if not utils.isChannel(target):
real_target = utils.nickToUid(irc, target)
if real_target is None:
utils.msg(irc, source, 'Error: unknown user %r' % target)
utils.msg(irc, source, 'Error: Unknown user %r.' % target)
return
else:
real_target = target
if not text:
utils.msg(irc, source, 'Error: no text given.')
utils.msg(irc, source, 'Error: No text given.')
return
irc.proto.messageClient(irc, sourceuid, real_target, text)
irc.callHooks([sourceuid, 'PYLINK_ADMIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}])

View File

@ -1,11 +1,13 @@
# commands.py: base PyLink commands
import sys
import os
from time import ctime
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils
from conf import conf
from log import log
import world
@utils.add_cmd
def status(irc, source, args):
@ -27,7 +29,7 @@ def identify(irc, source, args):
try:
username, password = args[0], args[1]
except IndexError:
utils.msg(irc, source, 'Error: not enough arguments.')
utils.msg(irc, source, 'Error: Not enough arguments.')
return
# Usernames are case-insensitive, passwords are NOT.
if username.lower() == conf['login']['user'].lower() and password == conf['login']['password']:
@ -37,7 +39,7 @@ def identify(irc, source, args):
log.info("(%s) Successful login to %r by %s.",
irc.name, username, utils.getHostmask(irc, source))
else:
utils.msg(irc, source, 'Incorrect credentials.')
utils.msg(irc, source, 'Error: Incorrect credentials.')
u = irc.users[source]
log.warning("(%s) Failed login to %r from %s.",
irc.name, username, utils.getHostmask(irc, source))
@ -46,8 +48,12 @@ def listcommands(irc, source, args):
"""takes no arguments.
Returns a list of available commands PyLink has to offer."""
cmds = list(utils.bot_commands.keys())
cmds = list(world.bot_commands.keys())
cmds.sort()
for idx, cmd in enumerate(cmds):
nfuncs = len(world.bot_commands[cmd])
if nfuncs > 1:
cmds[idx] = '%s(x%s)' % (cmd, nfuncs)
utils.msg(irc, source, 'Available commands include: %s' % ', '.join(cmds))
utils.msg(irc, source, 'To see help on a specific command, type \x02help <command>\x02.')
utils.add_cmd(listcommands, 'list')
@ -62,20 +68,60 @@ def help(irc, source, args):
except IndexError: # No argument given, just return 'list' output
listcommands(irc, source, args)
return
try:
func = utils.bot_commands[command]
except KeyError:
utils.msg(irc, source, 'Error: no such command %r.' % command)
if command not in world.bot_commands:
utils.msg(irc, source, 'Error: Unknown command %r.' % command)
return
else:
funcs = world.bot_commands[command]
if len(funcs) > 1:
utils.msg(irc, source, 'The following \x02%s\x02 plugins bind to the \x02%s\x02 command: %s'
% (len(funcs), command, ', '.join([func.__module__ for func in funcs])))
for func in funcs:
doc = func.__doc__
mod = func.__module__
if doc:
lines = doc.split('\n')
# Bold the first line, which usually just tells you what
# arguments the command takes.
lines[0] = '\x02%s %s\x02' % (command, lines[0])
lines[0] = '\x02%s %s\x02 (plugin: %r)' % (command, lines[0], mod)
for line in lines:
utils.msg(irc, source, line.strip())
else:
utils.msg(irc, source, 'Error: Command %r doesn\'t offer any help.' % command)
utils.msg(irc, source, "Error: Command %r (from plugin %r) "
"doesn't offer any help." % (command, mod))
return
@utils.add_cmd
def showuser(irc, source, args):
"""<user>
Shows information about <user>."""
try:
target = args[0]
except IndexError:
utils.msg(irc, source, "Error: Not enough arguments. Needs 1: nick.")
return
u = utils.nickToUid(irc, target) or target
# Only show private info if the person is calling 'showuser' on themselves,
# or is an oper.
verbose = utils.isOper(irc, source) or u == source
if u not in irc.users:
utils.msg(irc, source, 'Error: Unknown user %r.' % target)
return
f = lambda s: utils.msg(irc, source, s)
userobj = irc.users[u]
f('Information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident,
userobj.host, userobj.realname))
sid = utils.clientToServer(irc, u)
serverobj = irc.servers[sid]
ts = userobj.ts
f('\x02Home server\x02: %s (%s); \x02Signon time:\x02 %s (%s)' % \
(serverobj.name, sid, ctime(float(ts)), ts))
if verbose:
f('\x02Protocol UID\x02: %s; \x02PyLink identification\x02: %s' % \
(u, userobj.identified))
f('\x02User modes\x02: %s' % utils.joinModes(userobj.modes))
f('\x02Real host\x02: %s; \x02IP\x02: %s; \x02Away status\x02: %s' % \
(userobj.realhost, userobj.ip, userobj.away or '\x1D(not set)\x1D'))
f('\x02Channels\x02: %s' % (' '.join(userobj.channels).strip() or '\x1D(none)\x1D'))

21
plugins/exec.py Executable file
View File

@ -0,0 +1,21 @@
# exec.py: Provides an 'exec' command to execute raw code
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils
from log import log
def _exec(irc, source, args):
"""<code>
Admin-only. Executes <code> in the current PyLink instance.
\x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02"""
utils.checkAuthenticated(irc, source, allowOper=False)
args = ' '.join(args)
if not args.strip():
utils.msg(irc, source, 'No code entered!')
return
log.info('(%s) Executing %r for %s', irc.name, args, utils.getHostmask(irc, source))
exec(args, globals(), locals())
utils.add_cmd(_exec, 'exec')

View File

@ -13,6 +13,7 @@ from expiringdict import ExpiringDict
import utils
from log import log
from conf import confname
import world
dbname = "pylinkrelay"
if confname != 'pylink':
@ -28,11 +29,11 @@ def relayWhoisHandlers(irc, target):
orig = getLocalUser(irc, target)
if orig:
network, remoteuid = orig
remotenick = utils.networkobjects[network].users[remoteuid].nick
remotenick = world.networkobjects[network].users[remoteuid].nick
return [320, "%s :is a remote user connected via PyLink Relay. Home "
"network: %s; Home nick: %s" % (user.nick, network,
remotenick)]
utils.whois_handlers.append(relayWhoisHandlers)
world.whois_handlers.append(relayWhoisHandlers)
def normalizeNick(irc, netname, nick, separator=None, uid=''):
separator = separator or irc.serverdata.get('separator') or "/"
@ -94,7 +95,7 @@ def loadDB():
db = {}
def exportDB(reschedule=False):
scheduler = utils.schedulers.get('relaydb')
scheduler = world.schedulers.get('relaydb')
if reschedule and scheduler:
scheduler.enter(30, 1, exportDB, argument=(True,))
log.debug("Relay: exporting links database to %s", dbname)
@ -110,7 +111,7 @@ def save(irc, source, args):
exportDB()
utils.msg(irc, source, 'Done.')
else:
utils.msg(irc, source, 'Error: you are not authenticated!')
utils.msg(irc, source, 'Error: You are not authenticated!')
return
def getPrefixModes(irc, remoteirc, channel, user):
@ -149,16 +150,48 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
host = userobj.host[:64]
realname = userobj.realname
modes = getSupportedUmodes(irc, remoteirc, userobj.modes)
opertype = ''
if ('o', None) in userobj.modes:
if hasattr(userobj, 'opertype'):
# InspIRCd's special OPERTYPE command; this is mandatory
# and setting of umode +/-o will fail unless this
# is used instead. This also sets an oper type for
# the user, which is used in WHOIS, etc.
# If an opertype exists for the user, add " (remote)"
# for the relayed clone, so that it shows in whois.
# Janus does this too. :)
# OPERTYPE uses underscores instead of spaces, FYI.
log.debug('(%s) relay.getRemoteUser: setting OPERTYPE of client for %r to %s',
irc.name, user, userobj.opertype)
opertype = userobj.opertype + '_(remote)'
else:
opertype = 'IRC_Operator_(remote)'
# Set hideoper on remote opers, to prevent inflating
# /lusers and various /stats
hideoper_mode = remoteirc.umodes.get('hideoper')
if hideoper_mode:
modes.append((hideoper_mode, None))
u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident,
host=host, realname=realname,
modes=modes, ts=userobj.ts).uid
modes=modes, ts=userobj.ts,
opertype=opertype).uid
remoteirc.users[u].remote = (irc.name, user)
remoteirc.users[u].opertype = opertype
away = userobj.away
if away:
remoteirc.proto.awayClient(remoteirc, u, away)
relayusers[(irc.name, user)][remoteirc.name] = u
return u
def handle_operup(irc, numeric, command, args):
newtype = args['text'] + '_(remote)'
for netname, user in relayusers[(irc.name, numeric)].items():
log.debug('(%s) relay.handle_opertype: setting OPERTYPE of %s/%s to %s', irc.name, user, netname, newtype)
remoteirc = world.networkobjects[netname]
remoteirc.users[user].opertype = newtype
utils.add_hook(handle_operup, 'PYLINK_CLIENT_OPERED')
def getLocalUser(irc, user, targetirc=None):
"""<irc object> <pseudoclient uid> [<target irc object>]
@ -179,7 +212,7 @@ def getLocalUser(irc, user, targetirc=None):
# If targetirc is given, we'll return simply the UID of the user on the
# target network, if it exists. Otherwise, we'll return a tuple
# with the home network name and the original user's UID.
sourceobj = utils.networkobjects.get(remoteuser[0])
sourceobj = world.networkobjects.get(remoteuser[0])
if targetirc and sourceobj:
if remoteuser[0] == targetirc.name:
# The user we found's home network happens to be the one being
@ -230,7 +263,7 @@ def initializeChannel(irc, channel):
remotenet, remotechan = link
if remotenet == irc.name:
continue
remoteirc = utils.networkobjects.get(remotenet)
remoteirc = world.networkobjects.get(remotenet)
if remoteirc is None:
continue
rc = remoteirc.channels[remotechan]
@ -255,12 +288,12 @@ def handle_join(irc, numeric, command, args):
return
ts = args['ts']
users = set(args['users'])
relayJoins(irc, channel, users, ts)
relayJoins(irc, channel, users, ts, burst=False)
utils.add_hook(handle_join, 'JOIN')
def handle_quit(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].copy().items():
remoteirc = utils.networkobjects[netname]
remoteirc = world.networkobjects[netname]
remoteirc.proto.quitClient(remoteirc, user, args['text'])
del relayusers[(irc.name, numeric)]
utils.add_hook(handle_quit, 'QUIT')
@ -274,7 +307,7 @@ utils.add_hook(handle_squit, 'SQUIT')
def handle_nick(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname]
remoteirc = world.networkobjects[netname]
newnick = normalizeNick(remoteirc, irc.name, args['newnick'], uid=user)
if remoteirc.users[user].nick != newnick:
remoteirc.proto.nickClient(remoteirc, user, newnick)
@ -288,7 +321,7 @@ def handle_part(irc, numeric, command, args):
return
for channel in channels:
for netname, user in relayusers[(irc.name, numeric)].copy().items():
remoteirc = utils.networkobjects[netname]
remoteirc = world.networkobjects[netname]
remotechan = findRemoteChan(irc, remoteirc, channel)
if remotechan is None:
continue
@ -324,7 +357,7 @@ def handle_privmsg(irc, numeric, command, args):
return
if utils.isChannel(target):
for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname]
remoteirc = world.networkobjects[netname]
real_target = findRemoteChan(irc, remoteirc, target)
if not real_target:
continue
@ -343,11 +376,11 @@ def handle_privmsg(irc, numeric, command, args):
# on the remote network, and we won't have anything to send our
# messages from.
if homenet not in remoteusers.keys():
utils.msg(irc, numeric, 'Error: you must be in a common channel '
utils.msg(irc, numeric, 'Error: You must be in a common channel '
'with %r in order to send messages.' % \
irc.users[target].nick, notice=True)
return
remoteirc = utils.networkobjects[homenet]
remoteirc = world.networkobjects[homenet]
user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
if notice:
remoteirc.proto.noticeClient(remoteirc, user, real_target, text)
@ -367,7 +400,7 @@ def handle_kick(irc, source, command, args):
if relay is None or target == irc.pseudoclient.uid:
return
origuser = getLocalUser(irc, target)
for name, remoteirc in utils.networkobjects.items():
for name, remoteirc in world.networkobjects.items():
if irc.name == name or not remoteirc.connected.is_set():
continue
remotechan = findRemoteChan(irc, remoteirc, channel)
@ -400,14 +433,14 @@ def handle_kick(irc, source, command, args):
# Join the kicked client back with its respective modes.
irc.proto.sjoinServer(irc, irc.sid, channel, [(modes, target)])
if kicker in irc.users:
log.info('(%s) Blocked KICK (reason %r) from %s to relay client %s/%s on %s.',
log.info('(%s) Relay claim: Blocked KICK (reason %r) from %s to relay client %s/%s on %s.',
irc.name, args['text'], irc.users[source].nick,
remoteirc.users[real_target].nick, remoteirc.name, channel)
utils.msg(irc, kicker, "This channel is claimed; your kick to "
"%s has been blocked because you are not "
"(half)opped." % channel, notice=True)
else:
log.info('(%s) Blocked KICK (reason %r) from server %s to relay client %s/%s on %s.',
log.info('(%s) Relay claim: Blocked KICK (reason %r) from server %s to relay client %s/%s on %s.',
irc.name, args['text'], irc.servers[source].name,
remoteirc.users[real_target].nick, remoteirc.name, channel)
return
@ -458,7 +491,7 @@ def handle_chgclient(irc, source, command, args):
text = args['newgecos']
if field:
for netname, user in relayusers[(irc.name, target)].items():
remoteirc = utils.networkobjects[netname]
remoteirc = world.networkobjects[netname]
try:
remoteirc.proto.updateClient(remoteirc, user, field, text)
except NotImplementedError: # IRCd doesn't support changing the field we want
@ -579,22 +612,30 @@ def getSupportedUmodes(irc, remoteirc, modes):
else:
log.debug("(%s) getSupportedUmodes: skipping mode (%r, %r) because "
"the remote network (%s)'s IRCd (%s) doesn't support it.",
irc.name, modechar, arg, remoteirc.name, irc.proto.__name__)
irc.name, modechar, arg, remoteirc.name,
remoteirc.proto.__name__)
return supported_modes
def handle_mode(irc, numeric, command, args):
target = args['target']
modes = args['modes']
for name, remoteirc in utils.networkobjects.items():
for name, remoteirc in world.networkobjects.items():
if irc.name == name or not remoteirc.connected.is_set():
continue
if utils.isChannel(target):
relayModes(irc, remoteirc, numeric, target, modes)
else:
# Set hideoper on remote opers, to prevent inflating
# /lusers and various /stats
hideoper_mode = remoteirc.umodes.get('hideoper')
modes = getSupportedUmodes(irc, remoteirc, modes)
if hideoper_mode:
if ('+o', None) in modes:
modes.append(('+%s' % hideoper_mode, None))
elif ('-o', None) in modes:
modes.append(('-%s' % hideoper_mode, None))
remoteuser = getRemoteUser(irc, remoteirc, target, spawnIfMissing=False)
if remoteuser is None:
continue
if remoteuser and modes:
remoteirc.proto.modeClient(remoteirc, remoteuser, remoteuser, modes)
utils.add_hook(handle_mode, 'MODE')
@ -602,7 +643,7 @@ utils.add_hook(handle_mode, 'MODE')
def handle_topic(irc, numeric, command, args):
channel = args['channel']
topic = args['topic']
for name, remoteirc in utils.networkobjects.items():
for name, remoteirc in world.networkobjects.items():
if irc.name == name or not remoteirc.connected.is_set():
continue
@ -628,7 +669,7 @@ def handle_kill(irc, numeric, command, args):
# We don't allow killing over the relay, so we must respawn the affected
# client and rejoin it to its channels.
del relayusers[realuser][irc.name]
remoteirc = utils.networkobjects[realuser[0]]
remoteirc = world.networkobjects[realuser[0]]
for remotechan in remoteirc.channels.copy():
localchan = findRemoteChan(remoteirc, irc, remotechan)
if localchan:
@ -637,7 +678,7 @@ def handle_kill(irc, numeric, command, args):
client = getRemoteUser(remoteirc, irc, realuser[1])
irc.proto.sjoinServer(irc, irc.sid, localchan, [(modes, client)])
if userdata and numeric in irc.users:
log.info('(%s) Blocked KILL (reason %r) from %s to relay client %s/%s.',
log.info('(%s) Relay claim: Blocked KILL (reason %r) from %s to relay client %s/%s.',
irc.name, args['text'], irc.users[numeric].nick,
remoteirc.users[realuser[1]].nick, realuser[0])
utils.msg(irc, numeric, "Your kill to %s has been blocked "
@ -645,7 +686,7 @@ def handle_kill(irc, numeric, command, args):
" users over the relay at this time." % \
userdata.nick, notice=True)
else:
log.info('(%s) Blocked KILL (reason %r) from server %s to relay client %s/%s.',
log.info('(%s) Relay claim: Blocked KILL (reason %r) from server %s to relay client %s/%s.',
irc.name, args['text'], irc.servers[numeric].name,
remoteirc.users[realuser[1]].nick, realuser[0])
# Target user was local.
@ -670,7 +711,7 @@ def isRelayClient(irc, user):
return False
def relayJoins(irc, channel, users, ts, burst=True):
for name, remoteirc in utils.networkobjects.items():
for name, remoteirc in world.networkobjects.items():
queued_users = []
if name == irc.name or not remoteirc.connected.is_set():
# Don't relay things to their source network...
@ -710,7 +751,7 @@ def relayJoins(irc, channel, users, ts, burst=True):
remoteirc.proto.joinClient(remoteirc, queued_users[0][1], remotechan)
def relayPart(irc, channel, user):
for name, remoteirc in utils.networkobjects.items():
for name, remoteirc in world.networkobjects.items():
if name == irc.name or not remoteirc.connected.is_set():
# Don't relay things to their source network...
continue
@ -730,7 +771,7 @@ def removeChannel(irc, channel):
if irc is None:
return
if channel not in map(str.lower, irc.serverdata['channels']):
irc.proto.partClient(irc, irc.pseudoclient.uid, channel)
irc.proto.partClient(irc, irc.pseudoclient.uid, channel, 'Channel delinked.')
relay = findRelay((irc.name, channel))
if relay:
for user in irc.channels[channel].users.copy():
@ -756,16 +797,16 @@ def create(irc, source, args):
try:
channel = utils.toLower(irc, args[0])
except IndexError:
utils.msg(irc, source, "Error: not enough arguments. Needs 1: channel.")
utils.msg(irc, source, "Error: Not enough arguments. Needs 1: channel.")
return
if not utils.isChannel(channel):
utils.msg(irc, source, 'Error: invalid channel %r.' % channel)
utils.msg(irc, source, 'Error: Invalid channel %r.' % channel)
return
if source not in irc.channels[channel].users:
utils.msg(irc, source, 'Error: you must be in %r to complete this operation.' % channel)
utils.msg(irc, source, 'Error: You must be in %r to complete this operation.' % channel)
return
if not utils.isOper(irc, source):
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.')
return
db[(irc.name, channel)] = {'claim': [irc.name], 'links': set(), 'blocked_nets': set()}
initializeChannel(irc, channel)
@ -779,24 +820,24 @@ def destroy(irc, source, args):
try:
channel = utils.toLower(irc, args[0])
except IndexError:
utils.msg(irc, source, "Error: not enough arguments. Needs 1: channel.")
utils.msg(irc, source, "Error: Not enough arguments. Needs 1: channel.")
return
if not utils.isChannel(channel):
utils.msg(irc, source, 'Error: invalid channel %r.' % channel)
utils.msg(irc, source, 'Error: Invalid channel %r.' % channel)
return
if not utils.isOper(irc, source):
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.')
return
entry = (irc.name, channel)
if entry in db:
for link in db[entry]['links']:
removeChannel(utils.networkobjects.get(link[0]), link[1])
removeChannel(world.networkobjects.get(link[0]), link[1])
removeChannel(irc, channel)
del db[entry]
utils.msg(irc, source, 'Done.')
else:
utils.msg(irc, source, 'Error: no such relay %r exists.' % channel)
utils.msg(irc, source, 'Error: No such relay %r exists.' % channel)
return
@utils.add_cmd
@ -809,7 +850,7 @@ def link(irc, source, args):
channel = utils.toLower(irc, args[1])
remotenet = args[0].lower()
except IndexError:
utils.msg(irc, source, "Error: not enough arguments. Needs 2-3: remote netname, channel, local channel name (optional).")
utils.msg(irc, source, "Error: Not enough arguments. Needs 2-3: remote netname, channel, local channel name (optional).")
return
try:
localchan = utils.toLower(irc, args[2])
@ -817,33 +858,33 @@ def link(irc, source, args):
localchan = channel
for c in (channel, localchan):
if not utils.isChannel(c):
utils.msg(irc, source, 'Error: invalid channel %r.' % c)
utils.msg(irc, source, 'Error: Invalid channel %r.' % c)
return
if source not in irc.channels[localchan].users:
utils.msg(irc, source, 'Error: you must be in %r to complete this operation.' % localchan)
utils.msg(irc, source, 'Error: You must be in %r to complete this operation.' % localchan)
return
if not utils.isOper(irc, source):
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.')
return
if remotenet not in utils.networkobjects:
utils.msg(irc, source, 'Error: no network named %r exists.' % remotenet)
if remotenet not in world.networkobjects:
utils.msg(irc, source, 'Error: No network named %r exists.' % remotenet)
return
localentry = findRelay((irc.name, localchan))
if localentry:
utils.msg(irc, source, 'Error: channel %r is already part of a relay.' % localchan)
utils.msg(irc, source, 'Error: Channel %r is already part of a relay.' % localchan)
return
try:
entry = db[(remotenet, channel)]
except KeyError:
utils.msg(irc, source, 'Error: no such relay %r exists.' % channel)
utils.msg(irc, source, 'Error: No such relay %r exists.' % channel)
return
else:
if irc.name in entry['blocked_nets']:
utils.msg(irc, source, 'Error: access denied (network is banned from linking to this channel).')
utils.msg(irc, source, 'Error: Access denied (network is banned from linking to this channel).')
return
for link in entry['links']:
if link[0] == irc.name:
utils.msg(irc, source, "Error: remote channel '%s%s' is already"
utils.msg(irc, source, "Error: Remote channel '%s%s' is already"
" linked here as %r." % (remotenet,
channel, link[1]))
return
@ -860,17 +901,17 @@ def delink(irc, source, args):
try:
channel = utils.toLower(irc, args[0])
except IndexError:
utils.msg(irc, source, "Error: not enough arguments. Needs 1-2: channel, remote netname (optional).")
utils.msg(irc, source, "Error: Not enough arguments. Needs 1-2: channel, remote netname (optional).")
return
try:
remotenet = args[1].lower()
except IndexError:
remotenet = None
if not utils.isOper(irc, source):
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.')
return
if not utils.isChannel(channel):
utils.msg(irc, source, 'Error: invalid channel %r.' % channel)
utils.msg(irc, source, 'Error: Invalid channel %r.' % channel)
return
entry = findRelay((irc.name, channel))
if entry:
@ -884,18 +925,18 @@ def delink(irc, source, args):
else:
for link in db[entry]['links'].copy():
if link[0] == remotenet:
removeChannel(utils.networkobjects.get(remotenet), link[1])
removeChannel(world.networkobjects.get(remotenet), link[1])
db[entry]['links'].remove(link)
else:
removeChannel(irc, channel)
db[entry]['links'].remove((irc.name, channel))
utils.msg(irc, source, 'Done.')
else:
utils.msg(irc, source, 'Error: no such relay %r.' % channel)
utils.msg(irc, source, 'Error: No such relay %r.' % channel)
def initializeAll(irc):
log.debug('(%s) initializeAll: waiting for utils.started', irc.name)
utils.started.wait()
log.debug('(%s) initializeAll: waiting for world.started', irc.name)
world.started.wait()
for chanpair, entrydata in db.items():
network, channel = chanpair
initializeChannel(irc, channel)
@ -905,7 +946,7 @@ def initializeAll(irc):
def main():
loadDB()
utils.schedulers['relaydb'] = scheduler = sched.scheduler()
world.schedulers['relaydb'] = scheduler = sched.scheduler()
scheduler.enter(30, 1, exportDB, argument=(True,))
# Thread this because exportDB() queues itself as part of its
# execution, in order to get a repeating loop.
@ -937,7 +978,7 @@ def handle_save(irc, numeric, command, args):
# It's one of our relay clients; try to fix our nick to the next
# available normalized nick.
remotenet, remoteuser = realuser
remoteirc = utils.networkobjects[remotenet]
remoteirc = world.networkobjects[remotenet]
nick = remoteirc.users[remoteuser].nick
# Limit how many times we can attempt to fix our nick, to prevent
# floods and such.
@ -963,7 +1004,7 @@ def linked(irc, source, args):
"""takes no arguments.
Returns a list of channels shared across the relay."""
networks = list(utils.networkobjects.keys())
networks = list(world.networkobjects.keys())
networks.remove(irc.name)
s = 'Connected networks: \x02%s\x02 %s' % (irc.name, ' '.join(networks))
utils.msg(irc, source, s)
@ -978,7 +1019,7 @@ def linked(irc, source, args):
def handle_away(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname]
remoteirc = world.networkobjects[netname]
remoteirc.proto.awayClient(remoteirc, user, args['text'])
utils.add_hook(handle_away, 'AWAY')
@ -989,15 +1030,37 @@ def handle_spawnmain(irc, numeric, command, args):
initializeAll(irc)
utils.add_hook(handle_spawnmain, 'PYLINK_SPAWNMAIN')
def handle_invite(irc, source, command, args):
target = args['target']
channel = args['channel']
if isRelayClient(irc, target):
remotenet, remoteuser = getLocalUser(irc, target)
remoteirc = world.networkobjects[remotenet]
remotechan = findRemoteChan(irc, remoteirc, channel)
remotesource = getRemoteUser(irc, remoteirc, source, spawnIfMissing=False)
if remotesource is None:
utils.msg(irc, source, 'Error: You must be in a common channel '
'with %s to invite them to channels.' % \
irc.users[target].nick,
notice=True)
elif remotechan is None:
utils.msg(irc, source, 'Error: You cannot invite someone to a '
'channel not on their network!',
notice=True)
else:
remoteirc.proto.inviteClient(remoteirc, remotesource, remoteuser,
remotechan)
utils.add_hook(handle_invite, 'INVITE')
@utils.add_cmd
def linkacl(irc, source, args):
"""ALLOW|DENY|LIST <channel> <remotenet>
Allows blocking / unblocking certain networks from linking to a relay, based on a blacklist.
LINKACL LIST returns a list of blocked networks for a channel, while the ALLOW and DENY subcommands allow manipulating this blacklist."""
missingargs = "Error: not enough arguments. Needs 2-3: subcommand (ALLOW/DENY/LIST), channel, remote network (for ALLOW/DENY)."
missingargs = "Error: Not enough arguments. Needs 2-3: subcommand (ALLOW/DENY/LIST), channel, remote network (for ALLOW/DENY)."
if not utils.isOper(irc, source):
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.')
return
try:
cmd = args[0].lower()
@ -1006,11 +1069,11 @@ def linkacl(irc, source, args):
utils.msg(irc, source, missingargs)
return
if not utils.isChannel(channel):
utils.msg(irc, source, 'Error: invalid channel %r.' % channel)
utils.msg(irc, source, 'Error: Invalid channel %r.' % channel)
return
relay = findRelay((irc.name, channel))
if not relay:
utils.msg(irc, source, 'Error: no such relay %r exists.' % channel)
utils.msg(irc, source, 'Error: No such relay %r exists.' % channel)
return
if cmd == 'list':
s = 'Blocked networks for \x02%s\x02: \x02%s\x02' % (channel, ', '.join(db[relay]['blocked_nets']) or '(empty)')
@ -1029,8 +1092,44 @@ def linkacl(irc, source, args):
try:
db[relay]['blocked_nets'].remove(remotenet)
except KeyError:
utils.msg(irc, source, 'Error: network %r is not on the blacklist for %r.' % (remotenet, channel))
utils.msg(irc, source, 'Error: Network %r is not on the blacklist for %r.' % (remotenet, channel))
else:
utils.msg(irc, source, 'Done.')
else:
utils.msg(irc, source, 'Error: unknown subcommand %r: valid ones are ALLOW, DENY, and LIST.' % cmd)
utils.msg(irc, source, 'Error: Unknown subcommand %r: valid ones are ALLOW, DENY, and LIST.' % cmd)
@utils.add_cmd
def showuser(irc, source, args):
"""<user>
Shows relay data about user <user>. This is intended to be used alongside the 'commands' plugin, which provides a 'showuser' command with more general information."""
try:
target = args[0]
except IndexError:
# No errors here; showuser from the commands plugin already does this
# for us.
return
u = utils.nickToUid(irc, target)
if u:
try:
userpair = getLocalUser(irc, u) or (irc.name, u)
remoteusers = relayusers[userpair].items()
except KeyError:
pass
else:
nicks = []
if remoteusers:
nicks.append('%s (home network): \x02%s\x02' % (userpair[0],
world.networkobjects[userpair[0]].users[userpair[1]].nick))
for r in remoteusers:
remotenet, remoteuser = r
remoteirc = world.networkobjects[remotenet]
nicks.append('%s: \x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick))
utils.msg(irc, source, "\x02Relay nicks\x02: %s" % ', '.join(nicks))
relaychannels = []
for ch in irc.users[u].channels:
relay = findRelay((irc.name, ch))
if relay:
relaychannels.append(''.join(relay))
if relaychannels and (utils.isOper(irc, source) or u == source):
utils.msg(irc, source, "\x02Relay channels\x02: %s" % ' '.join(relaychannels))

View File

@ -39,7 +39,7 @@ def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set()
u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip)
utils.applyModes(irc, uid, modes)
irc.servers[server].users.append(uid)
irc.servers[server].users.add(uid)
_send(irc, server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}"
" {ts} {modes} + :{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid,
@ -138,7 +138,7 @@ def removeClient(irc, numeric):
log.debug('Removing client %s from irc.users', numeric)
del irc.users[numeric]
log.debug('Removing client %s from irc.servers[%s]', numeric, sid)
irc.servers[sid].users.remove(numeric)
irc.servers[sid].users.discard(numeric)
def quitClient(irc, numeric, reason):
"""<irc object> <client numeric>
@ -195,6 +195,7 @@ def _operUp(irc, target, opertype=None):
otype = 'IRC_Operator'
log.debug('(%s) Sending OPERTYPE from %s to oper them up.',
irc.name, target)
userobj.opertype = otype
_send(irc, target, 'OPERTYPE %s' % otype)
def _sendModes(irc, numeric, target, modes, ts=None):
@ -449,7 +450,7 @@ def handle_uid(irc, numeric, command, args):
parsedmodes = utils.parseModes(irc, uid, [args[8], args[9]])
log.debug('Applying modes %s for %s', parsedmodes, uid)
utils.applyModes(irc, uid, parsedmodes)
irc.servers[numeric].users.append(uid)
irc.servers[numeric].users.add(uid)
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
def handle_quit(irc, numeric, command, args):
@ -706,7 +707,10 @@ def handle_opertype(irc, numeric, command, args):
omode = [('+o', None)]
irc.users[numeric].opertype = opertype = args[0]
utils.applyModes(irc, numeric, omode)
return {'target': numeric, 'modes': omode, 'text': opertype}
# OPERTYPE is essentially umode +o and metadata in one command;
# we'll call that too.
irc.callHooks([numeric, 'PYLINK_CLIENT_OPERED', {'text': opertype}])
return {'target': numeric, 'modes': omode}
def handle_fident(irc, numeric, command, args):
# :70MAAAAAB FHOST test

View File

@ -42,7 +42,7 @@ def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set()
u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip)
utils.applyModes(irc, uid, modes)
irc.servers[server].users.append(uid)
irc.servers[server].users.add(uid)
_send(irc, server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
"{realhost} * :{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid,
@ -469,7 +469,10 @@ def handle_euid(irc, numeric, command, args):
parsedmodes = utils.parseModes(irc, uid, [modes])
log.debug('Applying modes %s for %s', parsedmodes, uid)
utils.applyModes(irc, uid, parsedmodes)
irc.servers[numeric].users.append(uid)
irc.servers[numeric].users.add(uid)
if ('o', None) in parsedmodes:
otype = 'Server_Administrator' if ('a', None) in parsedmodes else 'IRC_Operator'
irc.callHooks([uid, 'PYLINK_CLIENT_OPERED', {'text': otype}])
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
def handle_uid(irc, numeric, command, args):
@ -502,6 +505,17 @@ def handle_tmode(irc, numeric, command, args):
ts = int(args[0])
return {'target': channel, 'modes': changedmodes, 'ts': ts}
def handle_mode(irc, numeric, command, args):
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc
target = args[0]
modestrings = args[1:]
changedmodes = utils.parseModes(irc, numeric, modestrings)
utils.applyModes(irc, target, changedmodes)
if ('+o', None) in changedmodes:
otype = 'Server_Administrator' if ('a', None) in irc.users[target].modes else 'IRC_Operator'
irc.callHooks([target, 'PYLINK_CLIENT_OPERED', {'text': otype}])
return {'target': target, 'modes': changedmodes}
def handle_events(irc, data):
# TS6 messages:
# :42X COMMAND arg1 arg2 :final long arg

21
runtests.py Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
import unittest
import glob
import os
import sys
runner = unittest.TextTestRunner(verbosity=2)
fails = []
suites = []
# Yay, import hacks!
sys.path.append(os.path.join(os.getcwd(), 'tests'))
for testfile in glob.glob('tests/test_*.py'):
# Strip the tests/ and .py extension: tests/test_whatever.py => test_whatever
module = testfile.replace('.py', '').replace('tests/', '')
module = __import__(module)
suites.append(unittest.defaultTestLoader.loadTestsFromModule(module))
testsuite = unittest.TestSuite(suites)
runner.run(testsuite)

35
tests/test_coreplugin.py Normal file
View File

@ -0,0 +1,35 @@
#!/usr/bin/env python
import inspircd
import unittest
import world
import coreplugin
import tests_common
world.testing = inspircd
class CorePluginTestCase(tests_common.PluginTestCase):
@unittest.skip("Test doesn't work yet.")
def testKillRespawn(self):
self.irc.run(':9PY KILL {u} :test'.format(u=self.u))
hooks = self.irc.takeHooks()
# Make sure we're respawning our PseudoClient when its killed
print(hooks)
spmain = [h for h in hooks if h[1] == 'PYLINK_SPAWNMAIN']
self.assertTrue(spmain, 'PYLINK_SPAWNMAIN hook was never sent!')
msgs = self.irc.takeMsgs()
commands = self.irc.takeCommands(msgs)
self.assertIn('UID', commands)
self.assertIn('FJOIN', commands)
# Also make sure that we're updating the irc.pseudoclient field
self.assertNotEqual(self.irc.pseudoclient.uid, spmain[0]['olduser'])
def testKickrejoin(self):
self.proto.kickClient(self.irc, self.u, '#pylink', self.u, 'test')
msgs = self.irc.takeMsgs()
commands = self.irc.takeCommands(msgs)
self.assertIn('FJOIN', commands)

View File

@ -1,15 +1,13 @@
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from log import log
import classes
import unittest
# Yes, we're going to even test the testing classes. Testception? I think so.
class TestFakeIRC(unittest.TestCase):
def setUp(self):
self.irc = classes.FakeIRC('unittest', classes.FakeProto(), classes.testconf)
self.irc = classes.FakeIRC('unittest', classes.FakeProto())
def testFakeIRC(self):
self.irc.run('this should do nothing')

View File

@ -2,31 +2,30 @@ import sys
import os
sys.path += [os.getcwd(), os.path.join(os.getcwd(), 'protocols')]
import unittest
from collections import defaultdict
import inspircd
import classes
import utils
import coreplugin
import world
class TestProtoInspIRCd(unittest.TestCase):
def setUp(self):
self.irc = classes.FakeIRC('unittest', inspircd, classes.testconf)
self.proto = self.irc.proto
self.sdata = self.irc.serverdata
# This is to initialize ourself as an internal PseudoServer, so we can spawn clients
self.proto.connect(self.irc)
self.u = self.irc.pseudoclient.uid
self.maxDiff = None
utils.command_hooks = defaultdict(list)
import tests_common
world.testing = inspircd
class InspIRCdTestCase(tests_common.CommonProtoTestCase):
def testCheckRecvpass(self):
# Correct recvpass here.
self.irc.run('SERVER somehow.someday abcd 0 0AL :Somehow Server - McMurdo Station, Antarctica')
# Incorrect recvpass here; should raise ProtocolError.
self.assertRaises(classes.ProtocolError, self.irc.run, 'SERVER somehow.someday BADPASS 0 0AL :Somehow Server - McMurdo Station, Antarctica')
def testConnect(self):
self.proto.connect(self.irc)
initial_messages = self.irc.takeMsgs()
commands = self.irc.takeCommands(initial_messages)
# SERVER pylink.unittest abcd 0 9PY :PyLink Service
serverline = 'SERVER %s %s 0 %s :PyLink Service' % (
self.sdata['hostname'], self.sdata['sendpass'], self.sdata['sid'])
serverline = 'SERVER %s %s 0 %s :%s' % (
self.sdata['hostname'], self.sdata['sendpass'], self.sdata['sid'],
self.irc.botdata['serverdesc'])
self.assertIn(serverline, initial_messages)
self.assertIn('BURST', commands)
self.assertIn('ENDBURST', commands)
@ -34,88 +33,12 @@ class TestProtoInspIRCd(unittest.TestCase):
self.assertIn('UID', commands)
self.assertIn('FJOIN', commands)
def testCheckRecvpass(self):
# Correct recvpass here.
self.irc.run('SERVER somehow.someday abcd 0 0AL :Somehow Server - McMurdo Station, Antarctica')
# Incorrect recvpass here; should raise ProtocolError.
self.assertRaises(classes.ProtocolError, self.irc.run, 'SERVER somehow.someday BADPASS 0 0AL :Somehow Server - McMurdo Station, Antarctica')
def testSpawnClient(self):
u = self.proto.spawnClient(self.irc, 'testuser3', 'moo', 'hello.world').uid
# Check the server index and the user index
self.assertIn(u, self.irc.servers[self.irc.sid].users)
self.assertIn(u, self.irc.users)
# Raise ValueError when trying to spawn a client on a server that's not ours
self.assertRaises(ValueError, self.proto.spawnClient, self.irc, 'abcd', 'user', 'dummy.user.net', server='44A')
# Unfilled args should get placeholder fields and not error.
self.proto.spawnClient(self.irc, 'testuser4')
def testJoinClient(self):
u = self.u
self.proto.joinClient(self.irc, u, '#Channel')
self.assertIn(u, self.irc.channels['#channel'].users)
# Non-existant user.
self.assertRaises(LookupError, self.proto.joinClient, self.irc, '9PYZZZZZZ', '#test')
def testPartClient(self):
u = self.u
self.proto.joinClient(self.irc, u, '#channel')
self.proto.partClient(self.irc, u, '#channel')
self.assertNotIn(u, self.irc.channels['#channel'].users)
def testQuitClient(self):
u = self.proto.spawnClient(self.irc, 'testuser3', 'moo', 'hello.world').uid
self.proto.joinClient(self.irc, u, '#channel')
self.assertRaises(LookupError, self.proto.quitClient, self.irc, '9PYZZZZZZ', 'quit reason')
self.proto.quitClient(self.irc, u, 'quit reason')
self.assertNotIn(u, self.irc.channels['#channel'].users)
self.assertNotIn(u, self.irc.users)
self.assertNotIn(u, self.irc.servers[self.irc.sid].users)
def testKickClient(self):
target = self.proto.spawnClient(self.irc, 'soccerball', 'soccerball', 'abcd').uid
self.proto.joinClient(self.irc, target, '#pylink')
self.assertIn(self.u, self.irc.channels['#pylink'].users)
self.assertIn(target, self.irc.channels['#pylink'].users)
self.proto.kickClient(self.irc, self.u, '#pylink', target, 'Pow!')
self.assertNotIn(target, self.irc.channels['#pylink'].users)
def testNickClient(self):
self.proto.nickClient(self.irc, self.u, 'NotPyLink')
self.assertEqual('NotPyLink', self.irc.users[self.u].nick)
def testModeClient(self):
testuser = self.proto.spawnClient(self.irc, 'testcakes')
self.irc.takeMsgs()
self.proto.modeClient(self.irc, self.u, testuser.uid, [('+i', None), ('+w', None)])
self.assertEqual({('i', None), ('w', None)}, testuser.modes)
self.proto.modeClient(self.irc, self.u, '#pylink', [('+s', None), ('+l', '30')])
self.assertEqual({('s', None), ('l', '30')}, self.irc.channels['#pylink'].modes)
cmds = self.irc.takeCommands(self.irc.takeMsgs())
self.assertEqual(cmds, ['MODE', 'FMODE'])
def testSpawnServer(self):
# Incorrect SID length
self.assertRaises(Exception, self.proto.spawnServer, self.irc, 'subserver.pylink', '34Q0')
self.proto.spawnServer(self.irc, 'subserver.pylink', '34Q')
# Duplicate server name
self.assertRaises(Exception, self.proto.spawnServer, self.irc, 'Subserver.PyLink', '34Z')
# Duplicate SID
self.assertRaises(Exception, self.proto.spawnServer, self.irc, 'another.Subserver.PyLink', '34Q')
self.assertIn('34Q', self.irc.servers)
super(InspIRCdTestCase, self).testSpawnServer()
# Are we bursting properly?
self.assertIn(':34Q ENDBURST', self.irc.takeMsgs())
def testSpawnClientOnServer(self):
self.proto.spawnServer(self.irc, 'subserver.pylink', '34Q')
u = self.proto.spawnClient(self.irc, 'person1', 'person', 'users.overdrive.pw', server='34Q')
# We're spawning clients on the right server, hopefully...
self.assertIn(u.uid, self.irc.servers['34Q'].users)
self.assertNotIn(u.uid, self.irc.servers[self.irc.sid].users)
def testSquit(self):
def testHandleSQuit(self):
# Spawn a messy network map, just because!
self.proto.spawnServer(self.irc, 'level1.pylink', '34P')
self.proto.spawnServer(self.irc, 'level2.pylink', '34Q', uplink='34P')
@ -136,18 +59,6 @@ class TestProtoInspIRCd(unittest.TestCase):
self.assertNotIn('34Q', self.irc.servers)
self.assertNotIn('34Z', self.irc.servers)
def testRSquit(self):
u = self.proto.spawnClient(self.irc, 'person1', 'person', 'users.overdrive.pw')
u.identified = 'admin'
self.proto.spawnServer(self.irc, 'level1.pylink', '34P')
self.irc.run(':%s RSQUIT level1.pylink :some reason' % self.u)
# No SQUIT yet, since the 'PyLink' client isn't identified
self.assertNotIn('SQUIT', self.irc.takeCommands(self.irc.takeMsgs()))
# The one we just spawned however, is.
self.irc.run(':%s RSQUIT level1.pylink :some reason' % u.uid)
self.assertIn('SQUIT', self.irc.takeCommands(self.irc.takeMsgs()))
self.assertNotIn('34P', self.irc.servers)
def testHandleServer(self):
self.irc.run('SERVER whatever.net abcd 0 10X :something')
self.assertIn('10X', self.irc.servers)
@ -157,124 +68,122 @@ class TestProtoInspIRCd(unittest.TestCase):
self.assertEqual('test.server', self.irc.servers['0AL'].name)
def testHandleUID(self):
self.irc.run('SERVER whatever.net abcd 0 10X :something')
self.irc.run(':10X UID 10XAAAAAB 1429934638 GL 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 +Wioswx +ACGKNOQXacfgklnoqvx :realname')
self.assertIn('10XAAAAAB', self.irc.servers['10X'].users)
self.assertIn('10XAAAAAB', self.irc.users)
u = self.irc.users['10XAAAAAB']
self.assertEqual('GL', u.nick)
expected = {'uid': '10XAAAAAB', 'ts': '1429934638', 'nick': 'GL',
'realhost': '0::1', 'ident': 'gl', 'ip': '0::1',
'host': 'hidden-7j810p.9mdf.lrek.0000.0000.IP'}
hookdata = self.irc.takeHooks()[0][-1]
self.assertEqual(hookdata, expected)
def testHandleKill(self):
self.irc.takeMsgs() # Ignore the initial connect messages
utils.add_hook(self.irc.dummyhook, 'KILL')
olduid = self.irc.pseudoclient.uid
self.irc.run(':{u} KILL {u} :killed'.format(u=olduid))
self.u = self.irc.pseudoclient.uid
self.irc.run(':{u} KILL {u} :killed'.format(u=self.u))
msgs = self.irc.takeMsgs()
commands = self.irc.takeCommands(msgs)
hookdata = self.irc.takeHooks()[0]
del hookdata['ts']
self.assertEqual({'target': olduid, 'text': 'killed'}, hookdata)
# Make sure we're respawning our PseudoClient when its killed
self.assertIn('UID', commands)
self.assertIn('FJOIN', commands)
# Also make sure that we're updating the irc.pseudoclient field
self.assertNotEqual(self.irc.pseudoclient.uid, olduid)
hookdata = self.irc.takeHooks()[0][-1]
self.assertEqual(hookdata['target'], self.u)
self.assertEqual(hookdata['text'], 'killed')
self.assertNotIn(self.u, self.irc.users)
def testHandleKick(self):
self.irc.takeMsgs() # Ignore the initial connect messages
utils.add_hook(self.irc.dummyhook, 'KICK')
self.irc.run(':{u} KICK #pylink {u} :kicked'.format(u=self.irc.pseudoclient.uid))
hookdata = self.irc.takeHooks()[0]
del hookdata['ts']
self.assertEqual({'target': self.u, 'text': 'kicked', 'channel': '#pylink'}, hookdata)
hookdata = self.irc.takeHooks()[0][-1]
self.assertEqual(hookdata['target'], self.u)
self.assertEqual(hookdata['text'], 'kicked')
self.assertEqual(hookdata['channel'], '#pylink')
# Ditto above
msgs = self.irc.takeMsgs()
commands = self.irc.takeCommands(msgs)
self.assertIn('FJOIN', commands)
def testHandleFjoinUsers(self):
def testHandleFJoinUsers(self):
self.irc.run(':10X FJOIN #Chat 1423790411 + :,10XAAAAAA ,10XAAAAAB')
self.assertEqual({'10XAAAAAA', '10XAAAAAB'}, self.irc.channels['#chat'].users)
# self.assertIn('10XAAAAAB', self.irc.channels['#chat'].users)
self.assertIn('#chat', self.irc.users['10XAAAAAA'].channels)
# Sequential FJOINs must NOT remove existing users
self.irc.run(':10X FJOIN #Chat 1423790412 + :,10XAAAAAC')
# Join list can be empty too, in the case of permanent channels with 0 users.
self.irc.run(':10X FJOIN #Chat 1423790413 +nt :')
def testHandleFjoinModes(self):
def testHandleFJoinModes(self):
self.irc.run(':10X FJOIN #Chat 1423790411 +nt :,10XAAAAAA ,10XAAAAAB')
self.assertEqual({('n', None), ('t', None)}, self.irc.channels['#chat'].modes)
# Sequential FJOINs must NOT remove existing modes
self.irc.run(':10X FJOIN #Chat 1423790412 + :,10XAAAAAC')
self.assertEqual({('n', None), ('t', None)}, self.irc.channels['#chat'].modes)
def testHandleFjoinModesWithArgs(self):
def testHandleFJoinModesWithArgs(self):
self.irc.run(':10X FJOIN #Chat 1423790414 +nlks 10 t0psekrit :,10XAAAAAA ,10XAAAAAB')
self.assertEqual({('n', None), ('s', None), ('l', '10'), ('k', 't0psekrit')},
self.irc.channels['#chat'].modes)
def testHandleFjoinPrefixes(self):
def testHandleFJoinPrefixes(self):
self.irc.run(':10X FJOIN #Chat 1423790418 +nt :ov,10XAAAAAA v,10XAAAAAB ,10XAAAAAC')
self.assertEqual({('n', None), ('t', None)}, self.irc.channels['#chat'].modes)
self.assertEqual({'10XAAAAAA', '10XAAAAAB', '10XAAAAAC'}, self.irc.channels['#chat'].users)
self.assertIn('10XAAAAAA', self.irc.channels['#chat'].prefixmodes['ops'])
self.assertEqual({'10XAAAAAA', '10XAAAAAB'}, self.irc.channels['#chat'].prefixmodes['voices'])
def testHandleFjoinHook(self):
utils.add_hook(self.irc.dummyhook, 'JOIN')
def testHandleFJoinHook(self):
self.irc.run(':10X FJOIN #PyLink 1423790418 +ls 10 :ov,10XAAAAAA v,10XAAAAAB ,10XAAAAAC')
hookdata = self.irc.takeHooks()[0]
hookdata = self.irc.takeHooks()[0][-1]
expected = {'modes': [('+l', '10'), ('+s', None)],
'channel': '#pylink',
'users': ['10XAAAAAA', '10XAAAAAB', '10XAAAAAC'],
'ts': 1423790418}
self.assertEqual(expected, hookdata)
def testHandleFmode(self):
self.irc.run(':10X FJOIN #pylink 1423790411 +n :o,10XAAAAAA ,10XAAAAAB')
utils.add_hook(self.irc.dummyhook, 'MODE')
def testHandleFMode(self):
self.irc.run(':70M FMODE #pylink 1423790412 +ikl herebedragons 100')
self.assertEqual({('i', None), ('k', 'herebedragons'), ('l', '100'), ('n', None)}, 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.assertEqual({('m', None), ('n', None)}, self.irc.channels['#pylink'].modes)
self.assertEqual({('m', None)}, self.irc.channels['#pylink'].modes)
hookdata = self.irc.takeHooks()
expected = [{'target': '#pylink', 'modes': [('+i', None), ('+k', 'herebedragons'), ('+l', '100')], 'ts': 1423790412},
{'target': '#pylink', 'modes': [('-i', None), ('-l', None), ('-k', 'herebedragons'), ('+m', None)], 'ts': 1423790413}]
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 1423790411 +n :o,10XAAAAAA ,10XAAAAAB')
utils.add_hook(self.irc.dummyhook, 'MODE')
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
self.assertNotIn(('o', '10XAAAAAA'), self.irc.channels['#pylink'].modes)
self.assertEqual({'10XAAAAAA'}, self.irc.channels['#pylink'].prefixmodes['ops'])
self.irc.run(':70M FMODE #pylink 1423790412 +lot 50 %s' % self.u)
self.irc.run(':70M FMODE #pylink 123 +lot 50 %s' % self.u)
self.assertIn(self.u, self.irc.channels['#pylink'].prefixmodes['ops'])
modes = {('l', '50'), ('n', None), ('t', None)}
self.assertEqual(modes, self.irc.channels['#pylink'].modes)
self.irc.run(':70M FMODE #pylink 1423790413 -o %s' % self.u)
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 = [{'target': '#pylink', 'modes': [('+l', '50'), ('+o', '9PYAAAAAA'), ('+t', None)], 'ts': 1423790412},
{'target': '#pylink', 'modes': [('-o', '9PYAAAAAA')], 'ts': 1423790413}]
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 testFmodeRemovesOldParams(self):
utils.add_hook(self.irc.dummyhook, 'MODE')
def testHandleFModeRemovesOldParams(self):
self.irc.run(':70M FMODE #pylink 1423790412 +l 50')
self.assertEqual({('l', '50')}, self.irc.channels['#pylink'].modes)
self.irc.run(':70M FMODE #pylink 1423790412 +l 30')
self.assertEqual({('l', '30')}, self.irc.channels['#pylink'].modes)
hookdata = self.irc.takeHooks()
expected = [{'target': '#pylink', 'modes': [('+l', '50')], 'ts': 1423790412},
{'target': '#pylink', 'modes': [('+l', '30')], 'ts': 1423790412}]
expected = [['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '50')], 'ts': 1423790412}],
['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '30')], 'ts': 1423790412}]]
self.assertEqual(expected, hookdata)
def testFjoinResetsTS(self):
def testHandleFJoinResetsTS(self):
curr_ts = self.irc.channels['#pylink'].ts
self.irc.run(':70M FJOIN #pylink 5 + :')
self.assertEqual(self.irc.channels['#pylink'].ts, 5)
@ -285,87 +194,60 @@ class TestProtoInspIRCd(unittest.TestCase):
def testHandleTopic(self):
self.irc.connect()
utils.add_hook(self.irc.dummyhook, 'TOPIC')
self.irc.run(':9PYAAAAAA TOPIC #PyLink :test')
self.assertEqual(self.irc.channels['#pylink'].topic, 'test')
hookdata = self.irc.takeHooks()[0]
# Setter is a nick here, not an UID - this is to be consistent
# with FTOPIC above, which sends the nick/prefix of the topic setter.
self.assertTrue(utils.isNick(hookdata.get('setter')))
hookdata = self.irc.takeHooks()[0][-1]
self.assertEqual(type(hookdata['ts']), int)
self.assertEqual(hookdata['topic'], 'test')
self.assertEqual(hookdata['channel'], '#pylink')
def testMsgHooks(self):
def testHandleMessages(self):
for m in ('NOTICE', 'PRIVMSG'):
utils.add_hook(self.irc.dummyhook, m)
self.irc.run(':70MAAAAAA %s #dev :afasfsa' % m)
hookdata = self.irc.takeHooks()[0]
del hookdata['ts']
self.assertEqual({'target': '#dev', 'text': 'afasfsa'}, hookdata)
hookdata = self.irc.takeHooks()[0][-1]
self.assertEqual(hookdata['target'], '#dev')
self.assertEqual(hookdata['text'], 'afasfsa')
def testHandlePart(self):
utils.add_hook(self.irc.dummyhook, 'PART')
hookdata = self.irc.takeHooks()
self.irc.run(':9PYAAAAAA PART #pylink')
hookdata = self.irc.takeHooks()[0]
del hookdata['ts']
self.assertEqual({'channel': '#pylink', 'text': ''}, hookdata)
def testUIDHook(self):
utils.add_hook(self.irc.dummyhook, 'UID')
# Create the server so we won't KeyError on processing UID
self.irc.run('SERVER whatever. abcd 0 10X :Whatever Server - Hellas Planitia, Mars')
self.irc.run(':10X UID 10XAAAAAB 1429934638 GL 0::1 '
'hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 '
'+Wioswx +ACGKNOQXacfgklnoqvx :realname')
expected = {'uid': '10XAAAAAB', 'ts': '1429934638', 'nick': 'GL',
'realhost': '0::1', 'ident': 'gl', 'ip': '0::1',
'host': 'hidden-7j810p.9mdf.lrek.0000.0000.IP'}
hookdata = self.irc.takeHooks()[0]
self.assertEqual(hookdata, expected)
hookdata = self.irc.takeHooks()[0][-1]
self.assertEqual(hookdata['channels'], ['#pylink'])
self.assertEqual(hookdata['text'], '')
def testHandleQuit(self):
utils.add_hook(self.irc.dummyhook, 'QUIT')
self.irc.run('SERVER whatever. abcd 0 10X :Whatever Server - Hellas Planitia, Mars')
self.irc.run(':10X UID 10XAAAAAB 1429934638 GL 0::1 '
'hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 '
'+Wioswx +ACGKNOQXacfgklnoqvx :realname')
self.irc.takeHooks()
self.irc.run(':10XAAAAAB QUIT :Quit: quit message goes here')
hookdata = self.irc.takeHooks()[0]
del hookdata['ts']
self.assertEqual(hookdata, {'text': 'Quit: quit message goes here'})
hookdata = self.irc.takeHooks()[0][-1]
self.assertEqual(hookdata['text'], 'Quit: quit message goes here')
self.assertNotIn('10XAAAAAB', self.irc.users)
self.assertNotIn('10XAAAAAB', self.irc.servers['10X'].users)
def testHandleServer(self):
utils.add_hook(self.irc.dummyhook, 'SERVER')
self.irc.run(':00A SERVER test.server * 1 00C :testing raw message syntax')
hookdata = self.irc.takeHooks()[0]
del hookdata['ts']
self.assertEqual(hookdata, {'name': 'test.server', 'sid': '00C',
'text': 'testing raw message syntax'})
hookdata = self.irc.takeHooks()[-1][-1]
self.assertEqual(hookdata['name'], 'test.server')
self.assertEqual(hookdata['sid'], '00C')
self.assertEqual(hookdata['text'], 'testing raw message syntax')
self.assertIn('00C', self.irc.servers)
def testHandleNick(self):
utils.add_hook(self.irc.dummyhook, 'NICK')
self.irc.run(':9PYAAAAAA NICK PyLink-devel 1434744242')
hookdata = self.irc.takeHooks()[0]
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)
def testHandleSave(self):
utils.add_hook(self.irc.dummyhook, 'SAVE')
self.irc.run(':9PYAAAAAA NICK Derp_ 1433728673')
self.irc.run(':70M SAVE 9PYAAAAAA 1433728673')
hookdata = self.irc.takeHooks()[0]
self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'ts': 1433728673})
hookdata = self.irc.takeHooks()[-1][-1]
self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'ts': 1433728673, 'oldnick': 'Derp_'})
self.assertEqual('9PYAAAAAA', self.irc.users['9PYAAAAAA'].nick)
def testInviteHook(self):
utils.add_hook(self.irc.dummyhook, 'INVITE')
def testHandleInvite(self):
self.irc.run(':10XAAAAAA INVITE 9PYAAAAAA #blah 0')
hookdata = self.irc.takeHooks()[0]
hookdata = self.irc.takeHooks()[-1][-1]
del hookdata['ts']
self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'channel': '#blah'})

View File

@ -4,7 +4,6 @@ cwd = os.getcwd()
sys.path += [cwd, os.path.join(cwd, 'plugins')]
import unittest
import utils
import classes
import relay
@ -13,28 +12,34 @@ def dummyf():
class TestRelay(unittest.TestCase):
def setUp(self):
self.irc = classes.FakeIRC('unittest', classes.FakeProto(), classes.testconf)
self.irc = classes.FakeIRC('unittest', classes.FakeProto())
self.irc.maxnicklen = 20
self.irc.proto.__name__ = "test"
self.f = relay.normalizeNick
self.f = lambda nick: relay.normalizeNick(self.irc, 'unittest', nick)
# Fake our protocol name to something that supports slashes in nicks.
# relay uses a whitelist for this to prevent accidentally introducing
# bad nicks:
self.irc.proto.__name__ = "inspircd"
def testNormalizeNick(self):
# Second argument simply states the suffix.
self.assertEqual(self.f(self.irc, 'unittest', 'helloworld'), 'helloworld/unittest')
self.assertEqual(self.f(self.irc, 'unittest', 'ObnoxiouslyLongNick'), 'Obnoxiously/unittest')
self.assertEqual(self.f(self.irc, 'unittest', '10XAAAAAA'), '_10XAAAAAA/unittest')
self.assertEqual(self.f('helloworld'), 'helloworld/unittest')
self.assertEqual(self.f('ObnoxiouslyLongNick'), 'Obnoxiously/unittest')
self.assertEqual(self.f('10XAAAAAA'), '_10XAAAAAA/unittest')
def testNormalizeNickConflict(self):
self.assertEqual(self.f(self.irc, 'unittest', 'helloworld'), 'helloworld/unittest')
self.assertEqual(self.f('helloworld'), 'helloworld/unittest')
self.irc.users['10XAAAAAA'] = classes.IrcUser('helloworld/unittest', 1234, '10XAAAAAA')
# Increase amount of /'s by one
self.assertEqual(self.f(self.irc, 'unittest', 'helloworld'), 'helloworld//unittest')
self.assertEqual(self.f('helloworld'), 'helloworld//unittest')
self.irc.users['10XAAAAAB'] = classes.IrcUser('helloworld//unittest', 1234, '10XAAAAAB')
# Cut off the nick, not the suffix if the result is too long.
self.assertEqual(self.f(self.irc, 'unittest', 'helloworld'), 'helloworl///unittest')
self.assertEqual(self.f('helloworld'), 'helloworl///unittest')
def testNormalizeNickRemovesSlashes(self):
self.irc.proto.__name__ = "charybdis"
self.assertEqual(self.f(self.irc, 'unittest', 'helloworld'), 'helloworld|unittest')
self.assertEqual(self.f(self.irc, 'unittest', 'abcde/eJanus'), 'abcde|eJanu|unittest')
self.assertEqual(self.f(self.irc, 'unittest', 'ObnoxiouslyLongNick'), 'Obnoxiously|unittest')
try:
self.assertEqual(self.f('helloworld'), 'helloworld|unittest')
self.assertEqual(self.f('abcde/eJanus'), 'abcde|eJanu|unittest')
self.assertEqual(self.f('ObnoxiouslyLongNick'), 'Obnoxiously|unittest')
finally:
self.irc.proto.__name__ = "inspircd"

View File

@ -5,11 +5,16 @@ import unittest
import itertools
import utils
import classes
import world
def dummyf():
pass
class TestUtils(unittest.TestCase):
def setUp(self):
self.irc = classes.FakeIRC('fakeirc', classes.FakeProto())
def testTS6UIDGenerator(self):
uidgen = utils.TS6UIDGenerator('9PY')
self.assertEqual(uidgen.next_uid(), '9PYAAAAAA')
@ -21,16 +26,16 @@ class TestUtils(unittest.TestCase):
utils.add_cmd(dummyf)
utils.add_cmd(dummyf, 'TEST')
# All command names should be automatically lowercased.
self.assertIn('dummyf', utils.bot_commands)
self.assertIn('test', utils.bot_commands)
self.assertNotIn('TEST', utils.bot_commands)
self.assertIn('dummyf', world.bot_commands)
self.assertIn('test', world.bot_commands)
self.assertNotIn('TEST', world.bot_commands)
def test_add_hook(self):
utils.add_hook(dummyf, 'join')
self.assertIn('JOIN', utils.command_hooks)
self.assertIn('JOIN', world.command_hooks)
# Command names stored in uppercase.
self.assertNotIn('join', utils.command_hooks)
self.assertIn(dummyf, utils.command_hooks['JOIN'])
self.assertNotIn('join', world.command_hooks)
self.assertIn(dummyf, world.command_hooks['JOIN'])
def testIsNick(self):
self.assertFalse(utils.isNick('abcdefgh', nicklen=3))
@ -96,5 +101,19 @@ 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 testReverseModes(self):
f = lambda x: utils.reverseModes(self.irc, '#test', x)
# Strings.
self.assertEqual(f("+nt-lk"), "-nt+lk")
self.assertEqual(f("nt-k"), "-nt+k")
# Lists.
self.assertEqual(f([('+m', None), ('+t', None), ('+l', '3'), ('-o', 'person')]),
[('-m', None), ('-t', None), ('-l', '3'), ('+o', 'person')])
# Sets.
self.assertEqual(f({('s', None), ('+o', 'whoever')}), {('-s', None), ('-o', 'whoever')})
# Combining modes with an initial + and those without
self.assertEqual(f({('s', None), ('+n', None)}), {('-s', None), ('-n', None)})
if __name__ == '__main__':
unittest.main()

96
tests/tests_common.py Normal file
View File

@ -0,0 +1,96 @@
import sys
import os
sys.path += [os.getcwd(), os.path.join(os.getcwd(), 'protocols')]
import unittest
import world
import classes
world.started.set()
class PluginTestCase(unittest.TestCase):
def setUp(self):
self.irc = classes.FakeIRC('unittest', world.testing)
self.proto = self.irc.proto
self.irc.connect()
self.sdata = self.irc.serverdata
self.u = self.irc.pseudoclient.uid
self.maxDiff = None
# Dummy servers/users used in tests below.
self.proto.spawnServer(self.irc, 'whatever.', sid='10X')
for x in range(3):
self.proto.spawnClient(self.irc, 'user%s' % x, server='10X')
class CommonProtoTestCase(PluginTestCase):
def testJoinClient(self):
u = self.u
self.proto.joinClient(self.irc, u, '#Channel')
self.assertIn(u, self.irc.channels['#channel'].users)
# Non-existant user.
self.assertRaises(LookupError, self.proto.joinClient, self.irc, '9PYZZZZZZ', '#test')
def testKickClient(self):
target = self.proto.spawnClient(self.irc, 'soccerball', 'soccerball', 'abcd').uid
self.proto.joinClient(self.irc, target, '#pylink')
self.assertIn(self.u, self.irc.channels['#pylink'].users)
self.assertIn(target, self.irc.channels['#pylink'].users)
self.proto.kickClient(self.irc, self.u, '#pylink', target, 'Pow!')
self.assertNotIn(target, self.irc.channels['#pylink'].users)
def testModeClient(self):
testuser = self.proto.spawnClient(self.irc, 'testcakes')
self.irc.takeMsgs()
self.proto.modeClient(self.irc, self.u, testuser.uid, [('+i', None), ('+w', None)])
self.assertEqual({('i', None), ('w', None)}, testuser.modes)
self.proto.modeClient(self.irc, self.u, '#pylink', [('+s', None), ('+l', '30')])
self.assertEqual({('s', None), ('l', '30')}, self.irc.channels['#pylink'].modes)
cmds = self.irc.takeCommands(self.irc.takeMsgs())
self.assertEqual(cmds, ['MODE', 'FMODE'])
def testNickClient(self):
self.proto.nickClient(self.irc, self.u, 'NotPyLink')
self.assertEqual('NotPyLink', self.irc.users[self.u].nick)
def testPartClient(self):
u = self.u
self.proto.joinClient(self.irc, u, '#channel')
self.proto.partClient(self.irc, u, '#channel')
self.assertNotIn(u, self.irc.channels['#channel'].users)
def testQuitClient(self):
u = self.proto.spawnClient(self.irc, 'testuser3', 'moo', 'hello.world').uid
self.proto.joinClient(self.irc, u, '#channel')
self.assertRaises(LookupError, self.proto.quitClient, self.irc, '9PYZZZZZZ', 'quit reason')
self.proto.quitClient(self.irc, u, 'quit reason')
self.assertNotIn(u, self.irc.channels['#channel'].users)
self.assertNotIn(u, self.irc.users)
self.assertNotIn(u, self.irc.servers[self.irc.sid].users)
def testSpawnClient(self):
u = self.proto.spawnClient(self.irc, 'testuser3', 'moo', 'hello.world').uid
# Check the server index and the user index
self.assertIn(u, self.irc.servers[self.irc.sid].users)
self.assertIn(u, self.irc.users)
# Raise ValueError when trying to spawn a client on a server that's not ours
self.assertRaises(ValueError, self.proto.spawnClient, self.irc, 'abcd', 'user', 'dummy.user.net', server='44A')
# Unfilled args should get placeholder fields and not error.
self.proto.spawnClient(self.irc, 'testuser4')
def testSpawnClientOnServer(self):
self.proto.spawnServer(self.irc, 'subserver.pylink', '34Q')
u = self.proto.spawnClient(self.irc, 'person1', 'person', 'users.overdrive.pw', server='34Q')
# We're spawning clients on the right server, hopefully...
self.assertIn(u.uid, self.irc.servers['34Q'].users)
self.assertNotIn(u.uid, self.irc.servers[self.irc.sid].users)
def testSpawnServer(self):
# Incorrect SID length
self.assertRaises(Exception, self.proto.spawnServer, self.irc, 'subserver.pylink', '34Q0')
self.proto.spawnServer(self.irc, 'subserver.pylink', '34Q')
# Duplicate server name
self.assertRaises(Exception, self.proto.spawnServer, self.irc, 'Subserver.PyLink', '34Z')
# Duplicate SID
self.assertRaises(Exception, self.proto.spawnServer, self.irc, 'another.Subserver.PyLink', '34Q')
self.assertIn('34Q', self.irc.servers)

View File

@ -1,19 +1,13 @@
import string
import re
from collections import defaultdict
import threading
import inspect
from log import log
import world
global bot_commands, command_hooks
# This should be a mapping of command names to functions
bot_commands = {}
command_hooks = defaultdict(list)
networkobjects = {}
schedulers = {}
plugins = []
whois_handlers = []
started = threading.Event()
# This is separate from classes.py to prevent import loops.
class NotAuthenticatedError(Exception):
pass
class TS6UIDGenerator():
"""TS6 UID Generator module, adapted from InspIRCd source
@ -114,12 +108,12 @@ def add_cmd(func, name=None):
if name is None:
name = func.__name__
name = name.lower()
bot_commands[name] = func
world.bot_commands[name].append(func)
def add_hook(func, command):
"""Add a hook <func> for command <command>."""
command = command.upper()
command_hooks[command].append(func)
world.command_hooks[command].append(func)
def toLower(irc, text):
"""<irc object> <text>
@ -168,7 +162,7 @@ def isServerName(s):
return _isASCII(s) and '.' in s and not s.startswith('.')
def parseModes(irc, target, args):
"""Parses a mode string 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')]
"""
# http://www.irc.org/tech_docs/005.html
@ -340,6 +334,34 @@ def joinModes(modes):
modelist += ' %s' % ' '.join(args)
return modelist
def reverseModes(irc, target, modes):
"""<mode string/mode list>
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)]
"""
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(" "))
else:
return newmodes
def isInternalClient(irc, numeric):
"""<irc object> <client numeric>
@ -357,15 +379,38 @@ def isInternalServer(irc, sid):
"""
return (sid in irc.servers and irc.servers[sid].internal)
def isOper(irc, uid):
def isOper(irc, uid, allowAuthed=True, allowOper=True):
"""<irc object> <UID>
Returns whether <UID> has operator status on PyLink. This can be achieved
by either identifying to PyLink as admin, or having user mode +o set.
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
results.
"""
return (uid in irc.users and (("o", None) in irc.users[uid].modes or irc.users[uid].identified))
if uid in irc.users:
if allowOper and ("o", None) in irc.users[uid].modes:
return True
elif allowAuthed and irc.users[uid].identified:
return 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."""
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,
getHostmask(irc, uid), lastfunc)
raise NotAuthenticatedError("You are not authenticated!")
return True
def getHostmask(irc, user):
"""<irc object> <UID>
Gets the hostmask of user <UID>, if present."""
userobj = irc.users.get(user)
if userobj is None:
return '<user object not found>'

18
world.py Normal file
View File

@ -0,0 +1,18 @@
# world.py: global state variables go here
from collections import defaultdict
import threading
# Global variable to indicate whether we're being ran directly, or imported
# for a testcase.
testing = True
global bot_commands, command_hooks
# This should be a mapping of command names to functions
bot_commands = defaultdict(list)
command_hooks = defaultdict(list)
networkobjects = {}
schedulers = {}
plugins = []
whois_handlers = []
started = threading.Event()