3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-11-01 09:19:23 +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 import threading
from random import randint from random import randint
import time
import socket
import threading
import ssl
from collections import defaultdict
import hashlib
from log import log from log import log
import main from conf import conf
import time 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(): class IrcUser():
def __init__(self, nick, ts, uid, ident='null', host='null', def __init__(self, nick, ts, uid, ident='null', host='null',
@ -36,7 +290,7 @@ class IrcServer():
""" """
def __init__(self, uplink, name, internal=False): def __init__(self, uplink, name, internal=False):
self.uplink = uplink self.uplink = uplink
self.users = [] self.users = set()
self.internal = internal self.internal = internal
self.name = name.lower() self.name = name.lower()
def __repr__(self): def __repr__(self):
@ -45,7 +299,7 @@ class IrcServer():
class IrcChannel(): class IrcChannel():
def __init__(self): def __init__(self):
self.users = set() self.users = set()
self.modes = set() self.modes = {('n', None), ('t', None)}
self.topic = '' self.topic = ''
self.ts = int(time.time()) self.ts = int(time.time())
self.topicset = False self.topicset = False
@ -60,33 +314,9 @@ class IrcChannel():
s.discard(target) s.discard(target)
self.users.discard(target) self.users.discard(target)
class ProtocolError(Exception): ### FakeIRC classes, used for test cases
pass
global testconf class FakeIRC(Irc):
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):
def connect(self): def connect(self):
self.messages = [] self.messages = []
self.hookargs = [] self.hookargs = []
@ -100,7 +330,10 @@ class FakeIRC(main.Irc):
def run(self, data): def run(self, data):
"""Queues a message to the fake IRC server.""" """Queues a message to the fake IRC server."""
log.debug('<- ' + data) 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): def send(self, data):
self.messages.append(data) self.messages.append(data)
@ -132,13 +365,13 @@ class FakeIRC(main.Irc):
self.hookmsgs = [] self.hookmsgs = []
return hookmsgs return hookmsgs
@staticmethod
def dummyhook(irc, source, command, parsed_args):
"""Dummy function to bind to hooks."""
irc.hookmsgs.append(parsed_args)
class FakeProto(): class FakeProto():
"""Dummy protocol module for testing purposes.""" """Dummy protocol module for testing purposes."""
def __init__(self):
self.hook_map = {}
self.casemapping = 'rfc1459'
self.__name__ = 'FakeProto'
@staticmethod @staticmethod
def handle_events(irc, data): def handle_events(irc, data):
pass pass

64
conf.py
View File

@ -1,19 +1,53 @@
import yaml import yaml
import sys import sys
from collections import defaultdict
global confname import world
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:
# 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 testconf
global conf testconf = {'bot':
conf = yaml.load(f) {
'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:
# 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:
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 import utils
from log import log from log import log
import world
# Handle KILLs sent to the PyLink client and respawn # Handle KILLs sent to the PyLink client and respawn
def handle_kill(irc, source, command, args): def handle_kill(irc, source, command, args):
@ -24,18 +25,17 @@ def handle_commands(irc, source, command, args):
cmd_args = text.split(' ') cmd_args = text.split(' ')
cmd = cmd_args[0].lower() cmd = cmd_args[0].lower()
cmd_args = cmd_args[1:] cmd_args = cmd_args[1:]
try: if cmd not in world.bot_commands:
func = utils.bot_commands[cmd] utils.msg(irc, source, 'Error: Unknown command %r.' % cmd)
except KeyError:
utils.msg(irc, source, 'Unknown command %r.' % cmd)
return
try:
log.info('(%s) Calling command %r for %s', irc.name, cmd, utils.getHostmask(irc, source))
func(irc, source, cmd_args)
except Exception as e:
log.exception('Unhandled exception caught in command %r', cmd)
utils.msg(irc, source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
return return
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)
utils.msg(irc, source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
return
utils.add_hook(handle_commands, 'PRIVMSG') utils.add_hook(handle_commands, 'PRIVMSG')
# Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd). # Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd).
@ -75,7 +75,13 @@ def handle_whois(irc, source, command, args):
# 313: sends a string denoting the target's operator privilege, # 313: sends a string denoting the target's operator privilege,
# only if they have umode +o. # only if they have umode +o.
if ('o', None) in user.modes: if ('o', None) in user.modes:
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. # 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd.
# Only show this to opers! # Only show this to opers!
if sourceisOper: if sourceisOper:
@ -84,7 +90,7 @@ def handle_whois(irc, source, command, args):
# idle time, so we simply return 0. # idle time, so we simply return 0.
# <- 317 GL GL 15 1437632859 :seconds idle, signon time # <- 317 GL GL 15 1437632859 :seconds idle, signon time
f(irc, server, 317, source, "%s 0 %s :seconds idle, signon time" % (nick, user.ts)) 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 # Iterate over custom plugin WHOIS handlers. They return a tuple
# or list with two arguments: the numeric, and the text to send. # or list with two arguments: the numeric, and the text to send.
try: try:

261
main.py
View File

@ -2,258 +2,17 @@
import imp import imp
import os import os
import socket
import time
import sys 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 import conf
from log import log
import classes import classes
import utils
import coreplugin 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__': if __name__ == '__main__':
log.info('PyLink starting...') log.info('PyLink starting...')
if conf.conf['login']['password'] == 'changeme': if conf.conf['login']['password'] == 'changeme':
@ -267,7 +26,7 @@ if __name__ == '__main__':
# Import plugins first globally, because they can listen for events # Import plugins first globally, because they can listen for events
# that happen before the connection phase. # that happen before the connection phase.
utils.plugins.append(coreplugin) world.plugins.append(coreplugin)
to_load = conf.conf['plugins'] to_load = conf.conf['plugins']
plugins_folder = [os.path.join(os.getcwd(), 'plugins')] plugins_folder = [os.path.join(os.getcwd(), 'plugins')]
# Here, we override the module lookup and import the plugins # Here, we override the module lookup and import the plugins
@ -276,7 +35,7 @@ if __name__ == '__main__':
try: try:
moduleinfo = imp.find_module(plugin, plugins_folder) moduleinfo = imp.find_module(plugin, plugins_folder)
pl = imp.load_source(plugin, moduleinfo[1]) pl = imp.load_source(plugin, moduleinfo[1])
utils.plugins.append(pl) world.plugins.append(pl)
except ImportError as e: except ImportError as e:
if str(e) == ('No module named %r' % plugin): if str(e) == ('No module named %r' % plugin):
log.error('Failed to load plugin %r: The plugin could not be found.', 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)) log.critical('Failed to load protocol module: ImportError: %s', protoname, str(e))
sys.exit(2) sys.exit(2)
else: else:
utils.networkobjects[network] = Irc(network, proto, conf.conf) world.networkobjects[network] = classes.Irc(network, proto)
utils.started.set() world.started.set()
log.info("loaded plugins: %s", utils.plugins) log.info("loaded plugins: %s", world.plugins)

View File

@ -1,47 +1,22 @@
# admin.py: PyLink administrative commands # admin.py: PyLink administrative commands
import sys import sys
import os import os
import inspect
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils import utils
from log import log 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 @utils.add_cmd
def spawnclient(irc, source, args): def spawnclient(irc, source, args):
"""<nick> <ident> <host> """<nick> <ident> <host>
Admin-only. Spawns the specified PseudoClient on the PyLink server. Admin-only. Spawns the specified PseudoClient on the PyLink server.
Note: this doesn't check the validity of any fields you give it!""" Note: this doesn't check the validity of any fields you give it!"""
checkauthenticated(irc, source) utils.checkAuthenticated(irc, source, allowOper=False)
try: try:
nick, ident, host = args[:3] nick, ident, host = args[:3]
except ValueError: 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 return
irc.proto.spawnClient(irc, nick, ident, host) irc.proto.spawnClient(irc, nick, ident, host)
@ -50,17 +25,17 @@ def quit(irc, source, args):
"""<target> [<reason>] """<target> [<reason>]
Admin-only. Quits the PyLink client with nick <target>, if one exists.""" Admin-only. Quits the PyLink client with nick <target>, if one exists."""
checkauthenticated(irc, source) utils.checkAuthenticated(irc, source, allowOper=False)
try: try:
nick = args[0] nick = args[0]
except IndexError: 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 return
if irc.pseudoclient.uid == utils.nickToUid(irc, nick): 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 return
u = utils.nickToUid(irc, nick) u = utils.nickToUid(irc, nick)
quitmsg = ' '.join(args[1:]) or 'Client quit' quitmsg = ' '.join(args[1:]) or 'Client Quit'
irc.proto.quitClient(irc, u, quitmsg) irc.proto.quitClient(irc, u, quitmsg)
irc.callHooks([u, 'PYLINK_ADMIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}]) irc.callHooks([u, 'PYLINK_ADMIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}])
@ -68,14 +43,14 @@ def joinclient(irc, source, args):
"""<target> <channel1>,[<channel2>], etc. """<target> <channel1>,[<channel2>], etc.
Admin-only. Joins <target>, the nick of a PyLink client, to a comma-separated list of channels.""" 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: try:
nick = args[0] nick = args[0]
clist = args[1].split(',') clist = args[1].split(',')
if not clist: if not clist:
raise IndexError raise IndexError
except 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 return
u = utils.nickToUid(irc, nick) u = utils.nickToUid(irc, nick)
for channel in clist: for channel in clist:
@ -93,12 +68,12 @@ def nick(irc, source, args):
"""<target> <newnick> """<target> <newnick>
Admin-only. Changes the nick of <target>, a PyLink client, to <newnick>.""" Admin-only. Changes the nick of <target>, a PyLink client, to <newnick>."""
checkauthenticated(irc, source) utils.checkAuthenticated(irc, source, allowOper=False)
try: try:
nick = args[0] nick = args[0]
newnick = args[1] newnick = args[1]
except IndexError: 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 return
u = utils.nickToUid(irc, nick) u = utils.nickToUid(irc, nick)
if newnick in ('0', u): if newnick in ('0', u):
@ -114,13 +89,13 @@ def part(irc, source, args):
"""<target> <channel1>,[<channel2>],... [<reason>] """<target> <channel1>,[<channel2>],... [<reason>]
Admin-only. Parts <target>, the nick of a PyLink client, from a comma-separated list of channels.""" 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: try:
nick = args[0] nick = args[0]
clist = args[1].split(',') clist = args[1].split(',')
reason = ' '.join(args[2:]) reason = ' '.join(args[2:])
except 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 return
u = utils.nickToUid(irc, nick) u = utils.nickToUid(irc, nick)
for channel in clist: for channel in clist:
@ -135,14 +110,14 @@ def kick(irc, source, args):
"""<source> <channel> <user> [<reason>] """<source> <channel> <user> [<reason>]
Admin-only. Kicks <user> from <channel> via <source>, where <source> is the nick of a PyLink client.""" 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: try:
nick = args[0] nick = args[0]
channel = args[1] channel = args[1]
target = args[2] target = args[2]
reason = ' '.join(args[3:]) reason = ' '.join(args[3:])
except IndexError: 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 return
u = utils.nickToUid(irc, nick) or nick u = utils.nickToUid(irc, nick) or nick
targetu = utils.nickToUid(irc, target) targetu = utils.nickToUid(irc, target)
@ -155,38 +130,19 @@ def kick(irc, source, args):
irc.proto.kickClient(irc, u, channel, targetu, reason) irc.proto.kickClient(irc, u, channel, targetu, reason)
irc.callHooks([u, 'PYLINK_ADMIN_KICK', {'channel': channel, 'target': targetu, 'text': reason, 'parse_as': 'KICK'}]) 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 @utils.add_cmd
def showchan(irc, source, args): def showchan(irc, source, args):
"""<channel> """<channel>
Admin-only. Shows information about <channel>.""" Admin-only. Shows information about <channel>."""
checkauthenticated(irc, source) utils.checkAuthenticated(irc, source, allowOper=False)
try: try:
channel = args[0].lower() channel = args[0].lower()
except IndexError: 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 return
if channel not in irc.channels: 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 return
s = ['\x02%s\x02: %s' % (k, v) for k, v in sorted(irc.channels[channel].__dict__.items())] 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)) s = 'Information on channel \x02%s\x02: %s' % (channel, '; '.join(s))
@ -197,14 +153,14 @@ def mode(irc, source, args):
"""<source> <target> <modes> """<source> <target> <modes>
Admin-only. Sets modes <modes> on <target> from <source>, where <source> is either the nick of a PyLink client, or the SID of a PyLink server.""" Admin-only. Sets modes <modes> on <target> from <source>, where <source> is either the nick of a PyLink client, or the SID of a PyLink server."""
checkauthenticated(irc, source) utils.checkAuthenticated(irc, source, allowOper=False)
try: try:
modesource, target, modes = args[0], args[1], args[2:] modesource, target, modes = args[0], args[1], args[2:]
except IndexError: except IndexError:
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 return
if not modes: if not modes:
utils.msg(irc, source, "Error: no modes given to set!") utils.msg(irc, source, "Error: No modes given to set!")
return return
parsedmodes = utils.parseModes(irc, target, modes) parsedmodes = utils.parseModes(irc, target, modes)
targetuid = utils.nickToUid(irc, target) targetuid = utils.nickToUid(irc, target)
@ -226,25 +182,25 @@ def msg(irc, source, args):
"""<source> <target> <text> """<source> <target> <text>
Admin-only. Sends message <text> from <source>, where <source> is the nick of a PyLink client.""" 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: try:
msgsource, target, text = args[0], args[1], ' '.join(args[2:]) msgsource, target, text = args[0], args[1], ' '.join(args[2:])
except IndexError: 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 return
sourceuid = utils.nickToUid(irc, msgsource) sourceuid = utils.nickToUid(irc, msgsource)
if not sourceuid: if not sourceuid:
utils.msg(irc, source, 'Error: unknown user %r' % msgsource) utils.msg(irc, source, 'Error: Unknown user %r.' % msgsource)
return return
if not utils.isChannel(target): if not utils.isChannel(target):
real_target = utils.nickToUid(irc, target) real_target = utils.nickToUid(irc, target)
if real_target is None: if real_target is None:
utils.msg(irc, source, 'Error: unknown user %r' % target) utils.msg(irc, source, 'Error: Unknown user %r.' % target)
return return
else: else:
real_target = target real_target = target
if not text: if not text:
utils.msg(irc, source, 'Error: no text given.') utils.msg(irc, source, 'Error: No text given.')
return return
irc.proto.messageClient(irc, sourceuid, real_target, text) irc.proto.messageClient(irc, sourceuid, real_target, text)
irc.callHooks([sourceuid, 'PYLINK_ADMIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}]) 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 # commands.py: base PyLink commands
import sys import sys
import os import os
from time import ctime
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils import utils
from conf import conf from conf import conf
from log import log from log import log
import world
@utils.add_cmd @utils.add_cmd
def status(irc, source, args): def status(irc, source, args):
@ -27,7 +29,7 @@ def identify(irc, source, args):
try: try:
username, password = args[0], args[1] username, password = args[0], args[1]
except IndexError: except IndexError:
utils.msg(irc, source, 'Error: not enough arguments.') utils.msg(irc, source, 'Error: Not enough arguments.')
return return
# Usernames are case-insensitive, passwords are NOT. # Usernames are case-insensitive, passwords are NOT.
if username.lower() == conf['login']['user'].lower() and password == conf['login']['password']: if username.lower() == conf['login']['user'].lower() and password == conf['login']['password']:
@ -35,9 +37,9 @@ def identify(irc, source, args):
irc.users[source].identified = realuser irc.users[source].identified = realuser
utils.msg(irc, source, 'Successfully logged in as %s.' % realuser) utils.msg(irc, source, 'Successfully logged in as %s.' % realuser)
log.info("(%s) Successful login to %r by %s.", log.info("(%s) Successful login to %r by %s.",
irc.name, username, utils.getHostmask(irc, source)) irc.name, username, utils.getHostmask(irc, source))
else: else:
utils.msg(irc, source, 'Incorrect credentials.') utils.msg(irc, source, 'Error: Incorrect credentials.')
u = irc.users[source] u = irc.users[source]
log.warning("(%s) Failed login to %r from %s.", log.warning("(%s) Failed login to %r from %s.",
irc.name, username, utils.getHostmask(irc, source)) irc.name, username, utils.getHostmask(irc, source))
@ -46,8 +48,12 @@ def listcommands(irc, source, args):
"""takes no arguments. """takes no arguments.
Returns a list of available commands PyLink has to offer.""" Returns a list of available commands PyLink has to offer."""
cmds = list(utils.bot_commands.keys()) cmds = list(world.bot_commands.keys())
cmds.sort() 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, 'Available commands include: %s' % ', '.join(cmds))
utils.msg(irc, source, 'To see help on a specific command, type \x02help <command>\x02.') utils.msg(irc, source, 'To see help on a specific command, type \x02help <command>\x02.')
utils.add_cmd(listcommands, 'list') utils.add_cmd(listcommands, 'list')
@ -62,20 +68,60 @@ def help(irc, source, args):
except IndexError: # No argument given, just return 'list' output except IndexError: # No argument given, just return 'list' output
listcommands(irc, source, args) listcommands(irc, source, args)
return return
try: if command not in world.bot_commands:
func = utils.bot_commands[command] utils.msg(irc, source, 'Error: Unknown command %r.' % command)
except KeyError:
utils.msg(irc, source, 'Error: no such command %r.' % command)
return return
else: else:
doc = func.__doc__ funcs = world.bot_commands[command]
if doc: if len(funcs) > 1:
lines = doc.split('\n') utils.msg(irc, source, 'The following \x02%s\x02 plugins bind to the \x02%s\x02 command: %s'
# Bold the first line, which usually just tells you what % (len(funcs), command, ', '.join([func.__module__ for func in funcs])))
# arguments the command takes. for func in funcs:
lines[0] = '\x02%s %s\x02' % (command, lines[0]) doc = func.__doc__
for line in lines: mod = func.__module__
utils.msg(irc, source, line.strip()) if doc:
else: lines = doc.split('\n')
utils.msg(irc, source, 'Error: Command %r doesn\'t offer any help.' % command) # Bold the first line, which usually just tells you what
return # arguments the command takes.
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 (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 import utils
from log import log from log import log
from conf import confname from conf import confname
import world
dbname = "pylinkrelay" dbname = "pylinkrelay"
if confname != 'pylink': if confname != 'pylink':
@ -28,11 +29,11 @@ def relayWhoisHandlers(irc, target):
orig = getLocalUser(irc, target) orig = getLocalUser(irc, target)
if orig: if orig:
network, remoteuid = 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 " return [320, "%s :is a remote user connected via PyLink Relay. Home "
"network: %s; Home nick: %s" % (user.nick, network, "network: %s; Home nick: %s" % (user.nick, network,
remotenick)] remotenick)]
utils.whois_handlers.append(relayWhoisHandlers) world.whois_handlers.append(relayWhoisHandlers)
def normalizeNick(irc, netname, nick, separator=None, uid=''): def normalizeNick(irc, netname, nick, separator=None, uid=''):
separator = separator or irc.serverdata.get('separator') or "/" separator = separator or irc.serverdata.get('separator') or "/"
@ -94,7 +95,7 @@ def loadDB():
db = {} db = {}
def exportDB(reschedule=False): def exportDB(reschedule=False):
scheduler = utils.schedulers.get('relaydb') scheduler = world.schedulers.get('relaydb')
if reschedule and scheduler: if reschedule and scheduler:
scheduler.enter(30, 1, exportDB, argument=(True,)) scheduler.enter(30, 1, exportDB, argument=(True,))
log.debug("Relay: exporting links database to %s", dbname) log.debug("Relay: exporting links database to %s", dbname)
@ -110,7 +111,7 @@ def save(irc, source, args):
exportDB() exportDB()
utils.msg(irc, source, 'Done.') utils.msg(irc, source, 'Done.')
else: else:
utils.msg(irc, source, 'Error: you are not authenticated!') utils.msg(irc, source, 'Error: You are not authenticated!')
return return
def getPrefixModes(irc, remoteirc, channel, user): def getPrefixModes(irc, remoteirc, channel, user):
@ -149,16 +150,48 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
host = userobj.host[:64] host = userobj.host[:64]
realname = userobj.realname realname = userobj.realname
modes = getSupportedUmodes(irc, remoteirc, userobj.modes) 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, u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident,
host=host, realname=realname, 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].remote = (irc.name, user)
remoteirc.users[u].opertype = opertype
away = userobj.away away = userobj.away
if away: if away:
remoteirc.proto.awayClient(remoteirc, u, away) remoteirc.proto.awayClient(remoteirc, u, away)
relayusers[(irc.name, user)][remoteirc.name] = u relayusers[(irc.name, user)][remoteirc.name] = u
return 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): def getLocalUser(irc, user, targetirc=None):
"""<irc object> <pseudoclient uid> [<target irc object>] """<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 # 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 # target network, if it exists. Otherwise, we'll return a tuple
# with the home network name and the original user's UID. # 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 targetirc and sourceobj:
if remoteuser[0] == targetirc.name: if remoteuser[0] == targetirc.name:
# The user we found's home network happens to be the one being # The user we found's home network happens to be the one being
@ -230,7 +263,7 @@ def initializeChannel(irc, channel):
remotenet, remotechan = link remotenet, remotechan = link
if remotenet == irc.name: if remotenet == irc.name:
continue continue
remoteirc = utils.networkobjects.get(remotenet) remoteirc = world.networkobjects.get(remotenet)
if remoteirc is None: if remoteirc is None:
continue continue
rc = remoteirc.channels[remotechan] rc = remoteirc.channels[remotechan]
@ -255,12 +288,12 @@ def handle_join(irc, numeric, command, args):
return return
ts = args['ts'] ts = args['ts']
users = set(args['users']) users = set(args['users'])
relayJoins(irc, channel, users, ts) relayJoins(irc, channel, users, ts, burst=False)
utils.add_hook(handle_join, 'JOIN') utils.add_hook(handle_join, 'JOIN')
def handle_quit(irc, numeric, command, args): def handle_quit(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].copy().items(): 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']) remoteirc.proto.quitClient(remoteirc, user, args['text'])
del relayusers[(irc.name, numeric)] del relayusers[(irc.name, numeric)]
utils.add_hook(handle_quit, 'QUIT') utils.add_hook(handle_quit, 'QUIT')
@ -274,7 +307,7 @@ utils.add_hook(handle_squit, 'SQUIT')
def handle_nick(irc, numeric, command, args): def handle_nick(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].items(): 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) newnick = normalizeNick(remoteirc, irc.name, args['newnick'], uid=user)
if remoteirc.users[user].nick != newnick: if remoteirc.users[user].nick != newnick:
remoteirc.proto.nickClient(remoteirc, user, newnick) remoteirc.proto.nickClient(remoteirc, user, newnick)
@ -288,7 +321,7 @@ def handle_part(irc, numeric, command, args):
return return
for channel in channels: for channel in channels:
for netname, user in relayusers[(irc.name, numeric)].copy().items(): for netname, user in relayusers[(irc.name, numeric)].copy().items():
remoteirc = utils.networkobjects[netname] remoteirc = world.networkobjects[netname]
remotechan = findRemoteChan(irc, remoteirc, channel) remotechan = findRemoteChan(irc, remoteirc, channel)
if remotechan is None: if remotechan is None:
continue continue
@ -324,7 +357,7 @@ def handle_privmsg(irc, numeric, command, args):
return return
if utils.isChannel(target): if utils.isChannel(target):
for netname, user in relayusers[(irc.name, numeric)].items(): for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname] remoteirc = world.networkobjects[netname]
real_target = findRemoteChan(irc, remoteirc, target) real_target = findRemoteChan(irc, remoteirc, target)
if not real_target: if not real_target:
continue 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 # on the remote network, and we won't have anything to send our
# messages from. # messages from.
if homenet not in remoteusers.keys(): 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.' % \ 'with %r in order to send messages.' % \
irc.users[target].nick, notice=True) irc.users[target].nick, notice=True)
return return
remoteirc = utils.networkobjects[homenet] remoteirc = world.networkobjects[homenet]
user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False) user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
if notice: if notice:
remoteirc.proto.noticeClient(remoteirc, user, real_target, text) 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: if relay is None or target == irc.pseudoclient.uid:
return return
origuser = getLocalUser(irc, target) 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(): if irc.name == name or not remoteirc.connected.is_set():
continue continue
remotechan = findRemoteChan(irc, remoteirc, channel) 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. # Join the kicked client back with its respective modes.
irc.proto.sjoinServer(irc, irc.sid, channel, [(modes, target)]) irc.proto.sjoinServer(irc, irc.sid, channel, [(modes, target)])
if kicker in irc.users: 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, irc.name, args['text'], irc.users[source].nick,
remoteirc.users[real_target].nick, remoteirc.name, channel) remoteirc.users[real_target].nick, remoteirc.name, channel)
utils.msg(irc, kicker, "This channel is claimed; your kick to " utils.msg(irc, kicker, "This channel is claimed; your kick to "
"%s has been blocked because you are not " "%s has been blocked because you are not "
"(half)opped." % channel, notice=True) "(half)opped." % channel, notice=True)
else: 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, irc.name, args['text'], irc.servers[source].name,
remoteirc.users[real_target].nick, remoteirc.name, channel) remoteirc.users[real_target].nick, remoteirc.name, channel)
return return
@ -458,7 +491,7 @@ def handle_chgclient(irc, source, command, args):
text = args['newgecos'] text = args['newgecos']
if field: if field:
for netname, user in relayusers[(irc.name, target)].items(): for netname, user in relayusers[(irc.name, target)].items():
remoteirc = utils.networkobjects[netname] remoteirc = world.networkobjects[netname]
try: try:
remoteirc.proto.updateClient(remoteirc, user, field, text) remoteirc.proto.updateClient(remoteirc, user, field, text)
except NotImplementedError: # IRCd doesn't support changing the field we want except NotImplementedError: # IRCd doesn't support changing the field we want
@ -579,30 +612,38 @@ def getSupportedUmodes(irc, remoteirc, modes):
else: else:
log.debug("(%s) getSupportedUmodes: skipping mode (%r, %r) because " log.debug("(%s) getSupportedUmodes: skipping mode (%r, %r) because "
"the remote network (%s)'s IRCd (%s) doesn't support it.", "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 return supported_modes
def handle_mode(irc, numeric, command, args): def handle_mode(irc, numeric, command, args):
target = args['target'] target = args['target']
modes = args['modes'] 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(): if irc.name == name or not remoteirc.connected.is_set():
continue continue
if utils.isChannel(target): if utils.isChannel(target):
relayModes(irc, remoteirc, numeric, target, modes) relayModes(irc, remoteirc, numeric, target, modes)
else: else:
# Set hideoper on remote opers, to prevent inflating
# /lusers and various /stats
hideoper_mode = remoteirc.umodes.get('hideoper')
modes = getSupportedUmodes(irc, remoteirc, modes) 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) remoteuser = getRemoteUser(irc, remoteirc, target, spawnIfMissing=False)
if remoteuser is None: if remoteuser and modes:
continue remoteirc.proto.modeClient(remoteirc, remoteuser, remoteuser, modes)
remoteirc.proto.modeClient(remoteirc, remoteuser, remoteuser, modes)
utils.add_hook(handle_mode, 'MODE') utils.add_hook(handle_mode, 'MODE')
def handle_topic(irc, numeric, command, args): def handle_topic(irc, numeric, command, args):
channel = args['channel'] channel = args['channel']
topic = args['topic'] 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(): if irc.name == name or not remoteirc.connected.is_set():
continue 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 # We don't allow killing over the relay, so we must respawn the affected
# client and rejoin it to its channels. # client and rejoin it to its channels.
del relayusers[realuser][irc.name] del relayusers[realuser][irc.name]
remoteirc = utils.networkobjects[realuser[0]] remoteirc = world.networkobjects[realuser[0]]
for remotechan in remoteirc.channels.copy(): for remotechan in remoteirc.channels.copy():
localchan = findRemoteChan(remoteirc, irc, remotechan) localchan = findRemoteChan(remoteirc, irc, remotechan)
if localchan: if localchan:
@ -637,7 +678,7 @@ def handle_kill(irc, numeric, command, args):
client = getRemoteUser(remoteirc, irc, realuser[1]) client = getRemoteUser(remoteirc, irc, realuser[1])
irc.proto.sjoinServer(irc, irc.sid, localchan, [(modes, client)]) irc.proto.sjoinServer(irc, irc.sid, localchan, [(modes, client)])
if userdata and numeric in irc.users: 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, irc.name, args['text'], irc.users[numeric].nick,
remoteirc.users[realuser[1]].nick, realuser[0]) remoteirc.users[realuser[1]].nick, realuser[0])
utils.msg(irc, numeric, "Your kill to %s has been blocked " 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." % \ " users over the relay at this time." % \
userdata.nick, notice=True) userdata.nick, notice=True)
else: 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, irc.name, args['text'], irc.servers[numeric].name,
remoteirc.users[realuser[1]].nick, realuser[0]) remoteirc.users[realuser[1]].nick, realuser[0])
# Target user was local. # Target user was local.
@ -670,7 +711,7 @@ def isRelayClient(irc, user):
return False return False
def relayJoins(irc, channel, users, ts, burst=True): def relayJoins(irc, channel, users, ts, burst=True):
for name, remoteirc in utils.networkobjects.items(): for name, remoteirc in world.networkobjects.items():
queued_users = [] queued_users = []
if name == irc.name or not remoteirc.connected.is_set(): if name == irc.name or not remoteirc.connected.is_set():
# Don't relay things to their source network... # 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) remoteirc.proto.joinClient(remoteirc, queued_users[0][1], remotechan)
def relayPart(irc, channel, user): 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(): if name == irc.name or not remoteirc.connected.is_set():
# Don't relay things to their source network... # Don't relay things to their source network...
continue continue
@ -730,7 +771,7 @@ def removeChannel(irc, channel):
if irc is None: if irc is None:
return return
if channel not in map(str.lower, irc.serverdata['channels']): 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)) relay = findRelay((irc.name, channel))
if relay: if relay:
for user in irc.channels[channel].users.copy(): for user in irc.channels[channel].users.copy():
@ -756,16 +797,16 @@ def create(irc, source, args):
try: try:
channel = utils.toLower(irc, args[0]) channel = utils.toLower(irc, args[0])
except IndexError: 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 return
if not utils.isChannel(channel): if not utils.isChannel(channel):
utils.msg(irc, source, 'Error: invalid channel %r.' % channel) utils.msg(irc, source, 'Error: Invalid channel %r.' % channel)
return return
if source not in irc.channels[channel].users: 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 return
if not utils.isOper(irc, source): 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 return
db[(irc.name, channel)] = {'claim': [irc.name], 'links': set(), 'blocked_nets': set()} db[(irc.name, channel)] = {'claim': [irc.name], 'links': set(), 'blocked_nets': set()}
initializeChannel(irc, channel) initializeChannel(irc, channel)
@ -779,24 +820,24 @@ def destroy(irc, source, args):
try: try:
channel = utils.toLower(irc, args[0]) channel = utils.toLower(irc, args[0])
except IndexError: 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 return
if not utils.isChannel(channel): if not utils.isChannel(channel):
utils.msg(irc, source, 'Error: invalid channel %r.' % channel) utils.msg(irc, source, 'Error: Invalid channel %r.' % channel)
return return
if not utils.isOper(irc, source): 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 return
entry = (irc.name, channel) entry = (irc.name, channel)
if entry in db: if entry in db:
for link in db[entry]['links']: 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) removeChannel(irc, channel)
del db[entry] del db[entry]
utils.msg(irc, source, 'Done.') utils.msg(irc, source, 'Done.')
else: else:
utils.msg(irc, source, 'Error: no such relay %r exists.' % channel) utils.msg(irc, source, 'Error: No such relay %r exists.' % channel)
return return
@utils.add_cmd @utils.add_cmd
@ -809,7 +850,7 @@ def link(irc, source, args):
channel = utils.toLower(irc, args[1]) channel = utils.toLower(irc, args[1])
remotenet = args[0].lower() remotenet = args[0].lower()
except IndexError: 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 return
try: try:
localchan = utils.toLower(irc, args[2]) localchan = utils.toLower(irc, args[2])
@ -817,33 +858,33 @@ def link(irc, source, args):
localchan = channel localchan = channel
for c in (channel, localchan): for c in (channel, localchan):
if not utils.isChannel(c): if not utils.isChannel(c):
utils.msg(irc, source, 'Error: invalid channel %r.' % c) utils.msg(irc, source, 'Error: Invalid channel %r.' % c)
return return
if source not in irc.channels[localchan].users: 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 return
if not utils.isOper(irc, source): 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 return
if remotenet not in utils.networkobjects: if remotenet not in world.networkobjects:
utils.msg(irc, source, 'Error: no network named %r exists.' % remotenet) utils.msg(irc, source, 'Error: No network named %r exists.' % remotenet)
return return
localentry = findRelay((irc.name, localchan)) localentry = findRelay((irc.name, localchan))
if localentry: 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 return
try: try:
entry = db[(remotenet, channel)] entry = db[(remotenet, channel)]
except KeyError: 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 return
else: else:
if irc.name in entry['blocked_nets']: 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 return
for link in entry['links']: for link in entry['links']:
if link[0] == irc.name: 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, " linked here as %r." % (remotenet,
channel, link[1])) channel, link[1]))
return return
@ -860,17 +901,17 @@ def delink(irc, source, args):
try: try:
channel = utils.toLower(irc, args[0]) channel = utils.toLower(irc, args[0])
except IndexError: 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 return
try: try:
remotenet = args[1].lower() remotenet = args[1].lower()
except IndexError: except IndexError:
remotenet = None remotenet = None
if not utils.isOper(irc, source): 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 return
if not utils.isChannel(channel): if not utils.isChannel(channel):
utils.msg(irc, source, 'Error: invalid channel %r.' % channel) utils.msg(irc, source, 'Error: Invalid channel %r.' % channel)
return return
entry = findRelay((irc.name, channel)) entry = findRelay((irc.name, channel))
if entry: if entry:
@ -884,18 +925,18 @@ def delink(irc, source, args):
else: else:
for link in db[entry]['links'].copy(): for link in db[entry]['links'].copy():
if link[0] == remotenet: if link[0] == remotenet:
removeChannel(utils.networkobjects.get(remotenet), link[1]) removeChannel(world.networkobjects.get(remotenet), link[1])
db[entry]['links'].remove(link) db[entry]['links'].remove(link)
else: else:
removeChannel(irc, channel) removeChannel(irc, channel)
db[entry]['links'].remove((irc.name, channel)) db[entry]['links'].remove((irc.name, channel))
utils.msg(irc, source, 'Done.') utils.msg(irc, source, 'Done.')
else: else:
utils.msg(irc, source, 'Error: no such relay %r.' % channel) utils.msg(irc, source, 'Error: No such relay %r.' % channel)
def initializeAll(irc): def initializeAll(irc):
log.debug('(%s) initializeAll: waiting for utils.started', irc.name) log.debug('(%s) initializeAll: waiting for world.started', irc.name)
utils.started.wait() world.started.wait()
for chanpair, entrydata in db.items(): for chanpair, entrydata in db.items():
network, channel = chanpair network, channel = chanpair
initializeChannel(irc, channel) initializeChannel(irc, channel)
@ -905,7 +946,7 @@ def initializeAll(irc):
def main(): def main():
loadDB() loadDB()
utils.schedulers['relaydb'] = scheduler = sched.scheduler() world.schedulers['relaydb'] = scheduler = sched.scheduler()
scheduler.enter(30, 1, exportDB, argument=(True,)) scheduler.enter(30, 1, exportDB, argument=(True,))
# Thread this because exportDB() queues itself as part of its # Thread this because exportDB() queues itself as part of its
# execution, in order to get a repeating loop. # 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 # It's one of our relay clients; try to fix our nick to the next
# available normalized nick. # available normalized nick.
remotenet, remoteuser = realuser remotenet, remoteuser = realuser
remoteirc = utils.networkobjects[remotenet] remoteirc = world.networkobjects[remotenet]
nick = remoteirc.users[remoteuser].nick nick = remoteirc.users[remoteuser].nick
# Limit how many times we can attempt to fix our nick, to prevent # Limit how many times we can attempt to fix our nick, to prevent
# floods and such. # floods and such.
@ -963,7 +1004,7 @@ def linked(irc, source, args):
"""takes no arguments. """takes no arguments.
Returns a list of channels shared across the relay.""" Returns a list of channels shared across the relay."""
networks = list(utils.networkobjects.keys()) networks = list(world.networkobjects.keys())
networks.remove(irc.name) networks.remove(irc.name)
s = 'Connected networks: \x02%s\x02 %s' % (irc.name, ' '.join(networks)) s = 'Connected networks: \x02%s\x02 %s' % (irc.name, ' '.join(networks))
utils.msg(irc, source, s) utils.msg(irc, source, s)
@ -978,7 +1019,7 @@ def linked(irc, source, args):
def handle_away(irc, numeric, command, args): def handle_away(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].items(): for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname] remoteirc = world.networkobjects[netname]
remoteirc.proto.awayClient(remoteirc, user, args['text']) remoteirc.proto.awayClient(remoteirc, user, args['text'])
utils.add_hook(handle_away, 'AWAY') utils.add_hook(handle_away, 'AWAY')
@ -989,15 +1030,37 @@ def handle_spawnmain(irc, numeric, command, args):
initializeAll(irc) initializeAll(irc)
utils.add_hook(handle_spawnmain, 'PYLINK_SPAWNMAIN') 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 @utils.add_cmd
def linkacl(irc, source, args): def linkacl(irc, source, args):
"""ALLOW|DENY|LIST <channel> <remotenet> """ALLOW|DENY|LIST <channel> <remotenet>
Allows blocking / unblocking certain networks from linking to a relay, based on a blacklist. 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.""" 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): 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 return
try: try:
cmd = args[0].lower() cmd = args[0].lower()
@ -1006,11 +1069,11 @@ def linkacl(irc, source, args):
utils.msg(irc, source, missingargs) utils.msg(irc, source, missingargs)
return return
if not utils.isChannel(channel): if not utils.isChannel(channel):
utils.msg(irc, source, 'Error: invalid channel %r.' % channel) utils.msg(irc, source, 'Error: Invalid channel %r.' % channel)
return return
relay = findRelay((irc.name, channel)) relay = findRelay((irc.name, channel))
if not relay: 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 return
if cmd == 'list': if cmd == 'list':
s = 'Blocked networks for \x02%s\x02: \x02%s\x02' % (channel, ', '.join(db[relay]['blocked_nets']) or '(empty)') 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: try:
db[relay]['blocked_nets'].remove(remotenet) db[relay]['blocked_nets'].remove(remotenet)
except KeyError: 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: else:
utils.msg(irc, source, 'Done.') utils.msg(irc, source, 'Done.')
else: 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, u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip) realhost=realhost, ip=ip)
utils.applyModes(irc, uid, modes) 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}" _send(irc, server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}"
" {ts} {modes} + :{realname}".format(ts=ts, host=host, " {ts} {modes} + :{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid, nick=nick, ident=ident, uid=uid,
@ -138,7 +138,7 @@ def removeClient(irc, numeric):
log.debug('Removing client %s from irc.users', numeric) log.debug('Removing client %s from irc.users', numeric)
del irc.users[numeric] del irc.users[numeric]
log.debug('Removing client %s from irc.servers[%s]', numeric, sid) 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): def quitClient(irc, numeric, reason):
"""<irc object> <client numeric> """<irc object> <client numeric>
@ -195,6 +195,7 @@ def _operUp(irc, target, opertype=None):
otype = 'IRC_Operator' otype = 'IRC_Operator'
log.debug('(%s) Sending OPERTYPE from %s to oper them up.', log.debug('(%s) Sending OPERTYPE from %s to oper them up.',
irc.name, target) irc.name, target)
userobj.opertype = otype
_send(irc, target, 'OPERTYPE %s' % otype) _send(irc, target, 'OPERTYPE %s' % otype)
def _sendModes(irc, numeric, target, modes, ts=None): 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]]) parsedmodes = utils.parseModes(irc, uid, [args[8], args[9]])
log.debug('Applying modes %s for %s', parsedmodes, uid) log.debug('Applying modes %s for %s', parsedmodes, uid)
utils.applyModes(irc, uid, parsedmodes) 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} return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
def handle_quit(irc, numeric, command, args): def handle_quit(irc, numeric, command, args):
@ -706,7 +707,10 @@ def handle_opertype(irc, numeric, command, args):
omode = [('+o', None)] omode = [('+o', None)]
irc.users[numeric].opertype = opertype = args[0] irc.users[numeric].opertype = opertype = args[0]
utils.applyModes(irc, numeric, omode) 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): def handle_fident(irc, numeric, command, args):
# :70MAAAAAB FHOST test # :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, u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip) realhost=realhost, ip=ip)
utils.applyModes(irc, uid, modes) 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} " _send(irc, server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
"{realhost} * :{realname}".format(ts=ts, host=host, "{realhost} * :{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid, nick=nick, ident=ident, uid=uid,
@ -469,7 +469,10 @@ def handle_euid(irc, numeric, command, args):
parsedmodes = utils.parseModes(irc, uid, [modes]) parsedmodes = utils.parseModes(irc, uid, [modes])
log.debug('Applying modes %s for %s', parsedmodes, uid) log.debug('Applying modes %s for %s', parsedmodes, uid)
utils.applyModes(irc, uid, parsedmodes) 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} return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
def handle_uid(irc, numeric, command, args): def handle_uid(irc, numeric, command, args):
@ -502,6 +505,17 @@ def handle_tmode(irc, numeric, command, args):
ts = int(args[0]) ts = int(args[0])
return {'target': channel, 'modes': changedmodes, 'ts': ts} 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): def handle_events(irc, data):
# TS6 messages: # TS6 messages:
# :42X COMMAND arg1 arg2 :final long arg # :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 sys
import os import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from log import log
import classes import classes
import unittest import unittest
# Yes, we're going to even test the testing classes. Testception? I think so.
class TestFakeIRC(unittest.TestCase): class TestFakeIRC(unittest.TestCase):
def setUp(self): def setUp(self):
self.irc = classes.FakeIRC('unittest', classes.FakeProto(), classes.testconf) self.irc = classes.FakeIRC('unittest', classes.FakeProto())
def testFakeIRC(self): def testFakeIRC(self):
self.irc.run('this should do nothing') self.irc.run('this should do nothing')

View File

@ -2,31 +2,30 @@ import sys
import os import os
sys.path += [os.getcwd(), os.path.join(os.getcwd(), 'protocols')] sys.path += [os.getcwd(), os.path.join(os.getcwd(), 'protocols')]
import unittest import unittest
from collections import defaultdict
import inspircd import inspircd
import classes import classes
import utils import world
import coreplugin
class TestProtoInspIRCd(unittest.TestCase): import tests_common
def setUp(self):
self.irc = classes.FakeIRC('unittest', inspircd, classes.testconf) world.testing = inspircd
self.proto = self.irc.proto
self.sdata = self.irc.serverdata class InspIRCdTestCase(tests_common.CommonProtoTestCase):
# This is to initialize ourself as an internal PseudoServer, so we can spawn clients def testCheckRecvpass(self):
self.proto.connect(self.irc) # Correct recvpass here.
self.u = self.irc.pseudoclient.uid self.irc.run('SERVER somehow.someday abcd 0 0AL :Somehow Server - McMurdo Station, Antarctica')
self.maxDiff = None # Incorrect recvpass here; should raise ProtocolError.
utils.command_hooks = defaultdict(list) self.assertRaises(classes.ProtocolError, self.irc.run, 'SERVER somehow.someday BADPASS 0 0AL :Somehow Server - McMurdo Station, Antarctica')
def testConnect(self): def testConnect(self):
self.proto.connect(self.irc)
initial_messages = self.irc.takeMsgs() initial_messages = self.irc.takeMsgs()
commands = self.irc.takeCommands(initial_messages) commands = self.irc.takeCommands(initial_messages)
# SERVER pylink.unittest abcd 0 9PY :PyLink Service # SERVER pylink.unittest abcd 0 9PY :PyLink Service
serverline = 'SERVER %s %s 0 %s :PyLink Service' % ( serverline = 'SERVER %s %s 0 %s :%s' % (
self.sdata['hostname'], self.sdata['sendpass'], self.sdata['sid']) self.sdata['hostname'], self.sdata['sendpass'], self.sdata['sid'],
self.irc.botdata['serverdesc'])
self.assertIn(serverline, initial_messages) self.assertIn(serverline, initial_messages)
self.assertIn('BURST', commands) self.assertIn('BURST', commands)
self.assertIn('ENDBURST', commands) self.assertIn('ENDBURST', commands)
@ -34,88 +33,12 @@ class TestProtoInspIRCd(unittest.TestCase):
self.assertIn('UID', commands) self.assertIn('UID', commands)
self.assertIn('FJOIN', 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): def testSpawnServer(self):
# Incorrect SID length super(InspIRCdTestCase, self).testSpawnServer()
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)
# Are we bursting properly? # Are we bursting properly?
self.assertIn(':34Q ENDBURST', self.irc.takeMsgs()) self.assertIn(':34Q ENDBURST', self.irc.takeMsgs())
def testSpawnClientOnServer(self): def testHandleSQuit(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):
# Spawn a messy network map, just because! # Spawn a messy network map, just because!
self.proto.spawnServer(self.irc, 'level1.pylink', '34P') self.proto.spawnServer(self.irc, 'level1.pylink', '34P')
self.proto.spawnServer(self.irc, 'level2.pylink', '34Q', uplink='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('34Q', self.irc.servers)
self.assertNotIn('34Z', 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): def testHandleServer(self):
self.irc.run('SERVER whatever.net abcd 0 10X :something') self.irc.run('SERVER whatever.net abcd 0 10X :something')
self.assertIn('10X', self.irc.servers) self.assertIn('10X', self.irc.servers)
@ -157,124 +68,122 @@ class TestProtoInspIRCd(unittest.TestCase):
self.assertEqual('test.server', self.irc.servers['0AL'].name) self.assertEqual('test.server', self.irc.servers['0AL'].name)
def testHandleUID(self): 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.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.servers['10X'].users)
self.assertIn('10XAAAAAB', self.irc.users)
u = self.irc.users['10XAAAAAB'] u = self.irc.users['10XAAAAAB']
self.assertEqual('GL', u.nick) 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): def testHandleKill(self):
self.irc.takeMsgs() # Ignore the initial connect messages self.irc.takeMsgs() # Ignore the initial connect messages
utils.add_hook(self.irc.dummyhook, 'KILL') self.u = self.irc.pseudoclient.uid
olduid = self.irc.pseudoclient.uid self.irc.run(':{u} KILL {u} :killed'.format(u=self.u))
self.irc.run(':{u} KILL {u} :killed'.format(u=olduid))
msgs = self.irc.takeMsgs() msgs = self.irc.takeMsgs()
commands = self.irc.takeCommands(msgs) commands = self.irc.takeCommands(msgs)
hookdata = self.irc.takeHooks()[0] hookdata = self.irc.takeHooks()[0][-1]
del hookdata['ts'] self.assertEqual(hookdata['target'], self.u)
self.assertEqual({'target': olduid, 'text': 'killed'}, hookdata) self.assertEqual(hookdata['text'], 'killed')
# Make sure we're respawning our PseudoClient when its killed self.assertNotIn(self.u, self.irc.users)
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)
def testHandleKick(self): def testHandleKick(self):
self.irc.takeMsgs() # Ignore the initial connect messages 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)) self.irc.run(':{u} KICK #pylink {u} :kicked'.format(u=self.irc.pseudoclient.uid))
hookdata = self.irc.takeHooks()[0] hookdata = self.irc.takeHooks()[0][-1]
del hookdata['ts'] self.assertEqual(hookdata['target'], self.u)
self.assertEqual({'target': self.u, 'text': 'kicked', 'channel': '#pylink'}, hookdata) self.assertEqual(hookdata['text'], 'kicked')
self.assertEqual(hookdata['channel'], '#pylink')
# Ditto above def testHandleFJoinUsers(self):
msgs = self.irc.takeMsgs()
commands = self.irc.takeCommands(msgs)
self.assertIn('FJOIN', commands)
def testHandleFjoinUsers(self):
self.irc.run(':10X FJOIN #Chat 1423790411 + :,10XAAAAAA ,10XAAAAAB') self.irc.run(':10X FJOIN #Chat 1423790411 + :,10XAAAAAA ,10XAAAAAB')
self.assertEqual({'10XAAAAAA', '10XAAAAAB'}, self.irc.channels['#chat'].users) 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 # Sequential FJOINs must NOT remove existing users
self.irc.run(':10X FJOIN #Chat 1423790412 + :,10XAAAAAC') self.irc.run(':10X FJOIN #Chat 1423790412 + :,10XAAAAAC')
# Join list can be empty too, in the case of permanent channels with 0 users. # Join list can be empty too, in the case of permanent channels with 0 users.
self.irc.run(':10X FJOIN #Chat 1423790413 +nt :') 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.irc.run(':10X FJOIN #Chat 1423790411 +nt :,10XAAAAAA ,10XAAAAAB')
self.assertEqual({('n', None), ('t', None)}, self.irc.channels['#chat'].modes) self.assertEqual({('n', None), ('t', None)}, self.irc.channels['#chat'].modes)
# Sequential FJOINs must NOT remove existing modes # Sequential FJOINs must NOT remove existing modes
self.irc.run(':10X FJOIN #Chat 1423790412 + :,10XAAAAAC') self.irc.run(':10X FJOIN #Chat 1423790412 + :,10XAAAAAC')
self.assertEqual({('n', None), ('t', None)}, self.irc.channels['#chat'].modes) 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.irc.run(':10X FJOIN #Chat 1423790414 +nlks 10 t0psekrit :,10XAAAAAA ,10XAAAAAB')
self.assertEqual({('n', None), ('s', None), ('l', '10'), ('k', 't0psekrit')}, self.assertEqual({('n', None), ('s', None), ('l', '10'), ('k', 't0psekrit')},
self.irc.channels['#chat'].modes) 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.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({('n', None), ('t', None)}, self.irc.channels['#chat'].modes)
self.assertEqual({'10XAAAAAA', '10XAAAAAB', '10XAAAAAC'}, self.irc.channels['#chat'].users) self.assertEqual({'10XAAAAAA', '10XAAAAAB', '10XAAAAAC'}, self.irc.channels['#chat'].users)
self.assertIn('10XAAAAAA', self.irc.channels['#chat'].prefixmodes['ops']) self.assertIn('10XAAAAAA', self.irc.channels['#chat'].prefixmodes['ops'])
self.assertEqual({'10XAAAAAA', '10XAAAAAB'}, self.irc.channels['#chat'].prefixmodes['voices']) self.assertEqual({'10XAAAAAA', '10XAAAAAB'}, self.irc.channels['#chat'].prefixmodes['voices'])
def testHandleFjoinHook(self): def testHandleFJoinHook(self):
utils.add_hook(self.irc.dummyhook, 'JOIN')
self.irc.run(':10X FJOIN #PyLink 1423790418 +ls 10 :ov,10XAAAAAA v,10XAAAAAB ,10XAAAAAC') 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)], expected = {'modes': [('+l', '10'), ('+s', None)],
'channel': '#pylink', 'channel': '#pylink',
'users': ['10XAAAAAA', '10XAAAAAB', '10XAAAAAC'], 'users': ['10XAAAAAA', '10XAAAAAB', '10XAAAAAC'],
'ts': 1423790418} 'ts': 1423790418}
self.assertEqual(expected, hookdata) self.assertEqual(expected, hookdata)
def testHandleFmode(self): def testHandleFMode(self):
self.irc.run(':10X FJOIN #pylink 1423790411 +n :o,10XAAAAAA ,10XAAAAAB')
utils.add_hook(self.irc.dummyhook, 'MODE')
self.irc.run(':70M FMODE #pylink 1423790412 +ikl herebedragons 100') self.irc.run(':70M FMODE #pylink 1423790412 +ikl herebedragons 100')
self.assertEqual({('i', None), ('k', 'herebedragons'), ('l', '100'), ('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.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() hookdata = self.irc.takeHooks()
expected = [{'target': '#pylink', 'modes': [('+i', None), ('+k', 'herebedragons'), ('+l', '100')], 'ts': 1423790412}, expected = [['70M', 'FMODE', {'target': '#pylink', 'modes':
{'target': '#pylink', 'modes': [('-i', None), ('-l', None), ('-k', 'herebedragons'), ('+m', None)], 'ts': 1423790413}] [('+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) self.assertEqual(expected, hookdata)
def testHandleFmodeWithPrefixes(self): def testHandleFModeWithPrefixes(self):
self.irc.run(':70M FJOIN #pylink 1423790411 +n :o,10XAAAAAA ,10XAAAAAB') self.irc.run(':70M FJOIN #pylink 123 +n :o,10XAAAAAA ,10XAAAAAB')
utils.add_hook(self.irc.dummyhook, 'MODE')
# Prefix modes are stored separately, so they should never show up in .modes # Prefix modes are stored separately, so they should never show up in .modes
self.assertNotIn(('o', '10XAAAAAA'), self.irc.channels['#pylink'].modes) self.assertNotIn(('o', '10XAAAAAA'), self.irc.channels['#pylink'].modes)
self.assertEqual({'10XAAAAAA'}, self.irc.channels['#pylink'].prefixmodes['ops']) 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']) self.assertIn(self.u, self.irc.channels['#pylink'].prefixmodes['ops'])
modes = {('l', '50'), ('n', None), ('t', None)} modes = {('l', '50'), ('n', None), ('t', None)}
self.assertEqual(modes, self.irc.channels['#pylink'].modes) 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.assertEqual(modes, self.irc.channels['#pylink'].modes)
self.assertNotIn(self.u, self.irc.channels['#pylink'].prefixmodes['ops']) self.assertNotIn(self.u, self.irc.channels['#pylink'].prefixmodes['ops'])
# Test hooks # Test hooks
hookdata = self.irc.takeHooks() hookdata = self.irc.takeHooks()
expected = [{'target': '#pylink', 'modes': [('+l', '50'), ('+o', '9PYAAAAAA'), ('+t', None)], 'ts': 1423790412}, expected = [['70M', 'FJOIN', {'channel': '#pylink', 'ts': 123, 'modes': [('+n', None)],
{'target': '#pylink', 'modes': [('-o', '9PYAAAAAA')], 'ts': 1423790413}] '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) self.assertEqual(expected, hookdata)
def testFmodeRemovesOldParams(self): def testHandleFModeRemovesOldParams(self):
utils.add_hook(self.irc.dummyhook, 'MODE')
self.irc.run(':70M FMODE #pylink 1423790412 +l 50') self.irc.run(':70M FMODE #pylink 1423790412 +l 50')
self.assertEqual({('l', '50')}, self.irc.channels['#pylink'].modes) self.assertEqual({('l', '50')}, self.irc.channels['#pylink'].modes)
self.irc.run(':70M FMODE #pylink 1423790412 +l 30') self.irc.run(':70M FMODE #pylink 1423790412 +l 30')
self.assertEqual({('l', '30')}, self.irc.channels['#pylink'].modes) self.assertEqual({('l', '30')}, self.irc.channels['#pylink'].modes)
hookdata = self.irc.takeHooks() hookdata = self.irc.takeHooks()
expected = [{'target': '#pylink', 'modes': [('+l', '50')], 'ts': 1423790412}, expected = [['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '50')], 'ts': 1423790412}],
{'target': '#pylink', 'modes': [('+l', '30')], 'ts': 1423790412}] ['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '30')], 'ts': 1423790412}]]
self.assertEqual(expected, hookdata) self.assertEqual(expected, hookdata)
def testFjoinResetsTS(self): def testHandleFJoinResetsTS(self):
curr_ts = self.irc.channels['#pylink'].ts curr_ts = self.irc.channels['#pylink'].ts
self.irc.run(':70M FJOIN #pylink 5 + :') self.irc.run(':70M FJOIN #pylink 5 + :')
self.assertEqual(self.irc.channels['#pylink'].ts, 5) self.assertEqual(self.irc.channels['#pylink'].ts, 5)
@ -285,87 +194,60 @@ class TestProtoInspIRCd(unittest.TestCase):
def testHandleTopic(self): def testHandleTopic(self):
self.irc.connect() self.irc.connect()
utils.add_hook(self.irc.dummyhook, 'TOPIC')
self.irc.run(':9PYAAAAAA TOPIC #PyLink :test') self.irc.run(':9PYAAAAAA TOPIC #PyLink :test')
self.assertEqual(self.irc.channels['#pylink'].topic, 'test') self.assertEqual(self.irc.channels['#pylink'].topic, 'test')
hookdata = self.irc.takeHooks()[0] hookdata = self.irc.takeHooks()[0][-1]
# 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')))
self.assertEqual(type(hookdata['ts']), int) self.assertEqual(type(hookdata['ts']), int)
self.assertEqual(hookdata['topic'], 'test') self.assertEqual(hookdata['topic'], 'test')
self.assertEqual(hookdata['channel'], '#pylink') self.assertEqual(hookdata['channel'], '#pylink')
def testMsgHooks(self): def testHandleMessages(self):
for m in ('NOTICE', 'PRIVMSG'): for m in ('NOTICE', 'PRIVMSG'):
utils.add_hook(self.irc.dummyhook, m)
self.irc.run(':70MAAAAAA %s #dev :afasfsa' % m) self.irc.run(':70MAAAAAA %s #dev :afasfsa' % m)
hookdata = self.irc.takeHooks()[0] hookdata = self.irc.takeHooks()[0][-1]
del hookdata['ts'] self.assertEqual(hookdata['target'], '#dev')
self.assertEqual({'target': '#dev', 'text': 'afasfsa'}, hookdata) self.assertEqual(hookdata['text'], 'afasfsa')
def testHandlePart(self): def testHandlePart(self):
utils.add_hook(self.irc.dummyhook, 'PART') hookdata = self.irc.takeHooks()
self.irc.run(':9PYAAAAAA PART #pylink') self.irc.run(':9PYAAAAAA PART #pylink')
hookdata = self.irc.takeHooks()[0] hookdata = self.irc.takeHooks()[0][-1]
del hookdata['ts'] self.assertEqual(hookdata['channels'], ['#pylink'])
self.assertEqual({'channel': '#pylink', 'text': ''}, hookdata) self.assertEqual(hookdata['text'], '')
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)
def testHandleQuit(self): def testHandleQuit(self):
utils.add_hook(self.irc.dummyhook, 'QUIT') self.irc.takeHooks()
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.run(':10XAAAAAB QUIT :Quit: quit message goes here') self.irc.run(':10XAAAAAB QUIT :Quit: quit message goes here')
hookdata = self.irc.takeHooks()[0] hookdata = self.irc.takeHooks()[0][-1]
del hookdata['ts'] self.assertEqual(hookdata['text'], 'Quit: quit message goes here')
self.assertEqual(hookdata, {'text': 'Quit: quit message goes here'})
self.assertNotIn('10XAAAAAB', self.irc.users) self.assertNotIn('10XAAAAAB', self.irc.users)
self.assertNotIn('10XAAAAAB', self.irc.servers['10X'].users) self.assertNotIn('10XAAAAAB', self.irc.servers['10X'].users)
def testHandleServer(self): def testHandleServer(self):
utils.add_hook(self.irc.dummyhook, 'SERVER')
self.irc.run(':00A SERVER test.server * 1 00C :testing raw message syntax') self.irc.run(':00A SERVER test.server * 1 00C :testing raw message syntax')
hookdata = self.irc.takeHooks()[0] hookdata = self.irc.takeHooks()[-1][-1]
del hookdata['ts'] self.assertEqual(hookdata['name'], 'test.server')
self.assertEqual(hookdata, {'name': 'test.server', 'sid': '00C', self.assertEqual(hookdata['sid'], '00C')
'text': 'testing raw message syntax'}) self.assertEqual(hookdata['text'], 'testing raw message syntax')
self.assertIn('00C', self.irc.servers) self.assertIn('00C', self.irc.servers)
def testHandleNick(self): def testHandleNick(self):
utils.add_hook(self.irc.dummyhook, 'NICK')
self.irc.run(':9PYAAAAAA NICK PyLink-devel 1434744242') 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} expected = {'newnick': 'PyLink-devel', 'oldnick': 'PyLink', 'ts': 1434744242}
self.assertEqual(hookdata, expected) self.assertEqual(hookdata, expected)
self.assertEqual('PyLink-devel', self.irc.users['9PYAAAAAA'].nick) self.assertEqual('PyLink-devel', self.irc.users['9PYAAAAAA'].nick)
def testHandleSave(self): def testHandleSave(self):
utils.add_hook(self.irc.dummyhook, 'SAVE')
self.irc.run(':9PYAAAAAA NICK Derp_ 1433728673') self.irc.run(':9PYAAAAAA NICK Derp_ 1433728673')
self.irc.run(':70M SAVE 9PYAAAAAA 1433728673') self.irc.run(':70M SAVE 9PYAAAAAA 1433728673')
hookdata = self.irc.takeHooks()[0] hookdata = self.irc.takeHooks()[-1][-1]
self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'ts': 1433728673}) self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'ts': 1433728673, 'oldnick': 'Derp_'})
self.assertEqual('9PYAAAAAA', self.irc.users['9PYAAAAAA'].nick) self.assertEqual('9PYAAAAAA', self.irc.users['9PYAAAAAA'].nick)
def testInviteHook(self): def testHandleInvite(self):
utils.add_hook(self.irc.dummyhook, 'INVITE')
self.irc.run(':10XAAAAAA INVITE 9PYAAAAAA #blah 0') self.irc.run(':10XAAAAAA INVITE 9PYAAAAAA #blah 0')
hookdata = self.irc.takeHooks()[0] hookdata = self.irc.takeHooks()[-1][-1]
del hookdata['ts'] del hookdata['ts']
self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'channel': '#blah'}) self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'channel': '#blah'})

View File

@ -4,7 +4,6 @@ cwd = os.getcwd()
sys.path += [cwd, os.path.join(cwd, 'plugins')] sys.path += [cwd, os.path.join(cwd, 'plugins')]
import unittest import unittest
import utils
import classes import classes
import relay import relay
@ -13,28 +12,34 @@ def dummyf():
class TestRelay(unittest.TestCase): class TestRelay(unittest.TestCase):
def setUp(self): 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.maxnicklen = 20
self.irc.proto.__name__ = "test" self.f = lambda nick: relay.normalizeNick(self.irc, 'unittest', nick)
self.f = relay.normalizeNick # 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): def testNormalizeNick(self):
# Second argument simply states the suffix. # Second argument simply states the suffix.
self.assertEqual(self.f(self.irc, 'unittest', 'helloworld'), 'helloworld/unittest') self.assertEqual(self.f('helloworld'), 'helloworld/unittest')
self.assertEqual(self.f(self.irc, 'unittest', 'ObnoxiouslyLongNick'), 'Obnoxiously/unittest') self.assertEqual(self.f('ObnoxiouslyLongNick'), 'Obnoxiously/unittest')
self.assertEqual(self.f(self.irc, 'unittest', '10XAAAAAA'), '_10XAAAAAA/unittest') self.assertEqual(self.f('10XAAAAAA'), '_10XAAAAAA/unittest')
def testNormalizeNickConflict(self): 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') self.irc.users['10XAAAAAA'] = classes.IrcUser('helloworld/unittest', 1234, '10XAAAAAA')
# Increase amount of /'s by one # 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') self.irc.users['10XAAAAAB'] = classes.IrcUser('helloworld//unittest', 1234, '10XAAAAAB')
# Cut off the nick, not the suffix if the result is too long. # 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): def testNormalizeNickRemovesSlashes(self):
self.irc.proto.__name__ = "charybdis" self.irc.proto.__name__ = "charybdis"
self.assertEqual(self.f(self.irc, 'unittest', 'helloworld'), 'helloworld|unittest') try:
self.assertEqual(self.f(self.irc, 'unittest', 'abcde/eJanus'), 'abcde|eJanu|unittest') self.assertEqual(self.f('helloworld'), 'helloworld|unittest')
self.assertEqual(self.f(self.irc, 'unittest', 'ObnoxiouslyLongNick'), 'Obnoxiously|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 itertools
import utils import utils
import classes
import world
def dummyf(): def dummyf():
pass pass
class TestUtils(unittest.TestCase): class TestUtils(unittest.TestCase):
def setUp(self):
self.irc = classes.FakeIRC('fakeirc', classes.FakeProto())
def testTS6UIDGenerator(self): def testTS6UIDGenerator(self):
uidgen = utils.TS6UIDGenerator('9PY') uidgen = utils.TS6UIDGenerator('9PY')
self.assertEqual(uidgen.next_uid(), '9PYAAAAAA') self.assertEqual(uidgen.next_uid(), '9PYAAAAAA')
@ -21,16 +26,16 @@ class TestUtils(unittest.TestCase):
utils.add_cmd(dummyf) utils.add_cmd(dummyf)
utils.add_cmd(dummyf, 'TEST') utils.add_cmd(dummyf, 'TEST')
# All command names should be automatically lowercased. # All command names should be automatically lowercased.
self.assertIn('dummyf', utils.bot_commands) self.assertIn('dummyf', world.bot_commands)
self.assertIn('test', utils.bot_commands) self.assertIn('test', world.bot_commands)
self.assertNotIn('TEST', utils.bot_commands) self.assertNotIn('TEST', world.bot_commands)
def test_add_hook(self): def test_add_hook(self):
utils.add_hook(dummyf, 'join') utils.add_hook(dummyf, 'join')
self.assertIn('JOIN', utils.command_hooks) self.assertIn('JOIN', world.command_hooks)
# Command names stored in uppercase. # Command names stored in uppercase.
self.assertNotIn('join', utils.command_hooks) self.assertNotIn('join', world.command_hooks)
self.assertIn(dummyf, utils.command_hooks['JOIN']) self.assertIn(dummyf, world.command_hooks['JOIN'])
def testIsNick(self): def testIsNick(self):
self.assertFalse(utils.isNick('abcdefgh', nicklen=3)) self.assertFalse(utils.isNick('abcdefgh', nicklen=3))
@ -96,5 +101,19 @@ class TestUtils(unittest.TestCase):
('+b', '*!*@*.badisp.net')]) ('+b', '*!*@*.badisp.net')])
self.assertEqual(res, '-o+l-nm+kb 9PYAAAAAA 50 hello *!*@*.badisp.net') self.assertEqual(res, '-o+l-nm+kb 9PYAAAAAA 50 hello *!*@*.badisp.net')
@unittest.skip('Wait, we need to work out the kinks first! (reversing changes of modes with arguments)')
def 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__': if __name__ == '__main__':
unittest.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 string
import re import re
from collections import defaultdict import inspect
import threading
from log import log from log import log
import world
global bot_commands, command_hooks # This is separate from classes.py to prevent import loops.
# This should be a mapping of command names to functions class NotAuthenticatedError(Exception):
bot_commands = {} pass
command_hooks = defaultdict(list)
networkobjects = {}
schedulers = {}
plugins = []
whois_handlers = []
started = threading.Event()
class TS6UIDGenerator(): class TS6UIDGenerator():
"""TS6 UID Generator module, adapted from InspIRCd source """TS6 UID Generator module, adapted from InspIRCd source
@ -114,12 +108,12 @@ def add_cmd(func, name=None):
if name is None: if name is None:
name = func.__name__ name = func.__name__
name = name.lower() name = name.lower()
bot_commands[name] = func world.bot_commands[name].append(func)
def add_hook(func, command): def add_hook(func, command):
"""Add a hook <func> for command <command>.""" """Add a hook <func> for command <command>."""
command = command.upper() command = command.upper()
command_hooks[command].append(func) world.command_hooks[command].append(func)
def toLower(irc, text): def toLower(irc, text):
"""<irc object> <text> """<irc object> <text>
@ -168,7 +162,7 @@ def isServerName(s):
return _isASCII(s) and '.' in s and not s.startswith('.') return _isASCII(s) and '.' in s and not s.startswith('.')
def parseModes(irc, target, args): 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')] ['+mitl-o', '3', 'person'] => [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')]
""" """
# http://www.irc.org/tech_docs/005.html # http://www.irc.org/tech_docs/005.html
@ -340,6 +334,34 @@ def joinModes(modes):
modelist += ' %s' % ' '.join(args) modelist += ' %s' % ' '.join(args)
return modelist 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): def isInternalClient(irc, numeric):
"""<irc object> <client numeric> """<irc object> <client numeric>
@ -357,15 +379,38 @@ def isInternalServer(irc, sid):
""" """
return (sid in irc.servers and irc.servers[sid].internal) 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> """<irc object> <UID>
Returns whether <UID> has operator status on PyLink. This can be achieved Returns whether <UID> has operator status on PyLink. This can be achieved
by either identifying to PyLink as admin, 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): def getHostmask(irc, user):
"""<irc object> <UID>
Gets the hostmask of user <UID>, if present."""
userobj = irc.users.get(user) userobj = irc.users.get(user)
if userobj is None: if userobj is None:
return '<user object not found>' return '<user object not found>'

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()