mirror of
https://github.com/jlu5/PyLink.git
synced 2024-12-25 12:12:53 +01:00
Merge commit '320de2079a78202e99c7b6aeb53c28c13f43ba47'
Many things here, including: - New 'exec' plugin - INVITE, umode +H (hideoper) support for relay - New and improved 'showuser' command, now with internals that support multiple binds to one command name. - relay: bug fixes, like not sending empty user mode changes.
This commit is contained in:
commit
d6cb9d45c7
305
classes.py
305
classes.py
@ -1,9 +1,263 @@
|
||||
import threading
|
||||
from random import randint
|
||||
import time
|
||||
import socket
|
||||
import threading
|
||||
import ssl
|
||||
from collections import defaultdict
|
||||
import hashlib
|
||||
|
||||
from log import log
|
||||
import main
|
||||
import time
|
||||
from conf import conf
|
||||
import world
|
||||
|
||||
### Exceptions
|
||||
|
||||
class ProtocolError(Exception):
|
||||
pass
|
||||
|
||||
### Internal classes (users, servers, channels)
|
||||
|
||||
class Irc():
|
||||
def initVars(self):
|
||||
self.pseudoclient = None
|
||||
self.connected = threading.Event()
|
||||
self.lastping = time.time()
|
||||
# Server, channel, and user indexes to be populated by our protocol module
|
||||
self.servers = {self.sid: IrcServer(None, self.serverdata['hostname'], internal=True)}
|
||||
self.users = {}
|
||||
self.channels = defaultdict(IrcChannel)
|
||||
# Sets flags such as whether to use halfops, etc. The default RFC1459
|
||||
# modes are implied.
|
||||
self.cmodes = {'op': 'o', 'secret': 's', 'private': 'p',
|
||||
'noextmsg': 'n', 'moderated': 'm', 'inviteonly': 'i',
|
||||
'topiclock': 't', 'limit': 'l', 'ban': 'b',
|
||||
'voice': 'v', 'key': 'k',
|
||||
# Type A, B, and C modes
|
||||
'*A': 'b',
|
||||
'*B': 'k',
|
||||
'*C': 'l',
|
||||
'*D': 'imnpstr'}
|
||||
self.umodes = {'invisible': 'i', 'snomask': 's', 'wallops': 'w',
|
||||
'oper': 'o',
|
||||
'*A': '', '*B': '', '*C': 's', '*D': 'iow'}
|
||||
|
||||
# This max nick length starts off as the config value, but may be
|
||||
# overwritten later by the protocol module if such information is
|
||||
# received. Note that only some IRCds (InspIRCd) give us nick length
|
||||
# during link, so it is still required that the config value be set!
|
||||
self.maxnicklen = self.serverdata['maxnicklen']
|
||||
self.prefixmodes = {'o': '@', 'v': '+'}
|
||||
|
||||
# Uplink SID (filled in by protocol module)
|
||||
self.uplink = None
|
||||
self.start_ts = int(time.time())
|
||||
|
||||
# UID generators, for servers that need it
|
||||
self.uidgen = {}
|
||||
|
||||
def __init__(self, netname, proto):
|
||||
# Initialize some variables
|
||||
self.name = netname.lower()
|
||||
self.conf = conf
|
||||
self.serverdata = conf['servers'][netname]
|
||||
self.sid = self.serverdata["sid"]
|
||||
self.botdata = conf['bot']
|
||||
self.proto = proto
|
||||
self.pingfreq = self.serverdata.get('pingfreq') or 30
|
||||
self.pingtimeout = self.pingfreq * 2
|
||||
|
||||
self.initVars()
|
||||
|
||||
if world.testing:
|
||||
# HACK: Don't thread if we're running tests.
|
||||
self.connect()
|
||||
else:
|
||||
self.connection_thread = threading.Thread(target = self.connect)
|
||||
self.connection_thread.start()
|
||||
self.pingTimer = None
|
||||
|
||||
def connect(self):
|
||||
ip = self.serverdata["ip"]
|
||||
port = self.serverdata["port"]
|
||||
while True:
|
||||
self.initVars()
|
||||
checks_ok = True
|
||||
try:
|
||||
self.socket = socket.socket()
|
||||
self.socket.setblocking(0)
|
||||
# Initial connection timeout is a lot smaller than the timeout after
|
||||
# we've connected; this is intentional.
|
||||
self.socket.settimeout(self.pingfreq)
|
||||
self.ssl = self.serverdata.get('ssl')
|
||||
if self.ssl:
|
||||
log.info('(%s) Attempting SSL for this connection...', self.name)
|
||||
certfile = self.serverdata.get('ssl_certfile')
|
||||
keyfile = self.serverdata.get('ssl_keyfile')
|
||||
if certfile and keyfile:
|
||||
try:
|
||||
self.socket = ssl.wrap_socket(self.socket,
|
||||
certfile=certfile,
|
||||
keyfile=keyfile)
|
||||
except OSError:
|
||||
log.exception('(%s) Caught OSError trying to '
|
||||
'initialize the SSL connection; '
|
||||
'are "ssl_certfile" and '
|
||||
'"ssl_keyfile" set correctly?',
|
||||
self.name)
|
||||
checks_ok = False
|
||||
else:
|
||||
log.error('(%s) SSL certfile/keyfile was not set '
|
||||
'correctly, aborting... ', self.name)
|
||||
checks_ok = False
|
||||
log.info("Connecting to network %r on %s:%s", self.name, ip, port)
|
||||
self.socket.connect((ip, port))
|
||||
self.socket.settimeout(self.pingtimeout)
|
||||
|
||||
if self.ssl and checks_ok:
|
||||
peercert = self.socket.getpeercert(binary_form=True)
|
||||
sha1fp = hashlib.sha1(peercert).hexdigest()
|
||||
expected_fp = self.serverdata.get('ssl_fingerprint')
|
||||
if expected_fp:
|
||||
if sha1fp != expected_fp:
|
||||
log.error('(%s) Uplink\'s SSL certificate '
|
||||
'fingerprint (SHA1) does not match the '
|
||||
'one configured: expected %r, got %r; '
|
||||
'disconnecting...', self.name,
|
||||
expected_fp, sha1fp)
|
||||
checks_ok = False
|
||||
else:
|
||||
log.info('(%s) Uplink SSL certificate fingerprint '
|
||||
'(SHA1) verified: %r', self.name, sha1fp)
|
||||
else:
|
||||
log.info('(%s) Uplink\'s SSL certificate fingerprint '
|
||||
'is %r. You can enhance the security of your '
|
||||
'link by specifying this in a "ssl_fingerprint"'
|
||||
' option in your server block.', self.name,
|
||||
sha1fp)
|
||||
|
||||
if checks_ok:
|
||||
self.proto.connect(self)
|
||||
self.spawnMain()
|
||||
log.info('(%s) Starting ping schedulers....', self.name)
|
||||
self.schedulePing()
|
||||
log.info('(%s) Server ready; listening for data.', self.name)
|
||||
self.run()
|
||||
else:
|
||||
log.error('(%s) A configuration error was encountered '
|
||||
'trying to set up this connection. Please check'
|
||||
' your configuration file and try again.',
|
||||
self.name)
|
||||
except (socket.error, ProtocolError, ConnectionError) as e:
|
||||
log.warning('(%s) Disconnected from IRC: %s: %s',
|
||||
self.name, type(e).__name__, str(e))
|
||||
self.disconnect()
|
||||
autoconnect = self.serverdata.get('autoconnect')
|
||||
log.debug('(%s) Autoconnect delay set to %s seconds.', self.name, autoconnect)
|
||||
if autoconnect is not None and autoconnect >= 0:
|
||||
log.info('(%s) Going to auto-reconnect in %s seconds.', self.name, autoconnect)
|
||||
time.sleep(autoconnect)
|
||||
else:
|
||||
return
|
||||
|
||||
def disconnect(self):
|
||||
log.debug('(%s) Canceling pingTimer at %s due to disconnect() call', self.name, time.time())
|
||||
self.connected.clear()
|
||||
try:
|
||||
self.socket.close()
|
||||
self.pingTimer.cancel()
|
||||
except: # Socket timed out during creation; ignore
|
||||
pass
|
||||
# Internal hook signifying that a network has disconnected.
|
||||
self.callHooks([None, 'PYLINK_DISCONNECT', {}])
|
||||
|
||||
def run(self):
|
||||
buf = b""
|
||||
data = b""
|
||||
while True:
|
||||
data = self.socket.recv(2048)
|
||||
buf += data
|
||||
if self.connected.is_set() and not data:
|
||||
log.warning('(%s) No data received and self.connected is set; disconnecting!', self.name)
|
||||
return
|
||||
elif (time.time() - self.lastping) > self.pingtimeout:
|
||||
log.warning('(%s) Connection timed out.', self.name)
|
||||
return
|
||||
while b'\n' in buf:
|
||||
line, buf = buf.split(b'\n', 1)
|
||||
line = line.strip(b'\r')
|
||||
# TODO: respect other encodings?
|
||||
line = line.decode("utf-8", "replace")
|
||||
log.debug("(%s) <- %s", self.name, line)
|
||||
hook_args = None
|
||||
try:
|
||||
hook_args = self.proto.handle_events(self, line)
|
||||
except Exception:
|
||||
log.exception('(%s) Caught error in handle_events, disconnecting!', self.name)
|
||||
return
|
||||
# Only call our hooks if there's data to process. Handlers that support
|
||||
# hooks will return a dict of parsed arguments, which can be passed on
|
||||
# to plugins and the like. For example, the JOIN handler will return
|
||||
# something like: {'channel': '#whatever', 'users': ['UID1', 'UID2',
|
||||
# 'UID3']}, etc.
|
||||
if hook_args is not None:
|
||||
self.callHooks(hook_args)
|
||||
|
||||
def callHooks(self, hook_args):
|
||||
numeric, command, parsed_args = hook_args
|
||||
# Always make sure TS is sent.
|
||||
if 'ts' not in parsed_args:
|
||||
parsed_args['ts'] = int(time.time())
|
||||
hook_cmd = command
|
||||
hook_map = self.proto.hook_map
|
||||
# Handlers can return a 'parse_as' key to send their payload to a
|
||||
# different hook. An example of this is "/join 0" being interpreted
|
||||
# as leaving all channels (PART).
|
||||
if command in hook_map:
|
||||
hook_cmd = hook_map[command]
|
||||
hook_cmd = parsed_args.get('parse_as') or hook_cmd
|
||||
log.debug('Parsed args %r received from %s handler (calling hook %s)', parsed_args, command, hook_cmd)
|
||||
# Iterate over hooked functions, catching errors accordingly
|
||||
for hook_func in world.command_hooks[hook_cmd]:
|
||||
try:
|
||||
log.debug('Calling function %s', hook_func)
|
||||
hook_func(self, numeric, command, parsed_args)
|
||||
except Exception:
|
||||
# We don't want plugins to crash our servers...
|
||||
log.exception('Unhandled exception caught in %r' % hook_func)
|
||||
continue
|
||||
|
||||
def send(self, data):
|
||||
# Safeguard against newlines in input!! Otherwise, each line gets
|
||||
# treated as a separate command, which is particularly nasty.
|
||||
data = data.replace('\n', ' ')
|
||||
data = data.encode("utf-8") + b"\n"
|
||||
stripped_data = data.decode("utf-8").strip("\n")
|
||||
log.debug("(%s) -> %s", self.name, stripped_data)
|
||||
try:
|
||||
self.socket.send(data)
|
||||
except (OSError, AttributeError):
|
||||
log.debug("(%s) Dropping message %r; network isn't connected!", self.name, stripped_data)
|
||||
|
||||
def schedulePing(self):
|
||||
self.proto.pingServer(self)
|
||||
self.pingTimer = threading.Timer(self.pingfreq, self.schedulePing)
|
||||
self.pingTimer.daemon = True
|
||||
self.pingTimer.start()
|
||||
log.debug('(%s) Ping scheduled at %s', self.name, time.time())
|
||||
|
||||
def spawnMain(self):
|
||||
nick = self.botdata.get('nick') or 'PyLink'
|
||||
ident = self.botdata.get('ident') or 'pylink'
|
||||
host = self.serverdata["hostname"]
|
||||
log.info('(%s) Connected! Spawning main client %s.', self.name, nick)
|
||||
olduserobj = self.pseudoclient
|
||||
self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("+o", None)})
|
||||
for chan in self.serverdata['channels']:
|
||||
self.proto.joinClient(self, self.pseudoclient.uid, chan)
|
||||
# PyLink internal hook called when spawnMain is called and the
|
||||
# contents of Irc().pseudoclient change.
|
||||
self.callHooks([self.sid, 'PYLINK_SPAWNMAIN', {'olduser': olduserobj}])
|
||||
|
||||
class IrcUser():
|
||||
def __init__(self, nick, ts, uid, ident='null', host='null',
|
||||
@ -36,7 +290,7 @@ class IrcServer():
|
||||
"""
|
||||
def __init__(self, uplink, name, internal=False):
|
||||
self.uplink = uplink
|
||||
self.users = []
|
||||
self.users = set()
|
||||
self.internal = internal
|
||||
self.name = name.lower()
|
||||
def __repr__(self):
|
||||
@ -45,7 +299,7 @@ class IrcServer():
|
||||
class IrcChannel():
|
||||
def __init__(self):
|
||||
self.users = set()
|
||||
self.modes = set()
|
||||
self.modes = {('n', None), ('t', None)}
|
||||
self.topic = ''
|
||||
self.ts = int(time.time())
|
||||
self.topicset = False
|
||||
@ -60,33 +314,9 @@ class IrcChannel():
|
||||
s.discard(target)
|
||||
self.users.discard(target)
|
||||
|
||||
class ProtocolError(Exception):
|
||||
pass
|
||||
### FakeIRC classes, used for test cases
|
||||
|
||||
global testconf
|
||||
testconf = {'bot':
|
||||
{
|
||||
'nick': 'PyLink',
|
||||
'user': 'pylink',
|
||||
'realname': 'PyLink Service Client',
|
||||
'loglevel': 'DEBUG',
|
||||
},
|
||||
'servers':
|
||||
{'unittest':
|
||||
{
|
||||
'ip': '0.0.0.0',
|
||||
'port': 7000,
|
||||
'recvpass': "abcd",
|
||||
'sendpass': "abcd",
|
||||
'protocol': "null",
|
||||
'hostname': "pylink.unittest",
|
||||
'sid': "9PY",
|
||||
'channels': ["#pylink"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
class FakeIRC(main.Irc):
|
||||
class FakeIRC(Irc):
|
||||
def connect(self):
|
||||
self.messages = []
|
||||
self.hookargs = []
|
||||
@ -100,7 +330,10 @@ class FakeIRC(main.Irc):
|
||||
def run(self, data):
|
||||
"""Queues a message to the fake IRC server."""
|
||||
log.debug('<- ' + data)
|
||||
self.proto.handle_events(self, data)
|
||||
hook_args = self.proto.handle_events(self, data)
|
||||
if hook_args is not None:
|
||||
self.hookmsgs.append(hook_args)
|
||||
self.callHooks(hook_args)
|
||||
|
||||
def send(self, data):
|
||||
self.messages.append(data)
|
||||
@ -132,13 +365,13 @@ class FakeIRC(main.Irc):
|
||||
self.hookmsgs = []
|
||||
return hookmsgs
|
||||
|
||||
@staticmethod
|
||||
def dummyhook(irc, source, command, parsed_args):
|
||||
"""Dummy function to bind to hooks."""
|
||||
irc.hookmsgs.append(parsed_args)
|
||||
|
||||
class FakeProto():
|
||||
"""Dummy protocol module for testing purposes."""
|
||||
def __init__(self):
|
||||
self.hook_map = {}
|
||||
self.casemapping = 'rfc1459'
|
||||
self.__name__ = 'FakeProto'
|
||||
|
||||
@staticmethod
|
||||
def handle_events(irc, data):
|
||||
pass
|
||||
|
46
conf.py
46
conf.py
@ -1,19 +1,53 @@
|
||||
import yaml
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
global confname
|
||||
try:
|
||||
import world
|
||||
|
||||
global testconf
|
||||
testconf = {'bot':
|
||||
{
|
||||
'nick': 'PyLink',
|
||||
'user': 'pylink',
|
||||
'realname': 'PyLink Service Client',
|
||||
# Suppress logging in the test output for the most part.
|
||||
'loglevel': 'CRITICAL',
|
||||
'serverdesc': 'PyLink unit tests'
|
||||
},
|
||||
'servers':
|
||||
# Wildcard defaultdict! This means that
|
||||
# any network name you try will work and return
|
||||
# this basic template:
|
||||
defaultdict(lambda: {
|
||||
'ip': '0.0.0.0',
|
||||
'port': 7000,
|
||||
'recvpass': "abcd",
|
||||
'sendpass': "chucknorris",
|
||||
'protocol': "null",
|
||||
'hostname': "pylink.unittest",
|
||||
'sid': "9PY",
|
||||
'channels': ["#pylink"],
|
||||
'maxnicklen': 20
|
||||
})
|
||||
}
|
||||
if world.testing:
|
||||
conf = testconf
|
||||
confname = 'testconf'
|
||||
else:
|
||||
try:
|
||||
# Get the config name from the command line, falling back to config.yml
|
||||
# if not given.
|
||||
fname = sys.argv[1]
|
||||
confname = fname.split('.', 1)[0]
|
||||
except IndexError:
|
||||
except IndexError:
|
||||
# confname is used for logging and PID writing, so that each
|
||||
# instance uses its own files. fname is the actual name of the file
|
||||
# we load.
|
||||
confname = 'pylink'
|
||||
fname = 'config.yml'
|
||||
|
||||
with open(fname, 'r') as f:
|
||||
global conf
|
||||
with open(fname, 'r') as f:
|
||||
try:
|
||||
conf = yaml.load(f)
|
||||
except Exception as e:
|
||||
print('ERROR: Failed to load config from %r: %s: %s' % (fname, type(e).__name__, e))
|
||||
sys.exit(4)
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import utils
|
||||
from log import log
|
||||
import world
|
||||
|
||||
# Handle KILLs sent to the PyLink client and respawn
|
||||
def handle_kill(irc, source, command, args):
|
||||
@ -24,13 +25,12 @@ def handle_commands(irc, source, command, args):
|
||||
cmd_args = text.split(' ')
|
||||
cmd = cmd_args[0].lower()
|
||||
cmd_args = cmd_args[1:]
|
||||
try:
|
||||
func = utils.bot_commands[cmd]
|
||||
except KeyError:
|
||||
utils.msg(irc, source, 'Unknown command %r.' % cmd)
|
||||
if cmd not in world.bot_commands:
|
||||
utils.msg(irc, source, 'Error: Unknown command %r.' % cmd)
|
||||
return
|
||||
try:
|
||||
log.info('(%s) Calling command %r for %s', irc.name, cmd, utils.getHostmask(irc, source))
|
||||
for func in world.bot_commands[cmd]:
|
||||
try:
|
||||
func(irc, source, cmd_args)
|
||||
except Exception as e:
|
||||
log.exception('Unhandled exception caught in command %r', cmd)
|
||||
@ -75,7 +75,13 @@ def handle_whois(irc, source, command, args):
|
||||
# 313: sends a string denoting the target's operator privilege,
|
||||
# only if they have umode +o.
|
||||
if ('o', None) in user.modes:
|
||||
f(irc, server, 313, source, "%s :is an IRC Operator" % nick)
|
||||
if hasattr(user, 'opertype'):
|
||||
opertype = user.opertype.replace("_", " ")
|
||||
else:
|
||||
opertype = "IRC Operator"
|
||||
# Let's be gramatically correct.
|
||||
n = 'n' if opertype[0].lower() in 'aeiou' else ''
|
||||
f(irc, server, 313, source, "%s :is a%s %s" % (nick, n, opertype))
|
||||
# 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd.
|
||||
# Only show this to opers!
|
||||
if sourceisOper:
|
||||
@ -84,7 +90,7 @@ def handle_whois(irc, source, command, args):
|
||||
# idle time, so we simply return 0.
|
||||
# <- 317 GL GL 15 1437632859 :seconds idle, signon time
|
||||
f(irc, server, 317, source, "%s 0 %s :seconds idle, signon time" % (nick, user.ts))
|
||||
for func in utils.whois_handlers:
|
||||
for func in world.whois_handlers:
|
||||
# Iterate over custom plugin WHOIS handlers. They return a tuple
|
||||
# or list with two arguments: the numeric, and the text to send.
|
||||
try:
|
||||
|
261
main.py
261
main.py
@ -2,258 +2,17 @@
|
||||
|
||||
import imp
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
import threading
|
||||
import ssl
|
||||
import hashlib
|
||||
|
||||
from log import log
|
||||
# This must be done before conf imports, so we get the real conf instead of testing one.
|
||||
import world
|
||||
world.testing = False
|
||||
|
||||
import conf
|
||||
from log import log
|
||||
import classes
|
||||
import utils
|
||||
import coreplugin
|
||||
|
||||
class Irc():
|
||||
|
||||
def initVars(self):
|
||||
self.pseudoclient = None
|
||||
self.connected = threading.Event()
|
||||
self.lastping = time.time()
|
||||
# Server, channel, and user indexes to be populated by our protocol module
|
||||
self.servers = {self.sid: classes.IrcServer(None, self.serverdata['hostname'], internal=True)}
|
||||
self.users = {}
|
||||
self.channels = defaultdict(classes.IrcChannel)
|
||||
# Sets flags such as whether to use halfops, etc. The default RFC1459
|
||||
# modes are implied.
|
||||
self.cmodes = {'op': 'o', 'secret': 's', 'private': 'p',
|
||||
'noextmsg': 'n', 'moderated': 'm', 'inviteonly': 'i',
|
||||
'topiclock': 't', 'limit': 'l', 'ban': 'b',
|
||||
'voice': 'v', 'key': 'k',
|
||||
# Type A, B, and C modes
|
||||
'*A': 'b',
|
||||
'*B': 'k',
|
||||
'*C': 'l',
|
||||
'*D': 'imnpstr'}
|
||||
self.umodes = {'invisible': 'i', 'snomask': 's', 'wallops': 'w',
|
||||
'oper': 'o',
|
||||
'*A': '', '*B': '', '*C': 's', '*D': 'iow'}
|
||||
|
||||
# This max nick length starts off as the config value, but may be
|
||||
# overwritten later by the protocol module if such information is
|
||||
# received. Note that only some IRCds (InspIRCd) give us nick length
|
||||
# during link, so it is still required that the config value be set!
|
||||
self.maxnicklen = self.serverdata['maxnicklen']
|
||||
self.prefixmodes = {'o': '@', 'v': '+'}
|
||||
|
||||
# Uplink SID (filled in by protocol module)
|
||||
self.uplink = None
|
||||
self.start_ts = int(time.time())
|
||||
|
||||
# UID generators, for servers that need it
|
||||
self.uidgen = {}
|
||||
|
||||
def __init__(self, netname, proto, conf):
|
||||
# Initialize some variables
|
||||
self.name = netname.lower()
|
||||
self.conf = conf
|
||||
self.serverdata = conf['servers'][netname]
|
||||
self.sid = self.serverdata["sid"]
|
||||
self.botdata = conf['bot']
|
||||
self.proto = proto
|
||||
self.pingfreq = self.serverdata.get('pingfreq') or 30
|
||||
self.pingtimeout = self.pingfreq * 2
|
||||
|
||||
self.initVars()
|
||||
|
||||
self.connection_thread = threading.Thread(target = self.connect)
|
||||
self.connection_thread.start()
|
||||
self.pingTimer = None
|
||||
|
||||
def connect(self):
|
||||
ip = self.serverdata["ip"]
|
||||
port = self.serverdata["port"]
|
||||
while True:
|
||||
self.initVars()
|
||||
checks_ok = True
|
||||
try:
|
||||
self.socket = socket.socket()
|
||||
self.socket.setblocking(0)
|
||||
# Initial connection timeout is a lot smaller than the timeout after
|
||||
# we've connected; this is intentional.
|
||||
self.socket.settimeout(self.pingfreq)
|
||||
self.ssl = self.serverdata.get('ssl')
|
||||
if self.ssl:
|
||||
log.info('(%s) Attempting SSL for this connection...', self.name)
|
||||
certfile = self.serverdata.get('ssl_certfile')
|
||||
keyfile = self.serverdata.get('ssl_keyfile')
|
||||
if certfile and keyfile:
|
||||
try:
|
||||
self.socket = ssl.wrap_socket(self.socket,
|
||||
certfile=certfile,
|
||||
keyfile=keyfile)
|
||||
except OSError:
|
||||
log.exception('(%s) Caught OSError trying to '
|
||||
'initialize the SSL connection; '
|
||||
'are "ssl_certfile" and '
|
||||
'"ssl_keyfile" set correctly?',
|
||||
self.name)
|
||||
checks_ok = False
|
||||
else:
|
||||
log.error('(%s) SSL certfile/keyfile was not set '
|
||||
'correctly, aborting... ', self.name)
|
||||
checks_ok = False
|
||||
log.info("Connecting to network %r on %s:%s", self.name, ip, port)
|
||||
self.socket.connect((ip, port))
|
||||
self.socket.settimeout(self.pingtimeout)
|
||||
|
||||
if self.ssl and checks_ok:
|
||||
peercert = self.socket.getpeercert(binary_form=True)
|
||||
sha1fp = hashlib.sha1(peercert).hexdigest()
|
||||
expected_fp = self.serverdata.get('ssl_fingerprint')
|
||||
if expected_fp:
|
||||
if sha1fp != expected_fp:
|
||||
log.error('(%s) Uplink\'s SSL certificate '
|
||||
'fingerprint (SHA1) does not match the '
|
||||
'one configured: expected %r, got %r; '
|
||||
'disconnecting...', self.name,
|
||||
expected_fp, sha1fp)
|
||||
checks_ok = False
|
||||
else:
|
||||
log.info('(%s) Uplink SSL certificate fingerprint '
|
||||
'(SHA1) verified: %r', self.name, sha1fp)
|
||||
else:
|
||||
log.info('(%s) Uplink\'s SSL certificate fingerprint '
|
||||
'is %r. You can enhance the security of your '
|
||||
'link by specifying this in a "ssl_fingerprint"'
|
||||
' option in your server block.', self.name,
|
||||
sha1fp)
|
||||
|
||||
if checks_ok:
|
||||
self.proto.connect(self)
|
||||
self.spawnMain()
|
||||
log.info('(%s) Starting ping schedulers....', self.name)
|
||||
self.schedulePing()
|
||||
log.info('(%s) Server ready; listening for data.', self.name)
|
||||
self.run()
|
||||
else:
|
||||
log.error('(%s) A configuration error was encountered '
|
||||
'trying to set up this connection. Please check'
|
||||
' your configuration file and try again.',
|
||||
self.name)
|
||||
except (socket.error, classes.ProtocolError, ConnectionError) as e:
|
||||
log.warning('(%s) Disconnected from IRC: %s: %s',
|
||||
self.name, type(e).__name__, str(e))
|
||||
self.disconnect()
|
||||
autoconnect = self.serverdata.get('autoconnect')
|
||||
log.debug('(%s) Autoconnect delay set to %s seconds.', self.name, autoconnect)
|
||||
if autoconnect is not None and autoconnect >= 0:
|
||||
log.info('(%s) Going to auto-reconnect in %s seconds.', self.name, autoconnect)
|
||||
time.sleep(autoconnect)
|
||||
else:
|
||||
return
|
||||
|
||||
def disconnect(self):
|
||||
log.debug('(%s) Canceling pingTimer at %s due to disconnect() call', self.name, time.time())
|
||||
self.connected.clear()
|
||||
try:
|
||||
self.socket.close()
|
||||
self.pingTimer.cancel()
|
||||
except: # Socket timed out during creation; ignore
|
||||
pass
|
||||
# Internal hook signifying that a network has disconnected.
|
||||
self.callHooks([None, 'PYLINK_DISCONNECT', {}])
|
||||
|
||||
def run(self):
|
||||
buf = b""
|
||||
data = b""
|
||||
while True:
|
||||
data = self.socket.recv(2048)
|
||||
buf += data
|
||||
if self.connected.is_set() and not data:
|
||||
log.warning('(%s) No data received and self.connected is set; disconnecting!', self.name)
|
||||
return
|
||||
elif (time.time() - self.lastping) > self.pingtimeout:
|
||||
log.warning('(%s) Connection timed out.', self.name)
|
||||
return
|
||||
while b'\n' in buf:
|
||||
line, buf = buf.split(b'\n', 1)
|
||||
line = line.strip(b'\r')
|
||||
# TODO: respect other encodings?
|
||||
line = line.decode("utf-8", "replace")
|
||||
log.debug("(%s) <- %s", self.name, line)
|
||||
hook_args = None
|
||||
try:
|
||||
hook_args = self.proto.handle_events(self, line)
|
||||
except Exception:
|
||||
log.exception('(%s) Caught error in handle_events, disconnecting!', self.name)
|
||||
return
|
||||
# Only call our hooks if there's data to process. Handlers that support
|
||||
# hooks will return a dict of parsed arguments, which can be passed on
|
||||
# to plugins and the like. For example, the JOIN handler will return
|
||||
# something like: {'channel': '#whatever', 'users': ['UID1', 'UID2',
|
||||
# 'UID3']}, etc.
|
||||
if hook_args is not None:
|
||||
self.callHooks(hook_args)
|
||||
|
||||
def callHooks(self, hook_args):
|
||||
numeric, command, parsed_args = hook_args
|
||||
# Always make sure TS is sent.
|
||||
if 'ts' not in parsed_args:
|
||||
parsed_args['ts'] = int(time.time())
|
||||
hook_cmd = command
|
||||
hook_map = self.proto.hook_map
|
||||
# Handlers can return a 'parse_as' key to send their payload to a
|
||||
# different hook. An example of this is "/join 0" being interpreted
|
||||
# as leaving all channels (PART).
|
||||
if command in hook_map:
|
||||
hook_cmd = hook_map[command]
|
||||
hook_cmd = parsed_args.get('parse_as') or hook_cmd
|
||||
log.debug('Parsed args %r received from %s handler (calling hook %s)', parsed_args, command, hook_cmd)
|
||||
# Iterate over hooked functions, catching errors accordingly
|
||||
for hook_func in utils.command_hooks[hook_cmd]:
|
||||
try:
|
||||
log.debug('Calling function %s', hook_func)
|
||||
hook_func(self, numeric, command, parsed_args)
|
||||
except Exception:
|
||||
# We don't want plugins to crash our servers...
|
||||
log.exception('Unhandled exception caught in %r' % hook_func)
|
||||
continue
|
||||
|
||||
def send(self, data):
|
||||
# Safeguard against newlines in input!! Otherwise, each line gets
|
||||
# treated as a separate command, which is particularly nasty.
|
||||
data = data.replace('\n', ' ')
|
||||
data = data.encode("utf-8") + b"\n"
|
||||
stripped_data = data.decode("utf-8").strip("\n")
|
||||
log.debug("(%s) -> %s", self.name, stripped_data)
|
||||
try:
|
||||
self.socket.send(data)
|
||||
except (OSError, AttributeError):
|
||||
log.debug("(%s) Dropping message %r; network isn't connected!", self.name, stripped_data)
|
||||
|
||||
def schedulePing(self):
|
||||
self.proto.pingServer(self)
|
||||
self.pingTimer = threading.Timer(self.pingfreq, self.schedulePing)
|
||||
self.pingTimer.daemon = True
|
||||
self.pingTimer.start()
|
||||
log.debug('(%s) Ping scheduled at %s', self.name, time.time())
|
||||
|
||||
def spawnMain(self):
|
||||
nick = self.botdata.get('nick') or 'PyLink'
|
||||
ident = self.botdata.get('ident') or 'pylink'
|
||||
host = self.serverdata["hostname"]
|
||||
log.info('(%s) Connected! Spawning main client %s.', self.name, nick)
|
||||
olduserobj = self.pseudoclient
|
||||
self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("+o", None)})
|
||||
for chan in self.serverdata['channels']:
|
||||
self.proto.joinClient(self, self.pseudoclient.uid, chan)
|
||||
# PyLink internal hook called when spawnMain is called and the
|
||||
# contents of Irc().pseudoclient change.
|
||||
self.callHooks([self.sid, 'PYLINK_SPAWNMAIN', {'olduser': olduserobj}])
|
||||
|
||||
if __name__ == '__main__':
|
||||
log.info('PyLink starting...')
|
||||
if conf.conf['login']['password'] == 'changeme':
|
||||
@ -267,7 +26,7 @@ if __name__ == '__main__':
|
||||
|
||||
# Import plugins first globally, because they can listen for events
|
||||
# that happen before the connection phase.
|
||||
utils.plugins.append(coreplugin)
|
||||
world.plugins.append(coreplugin)
|
||||
to_load = conf.conf['plugins']
|
||||
plugins_folder = [os.path.join(os.getcwd(), 'plugins')]
|
||||
# Here, we override the module lookup and import the plugins
|
||||
@ -276,7 +35,7 @@ if __name__ == '__main__':
|
||||
try:
|
||||
moduleinfo = imp.find_module(plugin, plugins_folder)
|
||||
pl = imp.load_source(plugin, moduleinfo[1])
|
||||
utils.plugins.append(pl)
|
||||
world.plugins.append(pl)
|
||||
except ImportError as e:
|
||||
if str(e) == ('No module named %r' % plugin):
|
||||
log.error('Failed to load plugin %r: The plugin could not be found.', plugin)
|
||||
@ -299,7 +58,7 @@ if __name__ == '__main__':
|
||||
log.critical('Failed to load protocol module: ImportError: %s', protoname, str(e))
|
||||
sys.exit(2)
|
||||
else:
|
||||
utils.networkobjects[network] = Irc(network, proto, conf.conf)
|
||||
utils.started.set()
|
||||
log.info("loaded plugins: %s", utils.plugins)
|
||||
world.networkobjects[network] = classes.Irc(network, proto)
|
||||
world.started.set()
|
||||
log.info("loaded plugins: %s", world.plugins)
|
||||
|
||||
|
@ -1,47 +1,22 @@
|
||||
# admin.py: PyLink administrative commands
|
||||
import sys
|
||||
import os
|
||||
import inspect
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import utils
|
||||
from log import log
|
||||
|
||||
class NotAuthenticatedError(Exception):
|
||||
pass
|
||||
|
||||
def checkauthenticated(irc, source):
|
||||
lastfunc = inspect.stack()[1][3]
|
||||
if not irc.users[source].identified:
|
||||
log.warning('(%s) Access denied for %s calling %r', irc.name,
|
||||
utils.getHostmask(irc, source), lastfunc)
|
||||
raise NotAuthenticatedError("You are not authenticated!")
|
||||
|
||||
def _exec(irc, source, args):
|
||||
"""<code>
|
||||
|
||||
Admin-only. Executes <code> in the current PyLink instance.
|
||||
\x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02"""
|
||||
checkauthenticated(irc, source)
|
||||
args = ' '.join(args)
|
||||
if not args.strip():
|
||||
utils.msg(irc, source, 'No code entered!')
|
||||
return
|
||||
log.info('(%s) Executing %r for %s', irc.name, args, utils.getHostmask(irc, source))
|
||||
exec(args, globals(), locals())
|
||||
utils.add_cmd(_exec, 'exec')
|
||||
|
||||
@utils.add_cmd
|
||||
def spawnclient(irc, source, args):
|
||||
"""<nick> <ident> <host>
|
||||
|
||||
Admin-only. Spawns the specified PseudoClient on the PyLink server.
|
||||
Note: this doesn't check the validity of any fields you give it!"""
|
||||
checkauthenticated(irc, source)
|
||||
utils.checkAuthenticated(irc, source, allowOper=False)
|
||||
try:
|
||||
nick, ident, host = args[:3]
|
||||
except ValueError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 3: nick, user, host.")
|
||||
utils.msg(irc, source, "Error: Not enough arguments. Needs 3: nick, user, host.")
|
||||
return
|
||||
irc.proto.spawnClient(irc, nick, ident, host)
|
||||
|
||||
@ -50,17 +25,17 @@ def quit(irc, source, args):
|
||||
"""<target> [<reason>]
|
||||
|
||||
Admin-only. Quits the PyLink client with nick <target>, if one exists."""
|
||||
checkauthenticated(irc, source)
|
||||
utils.checkAuthenticated(irc, source, allowOper=False)
|
||||
try:
|
||||
nick = args[0]
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 1-2: nick, reason (optional).")
|
||||
utils.msg(irc, source, "Error: Not enough arguments. Needs 1-2: nick, reason (optional).")
|
||||
return
|
||||
if irc.pseudoclient.uid == utils.nickToUid(irc, nick):
|
||||
utils.msg(irc, source, "Error: cannot quit the main PyLink PseudoClient!")
|
||||
utils.msg(irc, source, "Error: Cannot quit the main PyLink PseudoClient!")
|
||||
return
|
||||
u = utils.nickToUid(irc, nick)
|
||||
quitmsg = ' '.join(args[1:]) or 'Client quit'
|
||||
quitmsg = ' '.join(args[1:]) or 'Client Quit'
|
||||
irc.proto.quitClient(irc, u, quitmsg)
|
||||
irc.callHooks([u, 'PYLINK_ADMIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}])
|
||||
|
||||
@ -68,14 +43,14 @@ def joinclient(irc, source, args):
|
||||
"""<target> <channel1>,[<channel2>], etc.
|
||||
|
||||
Admin-only. Joins <target>, the nick of a PyLink client, to a comma-separated list of channels."""
|
||||
checkauthenticated(irc, source)
|
||||
utils.checkAuthenticated(irc, source, allowOper=False)
|
||||
try:
|
||||
nick = args[0]
|
||||
clist = args[1].split(',')
|
||||
if not clist:
|
||||
raise IndexError
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 2: nick, comma separated list of channels.")
|
||||
utils.msg(irc, source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
|
||||
return
|
||||
u = utils.nickToUid(irc, nick)
|
||||
for channel in clist:
|
||||
@ -93,12 +68,12 @@ def nick(irc, source, args):
|
||||
"""<target> <newnick>
|
||||
|
||||
Admin-only. Changes the nick of <target>, a PyLink client, to <newnick>."""
|
||||
checkauthenticated(irc, source)
|
||||
utils.checkAuthenticated(irc, source, allowOper=False)
|
||||
try:
|
||||
nick = args[0]
|
||||
newnick = args[1]
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 2: nick, newnick.")
|
||||
utils.msg(irc, source, "Error: Not enough arguments. Needs 2: nick, newnick.")
|
||||
return
|
||||
u = utils.nickToUid(irc, nick)
|
||||
if newnick in ('0', u):
|
||||
@ -114,13 +89,13 @@ def part(irc, source, args):
|
||||
"""<target> <channel1>,[<channel2>],... [<reason>]
|
||||
|
||||
Admin-only. Parts <target>, the nick of a PyLink client, from a comma-separated list of channels."""
|
||||
checkauthenticated(irc, source)
|
||||
utils.checkAuthenticated(irc, source, allowOper=False)
|
||||
try:
|
||||
nick = args[0]
|
||||
clist = args[1].split(',')
|
||||
reason = ' '.join(args[2:])
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 2: nick, comma separated list of channels.")
|
||||
utils.msg(irc, source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
|
||||
return
|
||||
u = utils.nickToUid(irc, nick)
|
||||
for channel in clist:
|
||||
@ -135,14 +110,14 @@ def kick(irc, source, args):
|
||||
"""<source> <channel> <user> [<reason>]
|
||||
|
||||
Admin-only. Kicks <user> from <channel> via <source>, where <source> is the nick of a PyLink client."""
|
||||
checkauthenticated(irc, source)
|
||||
utils.checkAuthenticated(irc, source, allowOper=False)
|
||||
try:
|
||||
nick = args[0]
|
||||
channel = args[1]
|
||||
target = args[2]
|
||||
reason = ' '.join(args[3:])
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 3-4: source nick, channel, target, reason (optional).")
|
||||
utils.msg(irc, source, "Error: Not enough arguments. Needs 3-4: source nick, channel, target, reason (optional).")
|
||||
return
|
||||
u = utils.nickToUid(irc, nick) or nick
|
||||
targetu = utils.nickToUid(irc, target)
|
||||
@ -155,38 +130,19 @@ def kick(irc, source, args):
|
||||
irc.proto.kickClient(irc, u, channel, targetu, reason)
|
||||
irc.callHooks([u, 'PYLINK_ADMIN_KICK', {'channel': channel, 'target': targetu, 'text': reason, 'parse_as': 'KICK'}])
|
||||
|
||||
@utils.add_cmd
|
||||
def showuser(irc, source, args):
|
||||
"""<user>
|
||||
|
||||
Admin-only. Shows information about <user>."""
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
target = args[0]
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 1: nick.")
|
||||
return
|
||||
u = utils.nickToUid(irc, target)
|
||||
if u is None:
|
||||
utils.msg(irc, source, 'Error: unknown user %r' % target)
|
||||
return
|
||||
s = ['\x02%s\x02: %s' % (k, v) for k, v in sorted(irc.users[u].__dict__.items())]
|
||||
s = 'Information on user \x02%s\x02: %s' % (target, '; '.join(s))
|
||||
utils.msg(irc, source, s)
|
||||
|
||||
@utils.add_cmd
|
||||
def showchan(irc, source, args):
|
||||
"""<channel>
|
||||
|
||||
Admin-only. Shows information about <channel>."""
|
||||
checkauthenticated(irc, source)
|
||||
utils.checkAuthenticated(irc, source, allowOper=False)
|
||||
try:
|
||||
channel = args[0].lower()
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 1: channel.")
|
||||
utils.msg(irc, source, "Error: Not enough arguments. Needs 1: channel.")
|
||||
return
|
||||
if channel not in irc.channels:
|
||||
utils.msg(irc, source, 'Error: unknown channel %r' % channel)
|
||||
utils.msg(irc, source, 'Error: Unknown channel %r.' % channel)
|
||||
return
|
||||
s = ['\x02%s\x02: %s' % (k, v) for k, v in sorted(irc.channels[channel].__dict__.items())]
|
||||
s = 'Information on channel \x02%s\x02: %s' % (channel, '; '.join(s))
|
||||
@ -197,14 +153,14 @@ def mode(irc, source, args):
|
||||
"""<source> <target> <modes>
|
||||
|
||||
Admin-only. Sets modes <modes> on <target> from <source>, where <source> is either the nick of a PyLink client, or the SID of a PyLink server."""
|
||||
checkauthenticated(irc, source)
|
||||
utils.checkAuthenticated(irc, source, allowOper=False)
|
||||
try:
|
||||
modesource, target, modes = args[0], args[1], args[2:]
|
||||
except IndexError:
|
||||
utils.msg(irc, source, 'Error: not enough arguments. Needs 3: source nick, target, modes to set.')
|
||||
utils.msg(irc, source, 'Error: Not enough arguments. Needs 3: source nick, target, modes to set.')
|
||||
return
|
||||
if not modes:
|
||||
utils.msg(irc, source, "Error: no modes given to set!")
|
||||
utils.msg(irc, source, "Error: No modes given to set!")
|
||||
return
|
||||
parsedmodes = utils.parseModes(irc, target, modes)
|
||||
targetuid = utils.nickToUid(irc, target)
|
||||
@ -226,25 +182,25 @@ def msg(irc, source, args):
|
||||
"""<source> <target> <text>
|
||||
|
||||
Admin-only. Sends message <text> from <source>, where <source> is the nick of a PyLink client."""
|
||||
checkauthenticated(irc, source)
|
||||
utils.checkAuthenticated(irc, source, allowOper=False)
|
||||
try:
|
||||
msgsource, target, text = args[0], args[1], ' '.join(args[2:])
|
||||
except IndexError:
|
||||
utils.msg(irc, source, 'Error: not enough arguments. Needs 3: source nick, target, text.')
|
||||
utils.msg(irc, source, 'Error: Not enough arguments. Needs 3: source nick, target, text.')
|
||||
return
|
||||
sourceuid = utils.nickToUid(irc, msgsource)
|
||||
if not sourceuid:
|
||||
utils.msg(irc, source, 'Error: unknown user %r' % msgsource)
|
||||
utils.msg(irc, source, 'Error: Unknown user %r.' % msgsource)
|
||||
return
|
||||
if not utils.isChannel(target):
|
||||
real_target = utils.nickToUid(irc, target)
|
||||
if real_target is None:
|
||||
utils.msg(irc, source, 'Error: unknown user %r' % target)
|
||||
utils.msg(irc, source, 'Error: Unknown user %r.' % target)
|
||||
return
|
||||
else:
|
||||
real_target = target
|
||||
if not text:
|
||||
utils.msg(irc, source, 'Error: no text given.')
|
||||
utils.msg(irc, source, 'Error: No text given.')
|
||||
return
|
||||
irc.proto.messageClient(irc, sourceuid, real_target, text)
|
||||
irc.callHooks([sourceuid, 'PYLINK_ADMIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}])
|
||||
|
@ -1,11 +1,13 @@
|
||||
# commands.py: base PyLink commands
|
||||
import sys
|
||||
import os
|
||||
from time import ctime
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import utils
|
||||
from conf import conf
|
||||
from log import log
|
||||
import world
|
||||
|
||||
@utils.add_cmd
|
||||
def status(irc, source, args):
|
||||
@ -27,7 +29,7 @@ def identify(irc, source, args):
|
||||
try:
|
||||
username, password = args[0], args[1]
|
||||
except IndexError:
|
||||
utils.msg(irc, source, 'Error: not enough arguments.')
|
||||
utils.msg(irc, source, 'Error: Not enough arguments.')
|
||||
return
|
||||
# Usernames are case-insensitive, passwords are NOT.
|
||||
if username.lower() == conf['login']['user'].lower() and password == conf['login']['password']:
|
||||
@ -37,7 +39,7 @@ def identify(irc, source, args):
|
||||
log.info("(%s) Successful login to %r by %s.",
|
||||
irc.name, username, utils.getHostmask(irc, source))
|
||||
else:
|
||||
utils.msg(irc, source, 'Incorrect credentials.')
|
||||
utils.msg(irc, source, 'Error: Incorrect credentials.')
|
||||
u = irc.users[source]
|
||||
log.warning("(%s) Failed login to %r from %s.",
|
||||
irc.name, username, utils.getHostmask(irc, source))
|
||||
@ -46,8 +48,12 @@ def listcommands(irc, source, args):
|
||||
"""takes no arguments.
|
||||
|
||||
Returns a list of available commands PyLink has to offer."""
|
||||
cmds = list(utils.bot_commands.keys())
|
||||
cmds = list(world.bot_commands.keys())
|
||||
cmds.sort()
|
||||
for idx, cmd in enumerate(cmds):
|
||||
nfuncs = len(world.bot_commands[cmd])
|
||||
if nfuncs > 1:
|
||||
cmds[idx] = '%s(x%s)' % (cmd, nfuncs)
|
||||
utils.msg(irc, source, 'Available commands include: %s' % ', '.join(cmds))
|
||||
utils.msg(irc, source, 'To see help on a specific command, type \x02help <command>\x02.')
|
||||
utils.add_cmd(listcommands, 'list')
|
||||
@ -62,20 +68,60 @@ def help(irc, source, args):
|
||||
except IndexError: # No argument given, just return 'list' output
|
||||
listcommands(irc, source, args)
|
||||
return
|
||||
try:
|
||||
func = utils.bot_commands[command]
|
||||
except KeyError:
|
||||
utils.msg(irc, source, 'Error: no such command %r.' % command)
|
||||
if command not in world.bot_commands:
|
||||
utils.msg(irc, source, 'Error: Unknown command %r.' % command)
|
||||
return
|
||||
else:
|
||||
funcs = world.bot_commands[command]
|
||||
if len(funcs) > 1:
|
||||
utils.msg(irc, source, 'The following \x02%s\x02 plugins bind to the \x02%s\x02 command: %s'
|
||||
% (len(funcs), command, ', '.join([func.__module__ for func in funcs])))
|
||||
for func in funcs:
|
||||
doc = func.__doc__
|
||||
mod = func.__module__
|
||||
if doc:
|
||||
lines = doc.split('\n')
|
||||
# Bold the first line, which usually just tells you what
|
||||
# arguments the command takes.
|
||||
lines[0] = '\x02%s %s\x02' % (command, lines[0])
|
||||
lines[0] = '\x02%s %s\x02 (plugin: %r)' % (command, lines[0], mod)
|
||||
for line in lines:
|
||||
utils.msg(irc, source, line.strip())
|
||||
else:
|
||||
utils.msg(irc, source, 'Error: Command %r doesn\'t offer any help.' % command)
|
||||
utils.msg(irc, source, "Error: Command %r (from plugin %r) "
|
||||
"doesn't offer any help." % (command, mod))
|
||||
return
|
||||
|
||||
@utils.add_cmd
|
||||
def showuser(irc, source, args):
|
||||
"""<user>
|
||||
|
||||
Shows information about <user>."""
|
||||
try:
|
||||
target = args[0]
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: Not enough arguments. Needs 1: nick.")
|
||||
return
|
||||
u = utils.nickToUid(irc, target) or target
|
||||
# Only show private info if the person is calling 'showuser' on themselves,
|
||||
# or is an oper.
|
||||
verbose = utils.isOper(irc, source) or u == source
|
||||
if u not in irc.users:
|
||||
utils.msg(irc, source, 'Error: Unknown user %r.' % target)
|
||||
return
|
||||
|
||||
f = lambda s: utils.msg(irc, source, s)
|
||||
userobj = irc.users[u]
|
||||
f('Information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident,
|
||||
userobj.host, userobj.realname))
|
||||
sid = utils.clientToServer(irc, u)
|
||||
serverobj = irc.servers[sid]
|
||||
ts = userobj.ts
|
||||
f('\x02Home server\x02: %s (%s); \x02Signon time:\x02 %s (%s)' % \
|
||||
(serverobj.name, sid, ctime(float(ts)), ts))
|
||||
if verbose:
|
||||
f('\x02Protocol UID\x02: %s; \x02PyLink identification\x02: %s' % \
|
||||
(u, userobj.identified))
|
||||
f('\x02User modes\x02: %s' % utils.joinModes(userobj.modes))
|
||||
f('\x02Real host\x02: %s; \x02IP\x02: %s; \x02Away status\x02: %s' % \
|
||||
(userobj.realhost, userobj.ip, userobj.away or '\x1D(not set)\x1D'))
|
||||
f('\x02Channels\x02: %s' % (' '.join(userobj.channels).strip() or '\x1D(none)\x1D'))
|
||||
|
21
plugins/exec.py
Executable file
21
plugins/exec.py
Executable 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')
|
229
plugins/relay.py
229
plugins/relay.py
@ -13,6 +13,7 @@ from expiringdict import ExpiringDict
|
||||
import utils
|
||||
from log import log
|
||||
from conf import confname
|
||||
import world
|
||||
|
||||
dbname = "pylinkrelay"
|
||||
if confname != 'pylink':
|
||||
@ -28,11 +29,11 @@ def relayWhoisHandlers(irc, target):
|
||||
orig = getLocalUser(irc, target)
|
||||
if orig:
|
||||
network, remoteuid = orig
|
||||
remotenick = utils.networkobjects[network].users[remoteuid].nick
|
||||
remotenick = world.networkobjects[network].users[remoteuid].nick
|
||||
return [320, "%s :is a remote user connected via PyLink Relay. Home "
|
||||
"network: %s; Home nick: %s" % (user.nick, network,
|
||||
remotenick)]
|
||||
utils.whois_handlers.append(relayWhoisHandlers)
|
||||
world.whois_handlers.append(relayWhoisHandlers)
|
||||
|
||||
def normalizeNick(irc, netname, nick, separator=None, uid=''):
|
||||
separator = separator or irc.serverdata.get('separator') or "/"
|
||||
@ -94,7 +95,7 @@ def loadDB():
|
||||
db = {}
|
||||
|
||||
def exportDB(reschedule=False):
|
||||
scheduler = utils.schedulers.get('relaydb')
|
||||
scheduler = world.schedulers.get('relaydb')
|
||||
if reschedule and scheduler:
|
||||
scheduler.enter(30, 1, exportDB, argument=(True,))
|
||||
log.debug("Relay: exporting links database to %s", dbname)
|
||||
@ -110,7 +111,7 @@ def save(irc, source, args):
|
||||
exportDB()
|
||||
utils.msg(irc, source, 'Done.')
|
||||
else:
|
||||
utils.msg(irc, source, 'Error: you are not authenticated!')
|
||||
utils.msg(irc, source, 'Error: You are not authenticated!')
|
||||
return
|
||||
|
||||
def getPrefixModes(irc, remoteirc, channel, user):
|
||||
@ -149,16 +150,48 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
|
||||
host = userobj.host[:64]
|
||||
realname = userobj.realname
|
||||
modes = getSupportedUmodes(irc, remoteirc, userobj.modes)
|
||||
opertype = ''
|
||||
if ('o', None) in userobj.modes:
|
||||
if hasattr(userobj, 'opertype'):
|
||||
# InspIRCd's special OPERTYPE command; this is mandatory
|
||||
# and setting of umode +/-o will fail unless this
|
||||
# is used instead. This also sets an oper type for
|
||||
# the user, which is used in WHOIS, etc.
|
||||
|
||||
# If an opertype exists for the user, add " (remote)"
|
||||
# for the relayed clone, so that it shows in whois.
|
||||
# Janus does this too. :)
|
||||
# OPERTYPE uses underscores instead of spaces, FYI.
|
||||
log.debug('(%s) relay.getRemoteUser: setting OPERTYPE of client for %r to %s',
|
||||
irc.name, user, userobj.opertype)
|
||||
opertype = userobj.opertype + '_(remote)'
|
||||
else:
|
||||
opertype = 'IRC_Operator_(remote)'
|
||||
# Set hideoper on remote opers, to prevent inflating
|
||||
# /lusers and various /stats
|
||||
hideoper_mode = remoteirc.umodes.get('hideoper')
|
||||
if hideoper_mode:
|
||||
modes.append((hideoper_mode, None))
|
||||
u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident,
|
||||
host=host, realname=realname,
|
||||
modes=modes, ts=userobj.ts).uid
|
||||
modes=modes, ts=userobj.ts,
|
||||
opertype=opertype).uid
|
||||
remoteirc.users[u].remote = (irc.name, user)
|
||||
remoteirc.users[u].opertype = opertype
|
||||
away = userobj.away
|
||||
if away:
|
||||
remoteirc.proto.awayClient(remoteirc, u, away)
|
||||
relayusers[(irc.name, user)][remoteirc.name] = u
|
||||
return u
|
||||
|
||||
def handle_operup(irc, numeric, command, args):
|
||||
newtype = args['text'] + '_(remote)'
|
||||
for netname, user in relayusers[(irc.name, numeric)].items():
|
||||
log.debug('(%s) relay.handle_opertype: setting OPERTYPE of %s/%s to %s', irc.name, user, netname, newtype)
|
||||
remoteirc = world.networkobjects[netname]
|
||||
remoteirc.users[user].opertype = newtype
|
||||
utils.add_hook(handle_operup, 'PYLINK_CLIENT_OPERED')
|
||||
|
||||
def getLocalUser(irc, user, targetirc=None):
|
||||
"""<irc object> <pseudoclient uid> [<target irc object>]
|
||||
|
||||
@ -179,7 +212,7 @@ def getLocalUser(irc, user, targetirc=None):
|
||||
# If targetirc is given, we'll return simply the UID of the user on the
|
||||
# target network, if it exists. Otherwise, we'll return a tuple
|
||||
# with the home network name and the original user's UID.
|
||||
sourceobj = utils.networkobjects.get(remoteuser[0])
|
||||
sourceobj = world.networkobjects.get(remoteuser[0])
|
||||
if targetirc and sourceobj:
|
||||
if remoteuser[0] == targetirc.name:
|
||||
# The user we found's home network happens to be the one being
|
||||
@ -230,7 +263,7 @@ def initializeChannel(irc, channel):
|
||||
remotenet, remotechan = link
|
||||
if remotenet == irc.name:
|
||||
continue
|
||||
remoteirc = utils.networkobjects.get(remotenet)
|
||||
remoteirc = world.networkobjects.get(remotenet)
|
||||
if remoteirc is None:
|
||||
continue
|
||||
rc = remoteirc.channels[remotechan]
|
||||
@ -255,12 +288,12 @@ def handle_join(irc, numeric, command, args):
|
||||
return
|
||||
ts = args['ts']
|
||||
users = set(args['users'])
|
||||
relayJoins(irc, channel, users, ts)
|
||||
relayJoins(irc, channel, users, ts, burst=False)
|
||||
utils.add_hook(handle_join, 'JOIN')
|
||||
|
||||
def handle_quit(irc, numeric, command, args):
|
||||
for netname, user in relayusers[(irc.name, numeric)].copy().items():
|
||||
remoteirc = utils.networkobjects[netname]
|
||||
remoteirc = world.networkobjects[netname]
|
||||
remoteirc.proto.quitClient(remoteirc, user, args['text'])
|
||||
del relayusers[(irc.name, numeric)]
|
||||
utils.add_hook(handle_quit, 'QUIT')
|
||||
@ -274,7 +307,7 @@ utils.add_hook(handle_squit, 'SQUIT')
|
||||
|
||||
def handle_nick(irc, numeric, command, args):
|
||||
for netname, user in relayusers[(irc.name, numeric)].items():
|
||||
remoteirc = utils.networkobjects[netname]
|
||||
remoteirc = world.networkobjects[netname]
|
||||
newnick = normalizeNick(remoteirc, irc.name, args['newnick'], uid=user)
|
||||
if remoteirc.users[user].nick != newnick:
|
||||
remoteirc.proto.nickClient(remoteirc, user, newnick)
|
||||
@ -288,7 +321,7 @@ def handle_part(irc, numeric, command, args):
|
||||
return
|
||||
for channel in channels:
|
||||
for netname, user in relayusers[(irc.name, numeric)].copy().items():
|
||||
remoteirc = utils.networkobjects[netname]
|
||||
remoteirc = world.networkobjects[netname]
|
||||
remotechan = findRemoteChan(irc, remoteirc, channel)
|
||||
if remotechan is None:
|
||||
continue
|
||||
@ -324,7 +357,7 @@ def handle_privmsg(irc, numeric, command, args):
|
||||
return
|
||||
if utils.isChannel(target):
|
||||
for netname, user in relayusers[(irc.name, numeric)].items():
|
||||
remoteirc = utils.networkobjects[netname]
|
||||
remoteirc = world.networkobjects[netname]
|
||||
real_target = findRemoteChan(irc, remoteirc, target)
|
||||
if not real_target:
|
||||
continue
|
||||
@ -343,11 +376,11 @@ def handle_privmsg(irc, numeric, command, args):
|
||||
# on the remote network, and we won't have anything to send our
|
||||
# messages from.
|
||||
if homenet not in remoteusers.keys():
|
||||
utils.msg(irc, numeric, 'Error: you must be in a common channel '
|
||||
utils.msg(irc, numeric, 'Error: You must be in a common channel '
|
||||
'with %r in order to send messages.' % \
|
||||
irc.users[target].nick, notice=True)
|
||||
return
|
||||
remoteirc = utils.networkobjects[homenet]
|
||||
remoteirc = world.networkobjects[homenet]
|
||||
user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
|
||||
if notice:
|
||||
remoteirc.proto.noticeClient(remoteirc, user, real_target, text)
|
||||
@ -367,7 +400,7 @@ def handle_kick(irc, source, command, args):
|
||||
if relay is None or target == irc.pseudoclient.uid:
|
||||
return
|
||||
origuser = getLocalUser(irc, target)
|
||||
for name, remoteirc in utils.networkobjects.items():
|
||||
for name, remoteirc in world.networkobjects.items():
|
||||
if irc.name == name or not remoteirc.connected.is_set():
|
||||
continue
|
||||
remotechan = findRemoteChan(irc, remoteirc, channel)
|
||||
@ -400,14 +433,14 @@ def handle_kick(irc, source, command, args):
|
||||
# Join the kicked client back with its respective modes.
|
||||
irc.proto.sjoinServer(irc, irc.sid, channel, [(modes, target)])
|
||||
if kicker in irc.users:
|
||||
log.info('(%s) Blocked KICK (reason %r) from %s to relay client %s/%s on %s.',
|
||||
log.info('(%s) Relay claim: Blocked KICK (reason %r) from %s to relay client %s/%s on %s.',
|
||||
irc.name, args['text'], irc.users[source].nick,
|
||||
remoteirc.users[real_target].nick, remoteirc.name, channel)
|
||||
utils.msg(irc, kicker, "This channel is claimed; your kick to "
|
||||
"%s has been blocked because you are not "
|
||||
"(half)opped." % channel, notice=True)
|
||||
else:
|
||||
log.info('(%s) Blocked KICK (reason %r) from server %s to relay client %s/%s on %s.',
|
||||
log.info('(%s) Relay claim: Blocked KICK (reason %r) from server %s to relay client %s/%s on %s.',
|
||||
irc.name, args['text'], irc.servers[source].name,
|
||||
remoteirc.users[real_target].nick, remoteirc.name, channel)
|
||||
return
|
||||
@ -458,7 +491,7 @@ def handle_chgclient(irc, source, command, args):
|
||||
text = args['newgecos']
|
||||
if field:
|
||||
for netname, user in relayusers[(irc.name, target)].items():
|
||||
remoteirc = utils.networkobjects[netname]
|
||||
remoteirc = world.networkobjects[netname]
|
||||
try:
|
||||
remoteirc.proto.updateClient(remoteirc, user, field, text)
|
||||
except NotImplementedError: # IRCd doesn't support changing the field we want
|
||||
@ -579,22 +612,30 @@ def getSupportedUmodes(irc, remoteirc, modes):
|
||||
else:
|
||||
log.debug("(%s) getSupportedUmodes: skipping mode (%r, %r) because "
|
||||
"the remote network (%s)'s IRCd (%s) doesn't support it.",
|
||||
irc.name, modechar, arg, remoteirc.name, irc.proto.__name__)
|
||||
irc.name, modechar, arg, remoteirc.name,
|
||||
remoteirc.proto.__name__)
|
||||
return supported_modes
|
||||
|
||||
def handle_mode(irc, numeric, command, args):
|
||||
target = args['target']
|
||||
modes = args['modes']
|
||||
for name, remoteirc in utils.networkobjects.items():
|
||||
for name, remoteirc in world.networkobjects.items():
|
||||
if irc.name == name or not remoteirc.connected.is_set():
|
||||
continue
|
||||
if utils.isChannel(target):
|
||||
relayModes(irc, remoteirc, numeric, target, modes)
|
||||
else:
|
||||
# Set hideoper on remote opers, to prevent inflating
|
||||
# /lusers and various /stats
|
||||
hideoper_mode = remoteirc.umodes.get('hideoper')
|
||||
modes = getSupportedUmodes(irc, remoteirc, modes)
|
||||
if hideoper_mode:
|
||||
if ('+o', None) in modes:
|
||||
modes.append(('+%s' % hideoper_mode, None))
|
||||
elif ('-o', None) in modes:
|
||||
modes.append(('-%s' % hideoper_mode, None))
|
||||
remoteuser = getRemoteUser(irc, remoteirc, target, spawnIfMissing=False)
|
||||
if remoteuser is None:
|
||||
continue
|
||||
if remoteuser and modes:
|
||||
remoteirc.proto.modeClient(remoteirc, remoteuser, remoteuser, modes)
|
||||
|
||||
utils.add_hook(handle_mode, 'MODE')
|
||||
@ -602,7 +643,7 @@ utils.add_hook(handle_mode, 'MODE')
|
||||
def handle_topic(irc, numeric, command, args):
|
||||
channel = args['channel']
|
||||
topic = args['topic']
|
||||
for name, remoteirc in utils.networkobjects.items():
|
||||
for name, remoteirc in world.networkobjects.items():
|
||||
if irc.name == name or not remoteirc.connected.is_set():
|
||||
continue
|
||||
|
||||
@ -628,7 +669,7 @@ def handle_kill(irc, numeric, command, args):
|
||||
# We don't allow killing over the relay, so we must respawn the affected
|
||||
# client and rejoin it to its channels.
|
||||
del relayusers[realuser][irc.name]
|
||||
remoteirc = utils.networkobjects[realuser[0]]
|
||||
remoteirc = world.networkobjects[realuser[0]]
|
||||
for remotechan in remoteirc.channels.copy():
|
||||
localchan = findRemoteChan(remoteirc, irc, remotechan)
|
||||
if localchan:
|
||||
@ -637,7 +678,7 @@ def handle_kill(irc, numeric, command, args):
|
||||
client = getRemoteUser(remoteirc, irc, realuser[1])
|
||||
irc.proto.sjoinServer(irc, irc.sid, localchan, [(modes, client)])
|
||||
if userdata and numeric in irc.users:
|
||||
log.info('(%s) Blocked KILL (reason %r) from %s to relay client %s/%s.',
|
||||
log.info('(%s) Relay claim: Blocked KILL (reason %r) from %s to relay client %s/%s.',
|
||||
irc.name, args['text'], irc.users[numeric].nick,
|
||||
remoteirc.users[realuser[1]].nick, realuser[0])
|
||||
utils.msg(irc, numeric, "Your kill to %s has been blocked "
|
||||
@ -645,7 +686,7 @@ def handle_kill(irc, numeric, command, args):
|
||||
" users over the relay at this time." % \
|
||||
userdata.nick, notice=True)
|
||||
else:
|
||||
log.info('(%s) Blocked KILL (reason %r) from server %s to relay client %s/%s.',
|
||||
log.info('(%s) Relay claim: Blocked KILL (reason %r) from server %s to relay client %s/%s.',
|
||||
irc.name, args['text'], irc.servers[numeric].name,
|
||||
remoteirc.users[realuser[1]].nick, realuser[0])
|
||||
# Target user was local.
|
||||
@ -670,7 +711,7 @@ def isRelayClient(irc, user):
|
||||
return False
|
||||
|
||||
def relayJoins(irc, channel, users, ts, burst=True):
|
||||
for name, remoteirc in utils.networkobjects.items():
|
||||
for name, remoteirc in world.networkobjects.items():
|
||||
queued_users = []
|
||||
if name == irc.name or not remoteirc.connected.is_set():
|
||||
# Don't relay things to their source network...
|
||||
@ -710,7 +751,7 @@ def relayJoins(irc, channel, users, ts, burst=True):
|
||||
remoteirc.proto.joinClient(remoteirc, queued_users[0][1], remotechan)
|
||||
|
||||
def relayPart(irc, channel, user):
|
||||
for name, remoteirc in utils.networkobjects.items():
|
||||
for name, remoteirc in world.networkobjects.items():
|
||||
if name == irc.name or not remoteirc.connected.is_set():
|
||||
# Don't relay things to their source network...
|
||||
continue
|
||||
@ -730,7 +771,7 @@ def removeChannel(irc, channel):
|
||||
if irc is None:
|
||||
return
|
||||
if channel not in map(str.lower, irc.serverdata['channels']):
|
||||
irc.proto.partClient(irc, irc.pseudoclient.uid, channel)
|
||||
irc.proto.partClient(irc, irc.pseudoclient.uid, channel, 'Channel delinked.')
|
||||
relay = findRelay((irc.name, channel))
|
||||
if relay:
|
||||
for user in irc.channels[channel].users.copy():
|
||||
@ -756,16 +797,16 @@ def create(irc, source, args):
|
||||
try:
|
||||
channel = utils.toLower(irc, args[0])
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 1: channel.")
|
||||
utils.msg(irc, source, "Error: Not enough arguments. Needs 1: channel.")
|
||||
return
|
||||
if not utils.isChannel(channel):
|
||||
utils.msg(irc, source, 'Error: invalid channel %r.' % channel)
|
||||
utils.msg(irc, source, 'Error: Invalid channel %r.' % channel)
|
||||
return
|
||||
if source not in irc.channels[channel].users:
|
||||
utils.msg(irc, source, 'Error: you must be in %r to complete this operation.' % channel)
|
||||
utils.msg(irc, source, 'Error: You must be in %r to complete this operation.' % channel)
|
||||
return
|
||||
if not utils.isOper(irc, source):
|
||||
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
|
||||
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.')
|
||||
return
|
||||
db[(irc.name, channel)] = {'claim': [irc.name], 'links': set(), 'blocked_nets': set()}
|
||||
initializeChannel(irc, channel)
|
||||
@ -779,24 +820,24 @@ def destroy(irc, source, args):
|
||||
try:
|
||||
channel = utils.toLower(irc, args[0])
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 1: channel.")
|
||||
utils.msg(irc, source, "Error: Not enough arguments. Needs 1: channel.")
|
||||
return
|
||||
if not utils.isChannel(channel):
|
||||
utils.msg(irc, source, 'Error: invalid channel %r.' % channel)
|
||||
utils.msg(irc, source, 'Error: Invalid channel %r.' % channel)
|
||||
return
|
||||
if not utils.isOper(irc, source):
|
||||
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
|
||||
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.')
|
||||
return
|
||||
|
||||
entry = (irc.name, channel)
|
||||
if entry in db:
|
||||
for link in db[entry]['links']:
|
||||
removeChannel(utils.networkobjects.get(link[0]), link[1])
|
||||
removeChannel(world.networkobjects.get(link[0]), link[1])
|
||||
removeChannel(irc, channel)
|
||||
del db[entry]
|
||||
utils.msg(irc, source, 'Done.')
|
||||
else:
|
||||
utils.msg(irc, source, 'Error: no such relay %r exists.' % channel)
|
||||
utils.msg(irc, source, 'Error: No such relay %r exists.' % channel)
|
||||
return
|
||||
|
||||
@utils.add_cmd
|
||||
@ -809,7 +850,7 @@ def link(irc, source, args):
|
||||
channel = utils.toLower(irc, args[1])
|
||||
remotenet = args[0].lower()
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 2-3: remote netname, channel, local channel name (optional).")
|
||||
utils.msg(irc, source, "Error: Not enough arguments. Needs 2-3: remote netname, channel, local channel name (optional).")
|
||||
return
|
||||
try:
|
||||
localchan = utils.toLower(irc, args[2])
|
||||
@ -817,33 +858,33 @@ def link(irc, source, args):
|
||||
localchan = channel
|
||||
for c in (channel, localchan):
|
||||
if not utils.isChannel(c):
|
||||
utils.msg(irc, source, 'Error: invalid channel %r.' % c)
|
||||
utils.msg(irc, source, 'Error: Invalid channel %r.' % c)
|
||||
return
|
||||
if source not in irc.channels[localchan].users:
|
||||
utils.msg(irc, source, 'Error: you must be in %r to complete this operation.' % localchan)
|
||||
utils.msg(irc, source, 'Error: You must be in %r to complete this operation.' % localchan)
|
||||
return
|
||||
if not utils.isOper(irc, source):
|
||||
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
|
||||
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.')
|
||||
return
|
||||
if remotenet not in utils.networkobjects:
|
||||
utils.msg(irc, source, 'Error: no network named %r exists.' % remotenet)
|
||||
if remotenet not in world.networkobjects:
|
||||
utils.msg(irc, source, 'Error: No network named %r exists.' % remotenet)
|
||||
return
|
||||
localentry = findRelay((irc.name, localchan))
|
||||
if localentry:
|
||||
utils.msg(irc, source, 'Error: channel %r is already part of a relay.' % localchan)
|
||||
utils.msg(irc, source, 'Error: Channel %r is already part of a relay.' % localchan)
|
||||
return
|
||||
try:
|
||||
entry = db[(remotenet, channel)]
|
||||
except KeyError:
|
||||
utils.msg(irc, source, 'Error: no such relay %r exists.' % channel)
|
||||
utils.msg(irc, source, 'Error: No such relay %r exists.' % channel)
|
||||
return
|
||||
else:
|
||||
if irc.name in entry['blocked_nets']:
|
||||
utils.msg(irc, source, 'Error: access denied (network is banned from linking to this channel).')
|
||||
utils.msg(irc, source, 'Error: Access denied (network is banned from linking to this channel).')
|
||||
return
|
||||
for link in entry['links']:
|
||||
if link[0] == irc.name:
|
||||
utils.msg(irc, source, "Error: remote channel '%s%s' is already"
|
||||
utils.msg(irc, source, "Error: Remote channel '%s%s' is already"
|
||||
" linked here as %r." % (remotenet,
|
||||
channel, link[1]))
|
||||
return
|
||||
@ -860,17 +901,17 @@ def delink(irc, source, args):
|
||||
try:
|
||||
channel = utils.toLower(irc, args[0])
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 1-2: channel, remote netname (optional).")
|
||||
utils.msg(irc, source, "Error: Not enough arguments. Needs 1-2: channel, remote netname (optional).")
|
||||
return
|
||||
try:
|
||||
remotenet = args[1].lower()
|
||||
except IndexError:
|
||||
remotenet = None
|
||||
if not utils.isOper(irc, source):
|
||||
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
|
||||
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.')
|
||||
return
|
||||
if not utils.isChannel(channel):
|
||||
utils.msg(irc, source, 'Error: invalid channel %r.' % channel)
|
||||
utils.msg(irc, source, 'Error: Invalid channel %r.' % channel)
|
||||
return
|
||||
entry = findRelay((irc.name, channel))
|
||||
if entry:
|
||||
@ -884,18 +925,18 @@ def delink(irc, source, args):
|
||||
else:
|
||||
for link in db[entry]['links'].copy():
|
||||
if link[0] == remotenet:
|
||||
removeChannel(utils.networkobjects.get(remotenet), link[1])
|
||||
removeChannel(world.networkobjects.get(remotenet), link[1])
|
||||
db[entry]['links'].remove(link)
|
||||
else:
|
||||
removeChannel(irc, channel)
|
||||
db[entry]['links'].remove((irc.name, channel))
|
||||
utils.msg(irc, source, 'Done.')
|
||||
else:
|
||||
utils.msg(irc, source, 'Error: no such relay %r.' % channel)
|
||||
utils.msg(irc, source, 'Error: No such relay %r.' % channel)
|
||||
|
||||
def initializeAll(irc):
|
||||
log.debug('(%s) initializeAll: waiting for utils.started', irc.name)
|
||||
utils.started.wait()
|
||||
log.debug('(%s) initializeAll: waiting for world.started', irc.name)
|
||||
world.started.wait()
|
||||
for chanpair, entrydata in db.items():
|
||||
network, channel = chanpair
|
||||
initializeChannel(irc, channel)
|
||||
@ -905,7 +946,7 @@ def initializeAll(irc):
|
||||
|
||||
def main():
|
||||
loadDB()
|
||||
utils.schedulers['relaydb'] = scheduler = sched.scheduler()
|
||||
world.schedulers['relaydb'] = scheduler = sched.scheduler()
|
||||
scheduler.enter(30, 1, exportDB, argument=(True,))
|
||||
# Thread this because exportDB() queues itself as part of its
|
||||
# execution, in order to get a repeating loop.
|
||||
@ -937,7 +978,7 @@ def handle_save(irc, numeric, command, args):
|
||||
# It's one of our relay clients; try to fix our nick to the next
|
||||
# available normalized nick.
|
||||
remotenet, remoteuser = realuser
|
||||
remoteirc = utils.networkobjects[remotenet]
|
||||
remoteirc = world.networkobjects[remotenet]
|
||||
nick = remoteirc.users[remoteuser].nick
|
||||
# Limit how many times we can attempt to fix our nick, to prevent
|
||||
# floods and such.
|
||||
@ -963,7 +1004,7 @@ def linked(irc, source, args):
|
||||
"""takes no arguments.
|
||||
|
||||
Returns a list of channels shared across the relay."""
|
||||
networks = list(utils.networkobjects.keys())
|
||||
networks = list(world.networkobjects.keys())
|
||||
networks.remove(irc.name)
|
||||
s = 'Connected networks: \x02%s\x02 %s' % (irc.name, ' '.join(networks))
|
||||
utils.msg(irc, source, s)
|
||||
@ -978,7 +1019,7 @@ def linked(irc, source, args):
|
||||
|
||||
def handle_away(irc, numeric, command, args):
|
||||
for netname, user in relayusers[(irc.name, numeric)].items():
|
||||
remoteirc = utils.networkobjects[netname]
|
||||
remoteirc = world.networkobjects[netname]
|
||||
remoteirc.proto.awayClient(remoteirc, user, args['text'])
|
||||
utils.add_hook(handle_away, 'AWAY')
|
||||
|
||||
@ -989,15 +1030,37 @@ def handle_spawnmain(irc, numeric, command, args):
|
||||
initializeAll(irc)
|
||||
utils.add_hook(handle_spawnmain, 'PYLINK_SPAWNMAIN')
|
||||
|
||||
def handle_invite(irc, source, command, args):
|
||||
target = args['target']
|
||||
channel = args['channel']
|
||||
if isRelayClient(irc, target):
|
||||
remotenet, remoteuser = getLocalUser(irc, target)
|
||||
remoteirc = world.networkobjects[remotenet]
|
||||
remotechan = findRemoteChan(irc, remoteirc, channel)
|
||||
remotesource = getRemoteUser(irc, remoteirc, source, spawnIfMissing=False)
|
||||
if remotesource is None:
|
||||
utils.msg(irc, source, 'Error: You must be in a common channel '
|
||||
'with %s to invite them to channels.' % \
|
||||
irc.users[target].nick,
|
||||
notice=True)
|
||||
elif remotechan is None:
|
||||
utils.msg(irc, source, 'Error: You cannot invite someone to a '
|
||||
'channel not on their network!',
|
||||
notice=True)
|
||||
else:
|
||||
remoteirc.proto.inviteClient(remoteirc, remotesource, remoteuser,
|
||||
remotechan)
|
||||
utils.add_hook(handle_invite, 'INVITE')
|
||||
|
||||
@utils.add_cmd
|
||||
def linkacl(irc, source, args):
|
||||
"""ALLOW|DENY|LIST <channel> <remotenet>
|
||||
|
||||
Allows blocking / unblocking certain networks from linking to a relay, based on a blacklist.
|
||||
LINKACL LIST returns a list of blocked networks for a channel, while the ALLOW and DENY subcommands allow manipulating this blacklist."""
|
||||
missingargs = "Error: not enough arguments. Needs 2-3: subcommand (ALLOW/DENY/LIST), channel, remote network (for ALLOW/DENY)."
|
||||
missingargs = "Error: Not enough arguments. Needs 2-3: subcommand (ALLOW/DENY/LIST), channel, remote network (for ALLOW/DENY)."
|
||||
if not utils.isOper(irc, source):
|
||||
utils.msg(irc, source, 'Error: you must be opered in order to complete this operation.')
|
||||
utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.')
|
||||
return
|
||||
try:
|
||||
cmd = args[0].lower()
|
||||
@ -1006,11 +1069,11 @@ def linkacl(irc, source, args):
|
||||
utils.msg(irc, source, missingargs)
|
||||
return
|
||||
if not utils.isChannel(channel):
|
||||
utils.msg(irc, source, 'Error: invalid channel %r.' % channel)
|
||||
utils.msg(irc, source, 'Error: Invalid channel %r.' % channel)
|
||||
return
|
||||
relay = findRelay((irc.name, channel))
|
||||
if not relay:
|
||||
utils.msg(irc, source, 'Error: no such relay %r exists.' % channel)
|
||||
utils.msg(irc, source, 'Error: No such relay %r exists.' % channel)
|
||||
return
|
||||
if cmd == 'list':
|
||||
s = 'Blocked networks for \x02%s\x02: \x02%s\x02' % (channel, ', '.join(db[relay]['blocked_nets']) or '(empty)')
|
||||
@ -1029,8 +1092,44 @@ def linkacl(irc, source, args):
|
||||
try:
|
||||
db[relay]['blocked_nets'].remove(remotenet)
|
||||
except KeyError:
|
||||
utils.msg(irc, source, 'Error: network %r is not on the blacklist for %r.' % (remotenet, channel))
|
||||
utils.msg(irc, source, 'Error: Network %r is not on the blacklist for %r.' % (remotenet, channel))
|
||||
else:
|
||||
utils.msg(irc, source, 'Done.')
|
||||
else:
|
||||
utils.msg(irc, source, 'Error: unknown subcommand %r: valid ones are ALLOW, DENY, and LIST.' % cmd)
|
||||
utils.msg(irc, source, 'Error: Unknown subcommand %r: valid ones are ALLOW, DENY, and LIST.' % cmd)
|
||||
|
||||
@utils.add_cmd
|
||||
def showuser(irc, source, args):
|
||||
"""<user>
|
||||
|
||||
Shows relay data about user <user>. This is intended to be used alongside the 'commands' plugin, which provides a 'showuser' command with more general information."""
|
||||
try:
|
||||
target = args[0]
|
||||
except IndexError:
|
||||
# No errors here; showuser from the commands plugin already does this
|
||||
# for us.
|
||||
return
|
||||
u = utils.nickToUid(irc, target)
|
||||
if u:
|
||||
try:
|
||||
userpair = getLocalUser(irc, u) or (irc.name, u)
|
||||
remoteusers = relayusers[userpair].items()
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
nicks = []
|
||||
if remoteusers:
|
||||
nicks.append('%s (home network): \x02%s\x02' % (userpair[0],
|
||||
world.networkobjects[userpair[0]].users[userpair[1]].nick))
|
||||
for r in remoteusers:
|
||||
remotenet, remoteuser = r
|
||||
remoteirc = world.networkobjects[remotenet]
|
||||
nicks.append('%s: \x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick))
|
||||
utils.msg(irc, source, "\x02Relay nicks\x02: %s" % ', '.join(nicks))
|
||||
relaychannels = []
|
||||
for ch in irc.users[u].channels:
|
||||
relay = findRelay((irc.name, ch))
|
||||
if relay:
|
||||
relaychannels.append(''.join(relay))
|
||||
if relaychannels and (utils.isOper(irc, source) or u == source):
|
||||
utils.msg(irc, source, "\x02Relay channels\x02: %s" % ' '.join(relaychannels))
|
||||
|
@ -39,7 +39,7 @@ def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set()
|
||||
u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
|
||||
realhost=realhost, ip=ip)
|
||||
utils.applyModes(irc, uid, modes)
|
||||
irc.servers[server].users.append(uid)
|
||||
irc.servers[server].users.add(uid)
|
||||
_send(irc, server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}"
|
||||
" {ts} {modes} + :{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
@ -138,7 +138,7 @@ def removeClient(irc, numeric):
|
||||
log.debug('Removing client %s from irc.users', numeric)
|
||||
del irc.users[numeric]
|
||||
log.debug('Removing client %s from irc.servers[%s]', numeric, sid)
|
||||
irc.servers[sid].users.remove(numeric)
|
||||
irc.servers[sid].users.discard(numeric)
|
||||
|
||||
def quitClient(irc, numeric, reason):
|
||||
"""<irc object> <client numeric>
|
||||
@ -195,6 +195,7 @@ def _operUp(irc, target, opertype=None):
|
||||
otype = 'IRC_Operator'
|
||||
log.debug('(%s) Sending OPERTYPE from %s to oper them up.',
|
||||
irc.name, target)
|
||||
userobj.opertype = otype
|
||||
_send(irc, target, 'OPERTYPE %s' % otype)
|
||||
|
||||
def _sendModes(irc, numeric, target, modes, ts=None):
|
||||
@ -449,7 +450,7 @@ def handle_uid(irc, numeric, command, args):
|
||||
parsedmodes = utils.parseModes(irc, uid, [args[8], args[9]])
|
||||
log.debug('Applying modes %s for %s', parsedmodes, uid)
|
||||
utils.applyModes(irc, uid, parsedmodes)
|
||||
irc.servers[numeric].users.append(uid)
|
||||
irc.servers[numeric].users.add(uid)
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
|
||||
|
||||
def handle_quit(irc, numeric, command, args):
|
||||
@ -706,7 +707,10 @@ def handle_opertype(irc, numeric, command, args):
|
||||
omode = [('+o', None)]
|
||||
irc.users[numeric].opertype = opertype = args[0]
|
||||
utils.applyModes(irc, numeric, omode)
|
||||
return {'target': numeric, 'modes': omode, 'text': opertype}
|
||||
# OPERTYPE is essentially umode +o and metadata in one command;
|
||||
# we'll call that too.
|
||||
irc.callHooks([numeric, 'PYLINK_CLIENT_OPERED', {'text': opertype}])
|
||||
return {'target': numeric, 'modes': omode}
|
||||
|
||||
def handle_fident(irc, numeric, command, args):
|
||||
# :70MAAAAAB FHOST test
|
||||
|
@ -42,7 +42,7 @@ def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set()
|
||||
u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
|
||||
realhost=realhost, ip=ip)
|
||||
utils.applyModes(irc, uid, modes)
|
||||
irc.servers[server].users.append(uid)
|
||||
irc.servers[server].users.add(uid)
|
||||
_send(irc, server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} "
|
||||
"{realhost} * :{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
@ -469,7 +469,10 @@ def handle_euid(irc, numeric, command, args):
|
||||
parsedmodes = utils.parseModes(irc, uid, [modes])
|
||||
log.debug('Applying modes %s for %s', parsedmodes, uid)
|
||||
utils.applyModes(irc, uid, parsedmodes)
|
||||
irc.servers[numeric].users.append(uid)
|
||||
irc.servers[numeric].users.add(uid)
|
||||
if ('o', None) in parsedmodes:
|
||||
otype = 'Server_Administrator' if ('a', None) in parsedmodes else 'IRC_Operator'
|
||||
irc.callHooks([uid, 'PYLINK_CLIENT_OPERED', {'text': otype}])
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
|
||||
|
||||
def handle_uid(irc, numeric, command, args):
|
||||
@ -502,6 +505,17 @@ def handle_tmode(irc, numeric, command, args):
|
||||
ts = int(args[0])
|
||||
return {'target': channel, 'modes': changedmodes, 'ts': ts}
|
||||
|
||||
def handle_mode(irc, numeric, command, args):
|
||||
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc
|
||||
target = args[0]
|
||||
modestrings = args[1:]
|
||||
changedmodes = utils.parseModes(irc, numeric, modestrings)
|
||||
utils.applyModes(irc, target, changedmodes)
|
||||
if ('+o', None) in changedmodes:
|
||||
otype = 'Server_Administrator' if ('a', None) in irc.users[target].modes else 'IRC_Operator'
|
||||
irc.callHooks([target, 'PYLINK_CLIENT_OPERED', {'text': otype}])
|
||||
return {'target': target, 'modes': changedmodes}
|
||||
|
||||
def handle_events(irc, data):
|
||||
# TS6 messages:
|
||||
# :42X COMMAND arg1 arg2 :final long arg
|
||||
|
21
runtests.py
Executable file
21
runtests.py
Executable 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
35
tests/test_coreplugin.py
Normal 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)
|
@ -1,15 +1,13 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from log import log
|
||||
|
||||
import classes
|
||||
import unittest
|
||||
|
||||
# Yes, we're going to even test the testing classes. Testception? I think so.
|
||||
class TestFakeIRC(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.irc = classes.FakeIRC('unittest', classes.FakeProto(), classes.testconf)
|
||||
self.irc = classes.FakeIRC('unittest', classes.FakeProto())
|
||||
|
||||
def testFakeIRC(self):
|
||||
self.irc.run('this should do nothing')
|
||||
|
@ -2,31 +2,30 @@ import sys
|
||||
import os
|
||||
sys.path += [os.getcwd(), os.path.join(os.getcwd(), 'protocols')]
|
||||
import unittest
|
||||
from collections import defaultdict
|
||||
|
||||
import inspircd
|
||||
import classes
|
||||
import utils
|
||||
import coreplugin
|
||||
import world
|
||||
|
||||
class TestProtoInspIRCd(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.irc = classes.FakeIRC('unittest', inspircd, classes.testconf)
|
||||
self.proto = self.irc.proto
|
||||
self.sdata = self.irc.serverdata
|
||||
# This is to initialize ourself as an internal PseudoServer, so we can spawn clients
|
||||
self.proto.connect(self.irc)
|
||||
self.u = self.irc.pseudoclient.uid
|
||||
self.maxDiff = None
|
||||
utils.command_hooks = defaultdict(list)
|
||||
import tests_common
|
||||
|
||||
world.testing = inspircd
|
||||
|
||||
class InspIRCdTestCase(tests_common.CommonProtoTestCase):
|
||||
def testCheckRecvpass(self):
|
||||
# Correct recvpass here.
|
||||
self.irc.run('SERVER somehow.someday abcd 0 0AL :Somehow Server - McMurdo Station, Antarctica')
|
||||
# Incorrect recvpass here; should raise ProtocolError.
|
||||
self.assertRaises(classes.ProtocolError, self.irc.run, 'SERVER somehow.someday BADPASS 0 0AL :Somehow Server - McMurdo Station, Antarctica')
|
||||
|
||||
def testConnect(self):
|
||||
self.proto.connect(self.irc)
|
||||
initial_messages = self.irc.takeMsgs()
|
||||
commands = self.irc.takeCommands(initial_messages)
|
||||
|
||||
# SERVER pylink.unittest abcd 0 9PY :PyLink Service
|
||||
serverline = 'SERVER %s %s 0 %s :PyLink Service' % (
|
||||
self.sdata['hostname'], self.sdata['sendpass'], self.sdata['sid'])
|
||||
serverline = 'SERVER %s %s 0 %s :%s' % (
|
||||
self.sdata['hostname'], self.sdata['sendpass'], self.sdata['sid'],
|
||||
self.irc.botdata['serverdesc'])
|
||||
self.assertIn(serverline, initial_messages)
|
||||
self.assertIn('BURST', commands)
|
||||
self.assertIn('ENDBURST', commands)
|
||||
@ -34,88 +33,12 @@ class TestProtoInspIRCd(unittest.TestCase):
|
||||
self.assertIn('UID', commands)
|
||||
self.assertIn('FJOIN', commands)
|
||||
|
||||
def testCheckRecvpass(self):
|
||||
# Correct recvpass here.
|
||||
self.irc.run('SERVER somehow.someday abcd 0 0AL :Somehow Server - McMurdo Station, Antarctica')
|
||||
# Incorrect recvpass here; should raise ProtocolError.
|
||||
self.assertRaises(classes.ProtocolError, self.irc.run, 'SERVER somehow.someday BADPASS 0 0AL :Somehow Server - McMurdo Station, Antarctica')
|
||||
|
||||
def testSpawnClient(self):
|
||||
u = self.proto.spawnClient(self.irc, 'testuser3', 'moo', 'hello.world').uid
|
||||
# Check the server index and the user index
|
||||
self.assertIn(u, self.irc.servers[self.irc.sid].users)
|
||||
self.assertIn(u, self.irc.users)
|
||||
# Raise ValueError when trying to spawn a client on a server that's not ours
|
||||
self.assertRaises(ValueError, self.proto.spawnClient, self.irc, 'abcd', 'user', 'dummy.user.net', server='44A')
|
||||
# Unfilled args should get placeholder fields and not error.
|
||||
self.proto.spawnClient(self.irc, 'testuser4')
|
||||
|
||||
def testJoinClient(self):
|
||||
u = self.u
|
||||
self.proto.joinClient(self.irc, u, '#Channel')
|
||||
self.assertIn(u, self.irc.channels['#channel'].users)
|
||||
# Non-existant user.
|
||||
self.assertRaises(LookupError, self.proto.joinClient, self.irc, '9PYZZZZZZ', '#test')
|
||||
|
||||
def testPartClient(self):
|
||||
u = self.u
|
||||
self.proto.joinClient(self.irc, u, '#channel')
|
||||
self.proto.partClient(self.irc, u, '#channel')
|
||||
self.assertNotIn(u, self.irc.channels['#channel'].users)
|
||||
|
||||
def testQuitClient(self):
|
||||
u = self.proto.spawnClient(self.irc, 'testuser3', 'moo', 'hello.world').uid
|
||||
self.proto.joinClient(self.irc, u, '#channel')
|
||||
self.assertRaises(LookupError, self.proto.quitClient, self.irc, '9PYZZZZZZ', 'quit reason')
|
||||
self.proto.quitClient(self.irc, u, 'quit reason')
|
||||
self.assertNotIn(u, self.irc.channels['#channel'].users)
|
||||
self.assertNotIn(u, self.irc.users)
|
||||
self.assertNotIn(u, self.irc.servers[self.irc.sid].users)
|
||||
|
||||
def testKickClient(self):
|
||||
target = self.proto.spawnClient(self.irc, 'soccerball', 'soccerball', 'abcd').uid
|
||||
self.proto.joinClient(self.irc, target, '#pylink')
|
||||
self.assertIn(self.u, self.irc.channels['#pylink'].users)
|
||||
self.assertIn(target, self.irc.channels['#pylink'].users)
|
||||
self.proto.kickClient(self.irc, self.u, '#pylink', target, 'Pow!')
|
||||
self.assertNotIn(target, self.irc.channels['#pylink'].users)
|
||||
|
||||
def testNickClient(self):
|
||||
self.proto.nickClient(self.irc, self.u, 'NotPyLink')
|
||||
self.assertEqual('NotPyLink', self.irc.users[self.u].nick)
|
||||
|
||||
def testModeClient(self):
|
||||
testuser = self.proto.spawnClient(self.irc, 'testcakes')
|
||||
self.irc.takeMsgs()
|
||||
self.proto.modeClient(self.irc, self.u, testuser.uid, [('+i', None), ('+w', None)])
|
||||
self.assertEqual({('i', None), ('w', None)}, testuser.modes)
|
||||
|
||||
self.proto.modeClient(self.irc, self.u, '#pylink', [('+s', None), ('+l', '30')])
|
||||
self.assertEqual({('s', None), ('l', '30')}, self.irc.channels['#pylink'].modes)
|
||||
|
||||
cmds = self.irc.takeCommands(self.irc.takeMsgs())
|
||||
self.assertEqual(cmds, ['MODE', 'FMODE'])
|
||||
|
||||
def testSpawnServer(self):
|
||||
# Incorrect SID length
|
||||
self.assertRaises(Exception, self.proto.spawnServer, self.irc, 'subserver.pylink', '34Q0')
|
||||
self.proto.spawnServer(self.irc, 'subserver.pylink', '34Q')
|
||||
# Duplicate server name
|
||||
self.assertRaises(Exception, self.proto.spawnServer, self.irc, 'Subserver.PyLink', '34Z')
|
||||
# Duplicate SID
|
||||
self.assertRaises(Exception, self.proto.spawnServer, self.irc, 'another.Subserver.PyLink', '34Q')
|
||||
self.assertIn('34Q', self.irc.servers)
|
||||
super(InspIRCdTestCase, self).testSpawnServer()
|
||||
# Are we bursting properly?
|
||||
self.assertIn(':34Q ENDBURST', self.irc.takeMsgs())
|
||||
|
||||
def testSpawnClientOnServer(self):
|
||||
self.proto.spawnServer(self.irc, 'subserver.pylink', '34Q')
|
||||
u = self.proto.spawnClient(self.irc, 'person1', 'person', 'users.overdrive.pw', server='34Q')
|
||||
# We're spawning clients on the right server, hopefully...
|
||||
self.assertIn(u.uid, self.irc.servers['34Q'].users)
|
||||
self.assertNotIn(u.uid, self.irc.servers[self.irc.sid].users)
|
||||
|
||||
def testSquit(self):
|
||||
def testHandleSQuit(self):
|
||||
# Spawn a messy network map, just because!
|
||||
self.proto.spawnServer(self.irc, 'level1.pylink', '34P')
|
||||
self.proto.spawnServer(self.irc, 'level2.pylink', '34Q', uplink='34P')
|
||||
@ -136,18 +59,6 @@ class TestProtoInspIRCd(unittest.TestCase):
|
||||
self.assertNotIn('34Q', self.irc.servers)
|
||||
self.assertNotIn('34Z', self.irc.servers)
|
||||
|
||||
def testRSquit(self):
|
||||
u = self.proto.spawnClient(self.irc, 'person1', 'person', 'users.overdrive.pw')
|
||||
u.identified = 'admin'
|
||||
self.proto.spawnServer(self.irc, 'level1.pylink', '34P')
|
||||
self.irc.run(':%s RSQUIT level1.pylink :some reason' % self.u)
|
||||
# No SQUIT yet, since the 'PyLink' client isn't identified
|
||||
self.assertNotIn('SQUIT', self.irc.takeCommands(self.irc.takeMsgs()))
|
||||
# The one we just spawned however, is.
|
||||
self.irc.run(':%s RSQUIT level1.pylink :some reason' % u.uid)
|
||||
self.assertIn('SQUIT', self.irc.takeCommands(self.irc.takeMsgs()))
|
||||
self.assertNotIn('34P', self.irc.servers)
|
||||
|
||||
def testHandleServer(self):
|
||||
self.irc.run('SERVER whatever.net abcd 0 10X :something')
|
||||
self.assertIn('10X', self.irc.servers)
|
||||
@ -157,124 +68,122 @@ class TestProtoInspIRCd(unittest.TestCase):
|
||||
self.assertEqual('test.server', self.irc.servers['0AL'].name)
|
||||
|
||||
def testHandleUID(self):
|
||||
self.irc.run('SERVER whatever.net abcd 0 10X :something')
|
||||
self.irc.run(':10X UID 10XAAAAAB 1429934638 GL 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 +Wioswx +ACGKNOQXacfgklnoqvx :realname')
|
||||
self.assertIn('10XAAAAAB', self.irc.servers['10X'].users)
|
||||
self.assertIn('10XAAAAAB', self.irc.users)
|
||||
u = self.irc.users['10XAAAAAB']
|
||||
self.assertEqual('GL', u.nick)
|
||||
|
||||
expected = {'uid': '10XAAAAAB', 'ts': '1429934638', 'nick': 'GL',
|
||||
'realhost': '0::1', 'ident': 'gl', 'ip': '0::1',
|
||||
'host': 'hidden-7j810p.9mdf.lrek.0000.0000.IP'}
|
||||
hookdata = self.irc.takeHooks()[0][-1]
|
||||
self.assertEqual(hookdata, expected)
|
||||
|
||||
def testHandleKill(self):
|
||||
self.irc.takeMsgs() # Ignore the initial connect messages
|
||||
utils.add_hook(self.irc.dummyhook, 'KILL')
|
||||
olduid = self.irc.pseudoclient.uid
|
||||
self.irc.run(':{u} KILL {u} :killed'.format(u=olduid))
|
||||
self.u = self.irc.pseudoclient.uid
|
||||
self.irc.run(':{u} KILL {u} :killed'.format(u=self.u))
|
||||
msgs = self.irc.takeMsgs()
|
||||
commands = self.irc.takeCommands(msgs)
|
||||
hookdata = self.irc.takeHooks()[0]
|
||||
del hookdata['ts']
|
||||
self.assertEqual({'target': olduid, 'text': 'killed'}, hookdata)
|
||||
# Make sure we're respawning our PseudoClient when its killed
|
||||
self.assertIn('UID', commands)
|
||||
self.assertIn('FJOIN', commands)
|
||||
# Also make sure that we're updating the irc.pseudoclient field
|
||||
self.assertNotEqual(self.irc.pseudoclient.uid, olduid)
|
||||
hookdata = self.irc.takeHooks()[0][-1]
|
||||
self.assertEqual(hookdata['target'], self.u)
|
||||
self.assertEqual(hookdata['text'], 'killed')
|
||||
self.assertNotIn(self.u, self.irc.users)
|
||||
|
||||
def testHandleKick(self):
|
||||
self.irc.takeMsgs() # Ignore the initial connect messages
|
||||
utils.add_hook(self.irc.dummyhook, 'KICK')
|
||||
self.irc.run(':{u} KICK #pylink {u} :kicked'.format(u=self.irc.pseudoclient.uid))
|
||||
hookdata = self.irc.takeHooks()[0]
|
||||
del hookdata['ts']
|
||||
self.assertEqual({'target': self.u, 'text': 'kicked', 'channel': '#pylink'}, hookdata)
|
||||
hookdata = self.irc.takeHooks()[0][-1]
|
||||
self.assertEqual(hookdata['target'], self.u)
|
||||
self.assertEqual(hookdata['text'], 'kicked')
|
||||
self.assertEqual(hookdata['channel'], '#pylink')
|
||||
|
||||
# Ditto above
|
||||
msgs = self.irc.takeMsgs()
|
||||
commands = self.irc.takeCommands(msgs)
|
||||
self.assertIn('FJOIN', commands)
|
||||
|
||||
def testHandleFjoinUsers(self):
|
||||
def testHandleFJoinUsers(self):
|
||||
self.irc.run(':10X FJOIN #Chat 1423790411 + :,10XAAAAAA ,10XAAAAAB')
|
||||
self.assertEqual({'10XAAAAAA', '10XAAAAAB'}, self.irc.channels['#chat'].users)
|
||||
# self.assertIn('10XAAAAAB', self.irc.channels['#chat'].users)
|
||||
self.assertIn('#chat', self.irc.users['10XAAAAAA'].channels)
|
||||
# Sequential FJOINs must NOT remove existing users
|
||||
self.irc.run(':10X FJOIN #Chat 1423790412 + :,10XAAAAAC')
|
||||
# Join list can be empty too, in the case of permanent channels with 0 users.
|
||||
self.irc.run(':10X FJOIN #Chat 1423790413 +nt :')
|
||||
|
||||
def testHandleFjoinModes(self):
|
||||
def testHandleFJoinModes(self):
|
||||
self.irc.run(':10X FJOIN #Chat 1423790411 +nt :,10XAAAAAA ,10XAAAAAB')
|
||||
self.assertEqual({('n', None), ('t', None)}, self.irc.channels['#chat'].modes)
|
||||
# Sequential FJOINs must NOT remove existing modes
|
||||
self.irc.run(':10X FJOIN #Chat 1423790412 + :,10XAAAAAC')
|
||||
self.assertEqual({('n', None), ('t', None)}, self.irc.channels['#chat'].modes)
|
||||
|
||||
def testHandleFjoinModesWithArgs(self):
|
||||
def testHandleFJoinModesWithArgs(self):
|
||||
self.irc.run(':10X FJOIN #Chat 1423790414 +nlks 10 t0psekrit :,10XAAAAAA ,10XAAAAAB')
|
||||
self.assertEqual({('n', None), ('s', None), ('l', '10'), ('k', 't0psekrit')},
|
||||
self.irc.channels['#chat'].modes)
|
||||
|
||||
def testHandleFjoinPrefixes(self):
|
||||
def testHandleFJoinPrefixes(self):
|
||||
self.irc.run(':10X FJOIN #Chat 1423790418 +nt :ov,10XAAAAAA v,10XAAAAAB ,10XAAAAAC')
|
||||
self.assertEqual({('n', None), ('t', None)}, self.irc.channels['#chat'].modes)
|
||||
self.assertEqual({'10XAAAAAA', '10XAAAAAB', '10XAAAAAC'}, self.irc.channels['#chat'].users)
|
||||
self.assertIn('10XAAAAAA', self.irc.channels['#chat'].prefixmodes['ops'])
|
||||
self.assertEqual({'10XAAAAAA', '10XAAAAAB'}, self.irc.channels['#chat'].prefixmodes['voices'])
|
||||
|
||||
def testHandleFjoinHook(self):
|
||||
utils.add_hook(self.irc.dummyhook, 'JOIN')
|
||||
def testHandleFJoinHook(self):
|
||||
self.irc.run(':10X FJOIN #PyLink 1423790418 +ls 10 :ov,10XAAAAAA v,10XAAAAAB ,10XAAAAAC')
|
||||
hookdata = self.irc.takeHooks()[0]
|
||||
hookdata = self.irc.takeHooks()[0][-1]
|
||||
expected = {'modes': [('+l', '10'), ('+s', None)],
|
||||
'channel': '#pylink',
|
||||
'users': ['10XAAAAAA', '10XAAAAAB', '10XAAAAAC'],
|
||||
'ts': 1423790418}
|
||||
self.assertEqual(expected, hookdata)
|
||||
|
||||
def testHandleFmode(self):
|
||||
self.irc.run(':10X FJOIN #pylink 1423790411 +n :o,10XAAAAAA ,10XAAAAAB')
|
||||
utils.add_hook(self.irc.dummyhook, 'MODE')
|
||||
def testHandleFMode(self):
|
||||
self.irc.run(':70M FMODE #pylink 1423790412 +ikl herebedragons 100')
|
||||
self.assertEqual({('i', None), ('k', 'herebedragons'), ('l', '100'), ('n', None)}, self.irc.channels['#pylink'].modes)
|
||||
self.assertEqual({('i', None), ('k', 'herebedragons'), ('l', '100')}, self.irc.channels['#pylink'].modes)
|
||||
self.irc.run(':70M FMODE #pylink 1423790413 -ilk+m herebedragons')
|
||||
self.assertEqual({('m', None), ('n', None)}, self.irc.channels['#pylink'].modes)
|
||||
self.assertEqual({('m', None)}, self.irc.channels['#pylink'].modes)
|
||||
|
||||
hookdata = self.irc.takeHooks()
|
||||
expected = [{'target': '#pylink', 'modes': [('+i', None), ('+k', 'herebedragons'), ('+l', '100')], 'ts': 1423790412},
|
||||
{'target': '#pylink', 'modes': [('-i', None), ('-l', None), ('-k', 'herebedragons'), ('+m', None)], 'ts': 1423790413}]
|
||||
expected = [['70M', 'FMODE', {'target': '#pylink', 'modes':
|
||||
[('+i', None), ('+k', 'herebedragons'),
|
||||
('+l', '100')], 'ts': 1423790412}
|
||||
],
|
||||
['70M', 'FMODE', {'target': '#pylink', 'modes':
|
||||
[('-i', None), ('-l', None),
|
||||
('-k', 'herebedragons'), ('+m', None)],
|
||||
'ts': 1423790413}]
|
||||
]
|
||||
self.assertEqual(expected, hookdata)
|
||||
|
||||
def testHandleFmodeWithPrefixes(self):
|
||||
self.irc.run(':70M FJOIN #pylink 1423790411 +n :o,10XAAAAAA ,10XAAAAAB')
|
||||
utils.add_hook(self.irc.dummyhook, 'MODE')
|
||||
def testHandleFModeWithPrefixes(self):
|
||||
self.irc.run(':70M FJOIN #pylink 123 +n :o,10XAAAAAA ,10XAAAAAB')
|
||||
# Prefix modes are stored separately, so they should never show up in .modes
|
||||
self.assertNotIn(('o', '10XAAAAAA'), self.irc.channels['#pylink'].modes)
|
||||
self.assertEqual({'10XAAAAAA'}, self.irc.channels['#pylink'].prefixmodes['ops'])
|
||||
self.irc.run(':70M FMODE #pylink 1423790412 +lot 50 %s' % self.u)
|
||||
self.irc.run(':70M FMODE #pylink 123 +lot 50 %s' % self.u)
|
||||
self.assertIn(self.u, self.irc.channels['#pylink'].prefixmodes['ops'])
|
||||
modes = {('l', '50'), ('n', None), ('t', None)}
|
||||
self.assertEqual(modes, self.irc.channels['#pylink'].modes)
|
||||
self.irc.run(':70M FMODE #pylink 1423790413 -o %s' % self.u)
|
||||
self.irc.run(':70M FMODE #pylink 123 -o %s' % self.u)
|
||||
self.assertEqual(modes, self.irc.channels['#pylink'].modes)
|
||||
self.assertNotIn(self.u, self.irc.channels['#pylink'].prefixmodes['ops'])
|
||||
# Test hooks
|
||||
hookdata = self.irc.takeHooks()
|
||||
expected = [{'target': '#pylink', 'modes': [('+l', '50'), ('+o', '9PYAAAAAA'), ('+t', None)], 'ts': 1423790412},
|
||||
{'target': '#pylink', 'modes': [('-o', '9PYAAAAAA')], 'ts': 1423790413}]
|
||||
expected = [['70M', 'FJOIN', {'channel': '#pylink', 'ts': 123, 'modes': [('+n', None)],
|
||||
'users': ['10XAAAAAA', '10XAAAAAB']}],
|
||||
['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '50'), ('+o', '9PYAAAAAA'), ('+t', None)], 'ts': 123}],
|
||||
['70M', 'FMODE', {'target': '#pylink', 'modes': [('-o', '9PYAAAAAA')], 'ts': 123}]]
|
||||
self.assertEqual(expected, hookdata)
|
||||
|
||||
def testFmodeRemovesOldParams(self):
|
||||
utils.add_hook(self.irc.dummyhook, 'MODE')
|
||||
def testHandleFModeRemovesOldParams(self):
|
||||
self.irc.run(':70M FMODE #pylink 1423790412 +l 50')
|
||||
self.assertEqual({('l', '50')}, self.irc.channels['#pylink'].modes)
|
||||
self.irc.run(':70M FMODE #pylink 1423790412 +l 30')
|
||||
self.assertEqual({('l', '30')}, self.irc.channels['#pylink'].modes)
|
||||
hookdata = self.irc.takeHooks()
|
||||
expected = [{'target': '#pylink', 'modes': [('+l', '50')], 'ts': 1423790412},
|
||||
{'target': '#pylink', 'modes': [('+l', '30')], 'ts': 1423790412}]
|
||||
expected = [['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '50')], 'ts': 1423790412}],
|
||||
['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '30')], 'ts': 1423790412}]]
|
||||
self.assertEqual(expected, hookdata)
|
||||
|
||||
def testFjoinResetsTS(self):
|
||||
def testHandleFJoinResetsTS(self):
|
||||
curr_ts = self.irc.channels['#pylink'].ts
|
||||
self.irc.run(':70M FJOIN #pylink 5 + :')
|
||||
self.assertEqual(self.irc.channels['#pylink'].ts, 5)
|
||||
@ -285,87 +194,60 @@ class TestProtoInspIRCd(unittest.TestCase):
|
||||
|
||||
def testHandleTopic(self):
|
||||
self.irc.connect()
|
||||
utils.add_hook(self.irc.dummyhook, 'TOPIC')
|
||||
self.irc.run(':9PYAAAAAA TOPIC #PyLink :test')
|
||||
self.assertEqual(self.irc.channels['#pylink'].topic, 'test')
|
||||
hookdata = self.irc.takeHooks()[0]
|
||||
# Setter is a nick here, not an UID - this is to be consistent
|
||||
# with FTOPIC above, which sends the nick/prefix of the topic setter.
|
||||
self.assertTrue(utils.isNick(hookdata.get('setter')))
|
||||
hookdata = self.irc.takeHooks()[0][-1]
|
||||
self.assertEqual(type(hookdata['ts']), int)
|
||||
self.assertEqual(hookdata['topic'], 'test')
|
||||
self.assertEqual(hookdata['channel'], '#pylink')
|
||||
|
||||
def testMsgHooks(self):
|
||||
def testHandleMessages(self):
|
||||
for m in ('NOTICE', 'PRIVMSG'):
|
||||
utils.add_hook(self.irc.dummyhook, m)
|
||||
self.irc.run(':70MAAAAAA %s #dev :afasfsa' % m)
|
||||
hookdata = self.irc.takeHooks()[0]
|
||||
del hookdata['ts']
|
||||
self.assertEqual({'target': '#dev', 'text': 'afasfsa'}, hookdata)
|
||||
hookdata = self.irc.takeHooks()[0][-1]
|
||||
self.assertEqual(hookdata['target'], '#dev')
|
||||
self.assertEqual(hookdata['text'], 'afasfsa')
|
||||
|
||||
def testHandlePart(self):
|
||||
utils.add_hook(self.irc.dummyhook, 'PART')
|
||||
hookdata = self.irc.takeHooks()
|
||||
self.irc.run(':9PYAAAAAA PART #pylink')
|
||||
hookdata = self.irc.takeHooks()[0]
|
||||
del hookdata['ts']
|
||||
self.assertEqual({'channel': '#pylink', 'text': ''}, hookdata)
|
||||
|
||||
def testUIDHook(self):
|
||||
utils.add_hook(self.irc.dummyhook, 'UID')
|
||||
# Create the server so we won't KeyError on processing UID
|
||||
self.irc.run('SERVER whatever. abcd 0 10X :Whatever Server - Hellas Planitia, Mars')
|
||||
self.irc.run(':10X UID 10XAAAAAB 1429934638 GL 0::1 '
|
||||
'hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 '
|
||||
'+Wioswx +ACGKNOQXacfgklnoqvx :realname')
|
||||
expected = {'uid': '10XAAAAAB', 'ts': '1429934638', 'nick': 'GL',
|
||||
'realhost': '0::1', 'ident': 'gl', 'ip': '0::1',
|
||||
'host': 'hidden-7j810p.9mdf.lrek.0000.0000.IP'}
|
||||
hookdata = self.irc.takeHooks()[0]
|
||||
self.assertEqual(hookdata, expected)
|
||||
hookdata = self.irc.takeHooks()[0][-1]
|
||||
self.assertEqual(hookdata['channels'], ['#pylink'])
|
||||
self.assertEqual(hookdata['text'], '')
|
||||
|
||||
def testHandleQuit(self):
|
||||
utils.add_hook(self.irc.dummyhook, 'QUIT')
|
||||
self.irc.run('SERVER whatever. abcd 0 10X :Whatever Server - Hellas Planitia, Mars')
|
||||
self.irc.run(':10X UID 10XAAAAAB 1429934638 GL 0::1 '
|
||||
'hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 '
|
||||
'+Wioswx +ACGKNOQXacfgklnoqvx :realname')
|
||||
self.irc.takeHooks()
|
||||
self.irc.run(':10XAAAAAB QUIT :Quit: quit message goes here')
|
||||
hookdata = self.irc.takeHooks()[0]
|
||||
del hookdata['ts']
|
||||
self.assertEqual(hookdata, {'text': 'Quit: quit message goes here'})
|
||||
hookdata = self.irc.takeHooks()[0][-1]
|
||||
self.assertEqual(hookdata['text'], 'Quit: quit message goes here')
|
||||
self.assertNotIn('10XAAAAAB', self.irc.users)
|
||||
self.assertNotIn('10XAAAAAB', self.irc.servers['10X'].users)
|
||||
|
||||
def testHandleServer(self):
|
||||
utils.add_hook(self.irc.dummyhook, 'SERVER')
|
||||
self.irc.run(':00A SERVER test.server * 1 00C :testing raw message syntax')
|
||||
hookdata = self.irc.takeHooks()[0]
|
||||
del hookdata['ts']
|
||||
self.assertEqual(hookdata, {'name': 'test.server', 'sid': '00C',
|
||||
'text': 'testing raw message syntax'})
|
||||
hookdata = self.irc.takeHooks()[-1][-1]
|
||||
self.assertEqual(hookdata['name'], 'test.server')
|
||||
self.assertEqual(hookdata['sid'], '00C')
|
||||
self.assertEqual(hookdata['text'], 'testing raw message syntax')
|
||||
self.assertIn('00C', self.irc.servers)
|
||||
|
||||
def testHandleNick(self):
|
||||
utils.add_hook(self.irc.dummyhook, 'NICK')
|
||||
self.irc.run(':9PYAAAAAA NICK PyLink-devel 1434744242')
|
||||
hookdata = self.irc.takeHooks()[0]
|
||||
hookdata = self.irc.takeHooks()[0][-1]
|
||||
expected = {'newnick': 'PyLink-devel', 'oldnick': 'PyLink', 'ts': 1434744242}
|
||||
self.assertEqual(hookdata, expected)
|
||||
self.assertEqual('PyLink-devel', self.irc.users['9PYAAAAAA'].nick)
|
||||
|
||||
def testHandleSave(self):
|
||||
utils.add_hook(self.irc.dummyhook, 'SAVE')
|
||||
self.irc.run(':9PYAAAAAA NICK Derp_ 1433728673')
|
||||
self.irc.run(':70M SAVE 9PYAAAAAA 1433728673')
|
||||
hookdata = self.irc.takeHooks()[0]
|
||||
self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'ts': 1433728673})
|
||||
hookdata = self.irc.takeHooks()[-1][-1]
|
||||
self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'ts': 1433728673, 'oldnick': 'Derp_'})
|
||||
self.assertEqual('9PYAAAAAA', self.irc.users['9PYAAAAAA'].nick)
|
||||
|
||||
def testInviteHook(self):
|
||||
utils.add_hook(self.irc.dummyhook, 'INVITE')
|
||||
def testHandleInvite(self):
|
||||
self.irc.run(':10XAAAAAA INVITE 9PYAAAAAA #blah 0')
|
||||
hookdata = self.irc.takeHooks()[0]
|
||||
hookdata = self.irc.takeHooks()[-1][-1]
|
||||
del hookdata['ts']
|
||||
self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'channel': '#blah'})
|
||||
|
||||
|
@ -4,7 +4,6 @@ cwd = os.getcwd()
|
||||
sys.path += [cwd, os.path.join(cwd, 'plugins')]
|
||||
import unittest
|
||||
|
||||
import utils
|
||||
import classes
|
||||
import relay
|
||||
|
||||
@ -13,28 +12,34 @@ def dummyf():
|
||||
|
||||
class TestRelay(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.irc = classes.FakeIRC('unittest', classes.FakeProto(), classes.testconf)
|
||||
self.irc = classes.FakeIRC('unittest', classes.FakeProto())
|
||||
self.irc.maxnicklen = 20
|
||||
self.irc.proto.__name__ = "test"
|
||||
self.f = relay.normalizeNick
|
||||
self.f = lambda nick: relay.normalizeNick(self.irc, 'unittest', nick)
|
||||
# Fake our protocol name to something that supports slashes in nicks.
|
||||
# relay uses a whitelist for this to prevent accidentally introducing
|
||||
# bad nicks:
|
||||
self.irc.proto.__name__ = "inspircd"
|
||||
|
||||
def testNormalizeNick(self):
|
||||
# Second argument simply states the suffix.
|
||||
self.assertEqual(self.f(self.irc, 'unittest', 'helloworld'), 'helloworld/unittest')
|
||||
self.assertEqual(self.f(self.irc, 'unittest', 'ObnoxiouslyLongNick'), 'Obnoxiously/unittest')
|
||||
self.assertEqual(self.f(self.irc, 'unittest', '10XAAAAAA'), '_10XAAAAAA/unittest')
|
||||
self.assertEqual(self.f('helloworld'), 'helloworld/unittest')
|
||||
self.assertEqual(self.f('ObnoxiouslyLongNick'), 'Obnoxiously/unittest')
|
||||
self.assertEqual(self.f('10XAAAAAA'), '_10XAAAAAA/unittest')
|
||||
|
||||
def testNormalizeNickConflict(self):
|
||||
self.assertEqual(self.f(self.irc, 'unittest', 'helloworld'), 'helloworld/unittest')
|
||||
self.assertEqual(self.f('helloworld'), 'helloworld/unittest')
|
||||
self.irc.users['10XAAAAAA'] = classes.IrcUser('helloworld/unittest', 1234, '10XAAAAAA')
|
||||
# Increase amount of /'s by one
|
||||
self.assertEqual(self.f(self.irc, 'unittest', 'helloworld'), 'helloworld//unittest')
|
||||
self.assertEqual(self.f('helloworld'), 'helloworld//unittest')
|
||||
self.irc.users['10XAAAAAB'] = classes.IrcUser('helloworld//unittest', 1234, '10XAAAAAB')
|
||||
# Cut off the nick, not the suffix if the result is too long.
|
||||
self.assertEqual(self.f(self.irc, 'unittest', 'helloworld'), 'helloworl///unittest')
|
||||
self.assertEqual(self.f('helloworld'), 'helloworl///unittest')
|
||||
|
||||
def testNormalizeNickRemovesSlashes(self):
|
||||
self.irc.proto.__name__ = "charybdis"
|
||||
self.assertEqual(self.f(self.irc, 'unittest', 'helloworld'), 'helloworld|unittest')
|
||||
self.assertEqual(self.f(self.irc, 'unittest', 'abcde/eJanus'), 'abcde|eJanu|unittest')
|
||||
self.assertEqual(self.f(self.irc, 'unittest', 'ObnoxiouslyLongNick'), 'Obnoxiously|unittest')
|
||||
try:
|
||||
self.assertEqual(self.f('helloworld'), 'helloworld|unittest')
|
||||
self.assertEqual(self.f('abcde/eJanus'), 'abcde|eJanu|unittest')
|
||||
self.assertEqual(self.f('ObnoxiouslyLongNick'), 'Obnoxiously|unittest')
|
||||
finally:
|
||||
self.irc.proto.__name__ = "inspircd"
|
||||
|
@ -5,11 +5,16 @@ import unittest
|
||||
import itertools
|
||||
|
||||
import utils
|
||||
import classes
|
||||
import world
|
||||
|
||||
def dummyf():
|
||||
pass
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.irc = classes.FakeIRC('fakeirc', classes.FakeProto())
|
||||
|
||||
def testTS6UIDGenerator(self):
|
||||
uidgen = utils.TS6UIDGenerator('9PY')
|
||||
self.assertEqual(uidgen.next_uid(), '9PYAAAAAA')
|
||||
@ -21,16 +26,16 @@ class TestUtils(unittest.TestCase):
|
||||
utils.add_cmd(dummyf)
|
||||
utils.add_cmd(dummyf, 'TEST')
|
||||
# All command names should be automatically lowercased.
|
||||
self.assertIn('dummyf', utils.bot_commands)
|
||||
self.assertIn('test', utils.bot_commands)
|
||||
self.assertNotIn('TEST', utils.bot_commands)
|
||||
self.assertIn('dummyf', world.bot_commands)
|
||||
self.assertIn('test', world.bot_commands)
|
||||
self.assertNotIn('TEST', world.bot_commands)
|
||||
|
||||
def test_add_hook(self):
|
||||
utils.add_hook(dummyf, 'join')
|
||||
self.assertIn('JOIN', utils.command_hooks)
|
||||
self.assertIn('JOIN', world.command_hooks)
|
||||
# Command names stored in uppercase.
|
||||
self.assertNotIn('join', utils.command_hooks)
|
||||
self.assertIn(dummyf, utils.command_hooks['JOIN'])
|
||||
self.assertNotIn('join', world.command_hooks)
|
||||
self.assertIn(dummyf, world.command_hooks['JOIN'])
|
||||
|
||||
def testIsNick(self):
|
||||
self.assertFalse(utils.isNick('abcdefgh', nicklen=3))
|
||||
@ -96,5 +101,19 @@ class TestUtils(unittest.TestCase):
|
||||
('+b', '*!*@*.badisp.net')])
|
||||
self.assertEqual(res, '-o+l-nm+kb 9PYAAAAAA 50 hello *!*@*.badisp.net')
|
||||
|
||||
@unittest.skip('Wait, we need to work out the kinks first! (reversing changes of modes with arguments)')
|
||||
def testReverseModes(self):
|
||||
f = lambda x: utils.reverseModes(self.irc, '#test', x)
|
||||
# Strings.
|
||||
self.assertEqual(f("+nt-lk"), "-nt+lk")
|
||||
self.assertEqual(f("nt-k"), "-nt+k")
|
||||
# Lists.
|
||||
self.assertEqual(f([('+m', None), ('+t', None), ('+l', '3'), ('-o', 'person')]),
|
||||
[('-m', None), ('-t', None), ('-l', '3'), ('+o', 'person')])
|
||||
# Sets.
|
||||
self.assertEqual(f({('s', None), ('+o', 'whoever')}), {('-s', None), ('-o', 'whoever')})
|
||||
# Combining modes with an initial + and those without
|
||||
self.assertEqual(f({('s', None), ('+n', None)}), {('-s', None), ('-n', None)})
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
96
tests/tests_common.py
Normal file
96
tests/tests_common.py
Normal 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)
|
79
utils.py
79
utils.py
@ -1,19 +1,13 @@
|
||||
import string
|
||||
import re
|
||||
from collections import defaultdict
|
||||
import threading
|
||||
import inspect
|
||||
|
||||
from log import log
|
||||
import world
|
||||
|
||||
global bot_commands, command_hooks
|
||||
# This should be a mapping of command names to functions
|
||||
bot_commands = {}
|
||||
command_hooks = defaultdict(list)
|
||||
networkobjects = {}
|
||||
schedulers = {}
|
||||
plugins = []
|
||||
whois_handlers = []
|
||||
started = threading.Event()
|
||||
# This is separate from classes.py to prevent import loops.
|
||||
class NotAuthenticatedError(Exception):
|
||||
pass
|
||||
|
||||
class TS6UIDGenerator():
|
||||
"""TS6 UID Generator module, adapted from InspIRCd source
|
||||
@ -114,12 +108,12 @@ def add_cmd(func, name=None):
|
||||
if name is None:
|
||||
name = func.__name__
|
||||
name = name.lower()
|
||||
bot_commands[name] = func
|
||||
world.bot_commands[name].append(func)
|
||||
|
||||
def add_hook(func, command):
|
||||
"""Add a hook <func> for command <command>."""
|
||||
command = command.upper()
|
||||
command_hooks[command].append(func)
|
||||
world.command_hooks[command].append(func)
|
||||
|
||||
def toLower(irc, text):
|
||||
"""<irc object> <text>
|
||||
@ -168,7 +162,7 @@ def isServerName(s):
|
||||
return _isASCII(s) and '.' in s and not s.startswith('.')
|
||||
|
||||
def parseModes(irc, target, args):
|
||||
"""Parses a mode string into a list of (mode, argument) tuples.
|
||||
"""Parses a modestring list into a list of (mode, argument) tuples.
|
||||
['+mitl-o', '3', 'person'] => [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')]
|
||||
"""
|
||||
# http://www.irc.org/tech_docs/005.html
|
||||
@ -340,6 +334,34 @@ def joinModes(modes):
|
||||
modelist += ' %s' % ' '.join(args)
|
||||
return modelist
|
||||
|
||||
def reverseModes(irc, target, modes):
|
||||
"""<mode string/mode list>
|
||||
|
||||
Reverses/Inverts the mode string or mode list given.
|
||||
|
||||
"+nt-lk" => "-nt+lk"
|
||||
"nt-k" => "-nt+k"
|
||||
[('+m', None), ('+t', None), ('+l', '3'), ('-o', 'person')] =>
|
||||
[('-m', None), ('-t', None), ('-l', '3'), ('+o', 'person')]
|
||||
[('s', None), ('+n', None)] => [('-s', None), ('-n', None)]
|
||||
"""
|
||||
origtype = type(modes)
|
||||
# Operate on joined modestrings only; it's easier.
|
||||
if origtype != str:
|
||||
modes = joinModes(modes)
|
||||
# Swap the +'s and -'s by replacing one with a dummy character, and then changing it back.
|
||||
assert '\x00' not in modes, 'NUL cannot be in the mode list (it is a reserved character)!'
|
||||
if not modes.startswith(('+', '-')):
|
||||
modes = '+' + modes
|
||||
newmodes = modes.replace('+', '\x00')
|
||||
newmodes = newmodes.replace('-', '+')
|
||||
newmodes = newmodes.replace('\x00', '-')
|
||||
if origtype != str:
|
||||
# If the original query isn't a string, send back the parseModes() output.
|
||||
return parseModes(irc, target, newmodes.split(" "))
|
||||
else:
|
||||
return newmodes
|
||||
|
||||
def isInternalClient(irc, numeric):
|
||||
"""<irc object> <client numeric>
|
||||
|
||||
@ -357,15 +379,38 @@ def isInternalServer(irc, sid):
|
||||
"""
|
||||
return (sid in irc.servers and irc.servers[sid].internal)
|
||||
|
||||
def isOper(irc, uid):
|
||||
def isOper(irc, uid, allowAuthed=True, allowOper=True):
|
||||
"""<irc object> <UID>
|
||||
|
||||
Returns whether <UID> has operator status on PyLink. This can be achieved
|
||||
by either identifying to PyLink as admin, or having user mode +o set.
|
||||
by either identifying to PyLink as admin (if allowAuthed is True),
|
||||
or having user mode +o set (if allowOper is True). At least one of
|
||||
allowAuthed or allowOper must be True for this to give any meaningful
|
||||
results.
|
||||
"""
|
||||
return (uid in irc.users and (("o", None) in irc.users[uid].modes or irc.users[uid].identified))
|
||||
if uid in irc.users:
|
||||
if allowOper and ("o", None) in irc.users[uid].modes:
|
||||
return True
|
||||
elif allowAuthed and irc.users[uid].identified:
|
||||
return True
|
||||
return False
|
||||
|
||||
def checkAuthenticated(irc, uid, allowAuthed=True, allowOper=True):
|
||||
"""<irc object> <UID>
|
||||
|
||||
Checks whether user <UID> has operator status on PyLink, raising
|
||||
NotAuthenticatedError and logging the access denial if not."""
|
||||
lastfunc = inspect.stack()[1][3]
|
||||
if not isOper(irc, uid, allowAuthed=allowAuthed, allowOper=allowOper):
|
||||
log.warning('(%s) Access denied for %s calling %r', irc.name,
|
||||
getHostmask(irc, uid), lastfunc)
|
||||
raise NotAuthenticatedError("You are not authenticated!")
|
||||
return True
|
||||
|
||||
def getHostmask(irc, user):
|
||||
"""<irc object> <UID>
|
||||
|
||||
Gets the hostmask of user <UID>, if present."""
|
||||
userobj = irc.users.get(user)
|
||||
if userobj is None:
|
||||
return '<user object not found>'
|
||||
|
18
world.py
Normal file
18
world.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user