3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-11-01 01:09:22 +01:00

Merge branch 'master' into wip/unrealircd

This commit is contained in:
James Lu 2015-10-08 20:14:30 -07:00
commit 7c7f07b3a9
24 changed files with 926 additions and 308 deletions

View File

@ -8,6 +8,8 @@ PyLink is an extensible, plugin-based IRC Services framework written in Python.
That said, please report any bugs you find to the [issue tracker](https://github.com/GLolol/PyLink/issues). Pull requests are open if you'd like to contribute: note that **master** is bugfix only; new stuff goes to the **devel** branch.
You can also find support via our IRC channel: `#PyLink at irc.overdrive.pw` ([webchat](http://webchat.overdrive.pw/?channels=PyLink)). Ask your question and be patient.
### Dependencies
Dependencies currently include:

View File

@ -9,8 +9,8 @@ import hashlib
from copy import deepcopy
from log import log
from conf import conf
import world
import utils
### Exceptions
@ -21,11 +21,20 @@ class ProtocolError(Exception):
class Irc():
def initVars(self):
self.sid = self.serverdata["sid"]
self.botdata = self.conf['bot']
self.pingfreq = self.serverdata.get('pingfreq') or 30
self.pingtimeout = self.pingfreq * 2
self.connected.clear()
self.aborted.clear()
self.pseudoclient = None
self.lastping = time.time()
# Internal variable to set the place the last command was called (in PM
# or in a channel), used by fantasy command support.
self.called_by = None
# Server, channel, and user indexes to be populated by our protocol module
self.servers = {self.sid: IrcServer(None, self.serverdata['hostname'],
internal=True, desc=self.serverdata.get('serverdesc')
@ -58,7 +67,7 @@ class Irc():
self.uplink = None
self.start_ts = int(time.time())
def __init__(self, netname, proto):
def __init__(self, netname, proto, conf):
# Initialize some variables
self.name = netname.lower()
self.conf = conf
@ -84,13 +93,14 @@ class Irc():
self.pingTimer = None
def connect(self):
ip = self.serverdata["ip"]
port = self.serverdata["port"]
while True:
self.initVars()
ip = self.serverdata["ip"]
port = self.serverdata["port"]
checks_ok = True
try:
self.socket = socket.socket()
stype = socket.AF_INET6 if self.serverdata.get("ipv6") else socket.AF_INET
self.socket = socket.socket(stype)
self.socket.setblocking(0)
# Initial connection timeout is a lot smaller than the timeout after
# we've connected; this is intentional.
@ -160,20 +170,41 @@ class Irc():
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:
if autoconnect is not None and autoconnect >= 1:
log.info('(%s) Going to auto-reconnect in %s seconds.', self.name, autoconnect)
time.sleep(autoconnect)
else:
log.info('(%s) Stopping connect loop (autoconnect value %r is < 1).', self.name, autoconnect)
return
def callCommand(self, source, text):
cmd_args = text.strip().split(' ')
cmd = cmd_args[0].lower()
cmd_args = cmd_args[1:]
if cmd not in world.commands:
self.msg(self.called_by or source, 'Error: Unknown command %r.' % cmd)
return
log.info('(%s) Calling command %r for %s', self.name, cmd, utils.getHostmask(self, source))
for func in world.commands[cmd]:
try:
func(self, source, cmd_args)
except utils.NotAuthenticatedError:
self.msg(self.called_by or source, 'Error: You are not authorized to perform this operation.')
except Exception as e:
log.exception('Unhandled exception caught in command %r', cmd)
self.msg(self.called_by or source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
def msg(self, target, text, notice=False, source=None):
"""Handy function to send messages/notices to clients. Source
is optional, and defaults to the main PyLink client if not specified."""
source = source or self.pseudoclient.uid
if notice:
self.proto.noticeClient(source, target, text)
cmd = 'PYLINK_SELF_NOTICE'
else:
self.proto.messageClient(source, target, text)
cmd = 'PYLINK_SELF_PRIVMSG'
self.callHooks([source, cmd, {'target': target, 'text': text}])
def _disconnect(self):
log.debug('(%s) Canceling pingTimer at %s due to _disconnect() call', self.name, time.time())
@ -242,9 +273,10 @@ class Irc():
log.debug('(%s) Parsed args %r received from %s handler (calling hook %s)',
self.name, parsed_args, command, hook_cmd)
# Iterate over hooked functions, catching errors accordingly
for hook_func in world.command_hooks[hook_cmd]:
for hook_func in world.hooks[hook_cmd]:
try:
log.debug('(%s) Calling function %s', self.name, hook_func)
log.debug('(%s) Calling hook function %s from plugin "%s"', self.name,
hook_func, hook_func.__module__)
hook_func(self, numeric, command, parsed_args)
except Exception:
# We don't want plugins to crash our servers...
@ -286,6 +318,9 @@ class Irc():
# contents of Irc().pseudoclient change.
self.callHooks([self.sid, 'PYLINK_SPAWNMAIN', {'olduser': olduserobj}])
def __repr__(self):
return "<classes.Irc object for %r>" % self.name
class IrcUser():
def __init__(self, nick, ts, uid, ident='null', host='null',
realname='PyLink dummy client', realhost='null',
@ -408,6 +443,44 @@ class Protocol():
self.casemapping = 'rfc1459'
self.hook_map = {}
def parseArgs(self, args):
"""Parses a string of RFC1459-style arguments split into a list, where ":" may
be used for multi-word arguments that last until the end of a line.
"""
real_args = []
for idx, arg in enumerate(args):
real_args.append(arg)
# If the argument starts with ':' and ISN'T the first argument.
# The first argument is used for denoting the source UID/SID.
if arg.startswith(':') and idx != 0:
# : is used for multi-word arguments that last until the end
# of the message. We can use list splicing here to turn them all
# into one argument.
# Set the last arg to a joined version of the remaining args
arg = args[idx:]
arg = ' '.join(arg)[1:]
# Cut the original argument list right before the multi-word arg,
# and then append the multi-word arg.
real_args = args[:idx]
real_args.append(arg)
break
return real_args
def removeClient(self, numeric):
"""Internal function to remove a client from our internal state."""
for c, v in self.irc.channels.copy().items():
v.removeuser(numeric)
# Clear empty non-permanent channels.
if not (self.irc.channels[c].users or ((self.irc.cmodes.get('permanent'), None) in self.irc.channels[c].modes)):
del self.irc.channels[c]
assert numeric not in v.users, "IrcChannel's removeuser() is broken!"
sid = numeric[:3]
log.debug('Removing client %s from self.irc.users', numeric)
del self.irc.users[numeric]
log.debug('Removing client %s from self.irc.servers[%s].users', numeric, sid)
self.irc.servers[sid].users.discard(numeric)
class FakeProto(Protocol):
"""Dummy protocol module for testing purposes."""
def handle_events(self, data):

34
conf.py
View File

@ -31,9 +31,36 @@ testconf = {'bot':
'sidrange': '8##'
})
}
def validateConf(conf):
"""Validates a parsed configuration dict."""
assert type(conf) == dict, "Invalid configuration given: should be type dict, not %s." % type(conf).__name__
for section in ('bot', 'servers', 'login'):
assert conf.get(section), "Missing %r section in config." % section
for netname, serverblock in conf['servers'].items():
for section in ('ip', 'port', 'recvpass', 'sendpass', 'hostname',
'sid', 'sidrange', 'protocol', 'maxnicklen'):
assert serverblock.get(section), "Missing %r in server block for %r." % (section, netname)
assert type(serverblock.get('channels')) == list, "'channels' option in " \
"server block for %s must be a list, not %s." % (netname, type(serverblock['channels']).__name__)
assert type(conf['login'].get('password')) == type(conf['login'].get('user')) == str and \
conf['login']['password'] != "changeme", "You have not set the login details correctly!"
return conf
def loadConf(fname):
"""Loads a PyLink configuration file from the filename given."""
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)
return conf
if world.testing:
conf = testconf
confname = 'testconf'
fname = None
else:
try:
# Get the config name from the command line, falling back to config.yml
@ -46,9 +73,4 @@ else:
# we load.
confname = 'pylink'
fname = 'config.yml'
with open(fname, 'r') as f:
try:
conf = yaml.load(f)
except Exception as e:
print('ERROR: Failed to load config from %r: %s: %s' % (fname, type(e).__name__, e))
sys.exit(4)
conf = validateConf(loadConf(fname))

View File

@ -15,6 +15,9 @@ bot:
# Console log verbosity: see https://docs.python.org/3/library/logging.html#logging-levels
loglevel: DEBUG
# Fantasy command prefix, if the fantasy plugin is loaded.
prefix: "."
login:
# PyLink administrative login - Change this, or the service will not start!
user: admin
@ -36,6 +39,10 @@ relay:
# will see yours.
show_ips: false
# Whether subservers should be spawned for each relay network (requires reload to change).
# Defaults to true.
spawn_servers: true
servers:
yournet:
# Server IP, port, and passwords
@ -67,7 +74,7 @@ servers:
# (omitting the .py extension).
protocol: "inspircd"
# Sets autoconnect delay - comment this out or set the value negative to disable autoconnect entirely.
# Sets autoconnect delay - comment this out or set the value below 1 to disable autoconnect entirely.
autoconnect: 5
# Sets ping frequency (i.e. how long we should wait between sending pings to our uplink).
@ -94,7 +101,11 @@ servers:
# ssl_fingerprint: "e0fee1adf795c84eec4735f039503eb18d9c35cc"
ts6net:
ip: 127.0.0.1
ip: ::1
# Determines whether IPv6 should be used for this connection.
ipv6: yes
port: 7000
recvpass: "abcd"
sendpass: "abcd"
@ -129,7 +140,19 @@ servers:
# Plugins to load (omit the .py extension)
plugins:
# Commands plugin: provides core commands such as logging in, shutting down PyLink, and
# various command help.
- commands
# Networks plugin: allows you to manage (dis)connections to networks while PyLink is running.
- networks
# Bots plugin: allows you to manipulate pseudo-clients (bots) on networks.
# - bots
# Relay plugin: Janus-style server-side relay plugin.
# - relay
# Fantasy plugin: allows you to trigger commands in channels by PyLink's nick or a
# configurable prefix character.
# - fantasy

View File

@ -20,23 +20,10 @@ utils.add_hook(handle_kick, 'KICK')
def handle_commands(irc, source, command, args):
"""Handle commands sent to the PyLink client (PRIVMSG)."""
if args['target'] == irc.pseudoclient.uid:
text = args['text'].strip()
cmd_args = text.split(' ')
cmd = cmd_args[0].lower()
cmd_args = cmd_args[1:]
if cmd not in world.bot_commands:
irc.msg(source, 'Error: Unknown command %r.' % cmd)
return
log.info('(%s) Calling command %r for %s', irc.name, cmd, utils.getHostmask(irc, source))
for func in world.bot_commands[cmd]:
try:
func(irc, source, cmd_args)
except utils.NotAuthenticatedError:
irc.msg(source, 'Error: You are not authorized to perform this operation.')
except Exception as e:
log.exception('Unhandled exception caught in command %r', cmd)
irc.msg(source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
if args['target'] == irc.pseudoclient.uid and not utils.isInternalClient(irc, source):
irc.called_by = source
irc.callCommand(source, args['text'])
utils.add_hook(handle_commands, 'PRIVMSG')
def handle_whois(irc, source, command, args):

View File

@ -18,6 +18,6 @@ PyLink is an a modular, plugin-based IRC PseudoService framework. It uses swappa
#### Future topics (not yet available)
- [Using PyLink's utils module](using-utils.md)
- [PyLink hooks reference](hooks-reference.md)
- [Writing tests for PyLink modules](writing-tests.md)
- [Using PyLink's utils module](using-utils.md)

View File

@ -74,9 +74,11 @@ For&nbsp;InspIRCd,&nbsp;the&nbsp;only&nbsp;ENCAP&nbsp;command&nbsp;we&nbsp;handl
<dl><dt><a name="InspIRCdProtocol-handle_endburst"><strong>handle_endburst</strong></a>(self, numeric, command, args)</dt><dd><tt>ENDBURST&nbsp;handler;&nbsp;sends&nbsp;a&nbsp;hook&nbsp;with&nbsp;empty&nbsp;contents.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_events"><strong>handle_events</strong></a>(self, data)</dt><dd><tt>Event&nbsp;handler&nbsp;for&nbsp;the&nbsp;InspIRCd&nbsp;protocol.&nbsp;This&nbsp;passes&nbsp;most&nbsp;commands&nbsp;to<br>
the&nbsp;various&nbsp;handle_ABCD()&nbsp;functions&nbsp;elsewhere&nbsp;in&nbsp;this&nbsp;module,&nbsp;but&nbsp;also<br>
handles&nbsp;commands&nbsp;sent&nbsp;in&nbsp;the&nbsp;initial&nbsp;server&nbsp;linking&nbsp;phase.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_events"><strong>handle_events</strong></a>(self, data)</dt><dd><tt>Event&nbsp;handler&nbsp;for&nbsp;the&nbsp;InspIRCd&nbsp;protocol.<br>
&nbsp;<br>
This&nbsp;passes&nbsp;most&nbsp;commands&nbsp;to&nbsp;the&nbsp;various&nbsp;handle_ABCD()&nbsp;functions<br>
elsewhere&nbsp;in&nbsp;this&nbsp;module,&nbsp;but&nbsp;also&nbsp;handles&nbsp;commands&nbsp;sent&nbsp;in&nbsp;the<br>
initial&nbsp;server&nbsp;linking&nbsp;phase.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_fhost"><strong>handle_fhost</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;FHOST,&nbsp;used&nbsp;for&nbsp;denoting&nbsp;hostname&nbsp;changes.</tt></dd></dl>
@ -94,6 +96,8 @@ handles&nbsp;commands&nbsp;sent&nbsp;in&nbsp;the&nbsp;initial&nbsp;server&nbsp;l
<dl><dt><a name="InspIRCdProtocol-handle_invite"><strong>handle_invite</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;INVITEs.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_mode"><strong>handle_mode</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;user&nbsp;mode&nbsp;changes.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_opertype"><strong>handle_opertype</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;OPERTYPE,&nbsp;which&nbsp;is&nbsp;used&nbsp;to&nbsp;denote&nbsp;an&nbsp;oper&nbsp;up.<br>
&nbsp;<br>
This&nbsp;calls&nbsp;the&nbsp;internal&nbsp;hook&nbsp;PYLINK_CLIENT_OPERED,&nbsp;sets&nbsp;the&nbsp;internal<br>
@ -101,9 +105,11 @@ opertype&nbsp;of&nbsp;the&nbsp;client,&nbsp;and&nbsp;assumes&nbsp;setting&nbsp;u
<dl><dt><a name="InspIRCdProtocol-handle_ping"><strong>handle_ping</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;PING&nbsp;commands,&nbsp;so&nbsp;we&nbsp;don't&nbsp;time&nbsp;out.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_pong"><strong>handle_pong</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;PONG&nbsp;commands.&nbsp;This&nbsp;is&nbsp;used&nbsp;to&nbsp;keep&nbsp;track&nbsp;of&nbsp;whether<br>
the&nbsp;uplink&nbsp;is&nbsp;alive&nbsp;by&nbsp;the&nbsp;Irc()&nbsp;internals&nbsp;-&nbsp;a&nbsp;server&nbsp;that&nbsp;fails&nbsp;to&nbsp;reply<br>
to&nbsp;our&nbsp;PINGs&nbsp;eventually&nbsp;times&nbsp;out&nbsp;and&nbsp;is&nbsp;disconnected.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_pong"><strong>handle_pong</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;PONG&nbsp;commands.<br>
&nbsp;<br>
This&nbsp;is&nbsp;used&nbsp;to&nbsp;keep&nbsp;track&nbsp;of&nbsp;whether&nbsp;the&nbsp;uplink&nbsp;is&nbsp;alive&nbsp;by&nbsp;the&nbsp;Irc()<br>
internals&nbsp;-&nbsp;a&nbsp;server&nbsp;that&nbsp;fails&nbsp;to&nbsp;reply&nbsp;to&nbsp;our&nbsp;PINGs&nbsp;eventually<br>
times&nbsp;out&nbsp;and&nbsp;is&nbsp;disconnected.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_server"><strong>handle_server</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;SERVER&nbsp;commands&nbsp;(introduction&nbsp;of&nbsp;servers).</tt></dd></dl>
@ -111,7 +117,7 @@ to&nbsp;our&nbsp;PINGs&nbsp;eventually&nbsp;times&nbsp;out&nbsp;and&nbsp;is&nbsp
<dl><dt><a name="InspIRCdProtocol-inviteClient"><strong>inviteClient</strong></a>(self, numeric, target, channel)</dt><dd><tt>Sends&nbsp;an&nbsp;INVITE&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client..</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-joinClient"><strong>joinClient</strong></a>(self, client, channel)</dt><dd><tt>Joins&nbsp;an&nbsp;internal&nbsp;spawned&nbsp;client&nbsp;&lt;client&gt;&nbsp;to&nbsp;a&nbsp;channel.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-joinClient"><strong>joinClient</strong></a>(self, client, channel)</dt><dd><tt>Joins&nbsp;a&nbsp;PyLink&nbsp;client&nbsp;to&nbsp;a&nbsp;channel.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-killClient"><strong>killClient</strong></a>(self, numeric, target, reason)</dt><dd><tt>Sends&nbsp;a&nbsp;kill&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
@ -119,13 +125,13 @@ to&nbsp;our&nbsp;PINGs&nbsp;eventually&nbsp;times&nbsp;out&nbsp;and&nbsp;is&nbsp
<dl><dt><a name="InspIRCdProtocol-knockClient"><strong>knockClient</strong></a>(self, numeric, target, text)</dt><dd><tt>Sends&nbsp;a&nbsp;KNOCK&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-modeClient"><strong>modeClient</strong></a>(self, numeric, target, modes, ts=None)</dt><dd><tt>Sends&nbsp;modes&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.&nbsp;&lt;modes&gt;&nbsp;should&nbsp;be<br>
<dl><dt><a name="InspIRCdProtocol-modeClient"><strong>modeClient</strong></a>(self, numeric, target, modes, ts=None)</dt><dd><tt>Sends&nbsp;mode&nbsp;changes&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.&nbsp;&lt;modes&gt;&nbsp;should&nbsp;be<br>
a&nbsp;list&nbsp;of&nbsp;(mode,&nbsp;arg)&nbsp;tuples,&nbsp;i.e.&nbsp;the&nbsp;format&nbsp;of&nbsp;utils.parseModes()&nbsp;output.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-modeServer"><strong>modeServer</strong></a>(self, numeric, target, modes, ts=None)</dt><dd><tt>Sends&nbsp;modes&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;server.&nbsp;&lt;list&nbsp;of&nbsp;modes&gt;&nbsp;should&nbsp;be<br>
<dl><dt><a name="InspIRCdProtocol-modeServer"><strong>modeServer</strong></a>(self, numeric, target, modes, ts=None)</dt><dd><tt>Sends&nbsp;mode&nbsp;changes&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;server.&nbsp;&lt;list&nbsp;of&nbsp;modes&gt;&nbsp;should&nbsp;be<br>
a&nbsp;list&nbsp;of&nbsp;(mode,&nbsp;arg)&nbsp;tuples,&nbsp;i.e.&nbsp;the&nbsp;format&nbsp;of&nbsp;utils.parseModes()&nbsp;output.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-numericServer"><strong>numericServer</strong></a>(self, source, numeric, text)</dt></dl>
<dl><dt><a name="InspIRCdProtocol-numericServer"><strong>numericServer</strong></a>(self, source, numeric, target, text)</dt></dl>
<dl><dt><a name="InspIRCdProtocol-pingServer"><strong>pingServer</strong></a>(self, source=None, target=None)</dt><dd><tt>Sends&nbsp;a&nbsp;PING&nbsp;to&nbsp;a&nbsp;target&nbsp;server.&nbsp;Periodic&nbsp;PINGs&nbsp;are&nbsp;sent&nbsp;to&nbsp;our&nbsp;uplink<br>
automatically&nbsp;by&nbsp;the&nbsp;Irc()&nbsp;internals;&nbsp;plugins&nbsp;shouldn't&nbsp;have&nbsp;to&nbsp;use&nbsp;this.</tt></dd></dl>
@ -140,16 +146,19 @@ Example&nbsp;uses:<br>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#Class-sjoinServer">sjoinServer</a>('100',&nbsp;'#test',&nbsp;[('',&nbsp;'100AAABBC'),&nbsp;('qo',&nbsp;100AAABBB'),&nbsp;('h',&nbsp;'100AAADDD')])<br>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#Class-sjoinServer">sjoinServer</a>(self.<strong>irc</strong>.sid,&nbsp;'#test',&nbsp;[('o',&nbsp;self.<strong>irc</strong>.pseudoclient.uid)])</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-spawnClient"><strong>spawnClient</strong></a>(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None)</dt><dd><tt>Spawns&nbsp;a&nbsp;client&nbsp;with&nbsp;nick&nbsp;&lt;nick&gt;&nbsp;on&nbsp;the&nbsp;given&nbsp;IRC&nbsp;connection.<br>
<dl><dt><a name="InspIRCdProtocol-spawnClient"><strong>spawnClient</strong></a>(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None, manipulatable=False)</dt><dd><tt>Spawns&nbsp;a&nbsp;client&nbsp;with&nbsp;nick&nbsp;&lt;nick&gt;&nbsp;on&nbsp;the&nbsp;given&nbsp;IRC&nbsp;connection.<br>
&nbsp;<br>
Note:&nbsp;No&nbsp;nick&nbsp;collision&nbsp;/&nbsp;valid&nbsp;nickname&nbsp;checks&nbsp;are&nbsp;done&nbsp;here;&nbsp;it&nbsp;is<br>
up&nbsp;to&nbsp;plugins&nbsp;to&nbsp;make&nbsp;sure&nbsp;they&nbsp;don't&nbsp;introduce&nbsp;anything&nbsp;invalid.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-spawnServer"><strong>spawnServer</strong></a>(self, name, sid=None, uplink=None, desc=None)</dt><dd><tt>Spawns&nbsp;a&nbsp;server&nbsp;off&nbsp;a&nbsp;PyLink&nbsp;server.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-spawnServer"><strong>spawnServer</strong></a>(self, name, sid=None, uplink=None, desc=None)</dt><dd><tt>Spawns&nbsp;a&nbsp;server&nbsp;off&nbsp;a&nbsp;PyLink&nbsp;server.&nbsp;desc&nbsp;(server&nbsp;description)<br>
defaults&nbsp;to&nbsp;the&nbsp;one&nbsp;in&nbsp;the&nbsp;config.&nbsp;uplink&nbsp;defaults&nbsp;to&nbsp;the&nbsp;main&nbsp;PyLink<br>
server,&nbsp;and&nbsp;sid&nbsp;(the&nbsp;server&nbsp;ID)&nbsp;is&nbsp;automatically&nbsp;generated&nbsp;if&nbsp;not<br>
given.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-squitServer"><strong>squitServer</strong></a>(self, source, target, text='No reason given')</dt><dd><tt>SQUITs&nbsp;a&nbsp;PyLink&nbsp;server.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-topicServer"><strong>topicServer</strong></a>(self, numeric, target, text)</dt><dd><tt>Sends&nbsp;a&nbsp;burst&nbsp;topic&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;server.&nbsp;This&nbsp;is&nbsp;usally&nbsp;used&nbsp;on&nbsp;burst.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-topicServer"><strong>topicServer</strong></a>(self, numeric, target, text)</dt><dd><tt>Sends&nbsp;a&nbsp;topic&nbsp;change&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;server.&nbsp;This&nbsp;is&nbsp;usually&nbsp;used&nbsp;on&nbsp;burst.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-updateClient"><strong>updateClient</strong></a>(self, numeric, field, text)</dt><dd><tt>Updates&nbsp;the&nbsp;ident,&nbsp;host,&nbsp;or&nbsp;realname&nbsp;of&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
@ -161,9 +170,6 @@ Methods inherited from <a href="ts6_common.html#TS6BaseProtocol">ts6_common.TS6B
<dl><dt><a name="InspIRCdProtocol-handle_kill"><strong>handle_kill</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;KILLs.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_mode"><strong>handle_mode</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;user&nbsp;mode&nbsp;changes.&nbsp;For&nbsp;channel&nbsp;mode&nbsp;changes,<br>
TMODE&nbsp;(TS6/charybdis)&nbsp;and&nbsp;FMODE&nbsp;(InspIRCd)&nbsp;are&nbsp;used&nbsp;instead.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_nick"><strong>handle_nick</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;NICK&nbsp;changes.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_notice"><strong>handle_notice</strong></a> = handle_privmsg(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;PRIVMSG/NOTICE.</tt></dd></dl>
@ -172,7 +178,7 @@ TMODE&nbsp;(TS6/charybdis)&nbsp;and&nbsp;FMODE&nbsp;(InspIRCd)&nbsp;are&nbsp;use
<dl><dt><a name="InspIRCdProtocol-handle_privmsg"><strong>handle_privmsg</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;PRIVMSG/NOTICE.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_quit"><strong>handle_quit</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;QUITs.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_quit"><strong>handle_quit</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;QUIT&nbsp;commands.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_save"><strong>handle_save</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;SAVE&nbsp;messages,&nbsp;used&nbsp;to&nbsp;handle&nbsp;nick&nbsp;collisions.</tt></dd></dl>
@ -191,9 +197,6 @@ TB&nbsp;(TS6/charybdis)&nbsp;and&nbsp;FTOPIC&nbsp;(InspIRCd)&nbsp;are&nbsp;used&
<dl><dt><a name="InspIRCdProtocol-noticeClient"><strong>noticeClient</strong></a>(self, numeric, target, text)</dt><dd><tt>Sends&nbsp;a&nbsp;NOTICE&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-parseArgs"><strong>parseArgs</strong></a>(self, args)</dt><dd><tt>Parses&nbsp;a&nbsp;string&nbsp;of&nbsp;<a href="http://www.rfc-editor.org/rfc/rfc1459.txt">RFC1459</a>-style&nbsp;arguments&nbsp;split&nbsp;into&nbsp;a&nbsp;list,&nbsp;where&nbsp;":"&nbsp;may<br>
be&nbsp;used&nbsp;for&nbsp;multi-word&nbsp;arguments&nbsp;that&nbsp;last&nbsp;until&nbsp;the&nbsp;end&nbsp;of&nbsp;a&nbsp;line.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-parseTS6Args"><strong>parseTS6Args</strong></a>(self, args)</dt><dd><tt>Similar&nbsp;to&nbsp;<a href="#Class-parseArgs">parseArgs</a>(),&nbsp;but&nbsp;stripping&nbsp;leading&nbsp;colons&nbsp;from&nbsp;the&nbsp;first&nbsp;argument<br>
of&nbsp;a&nbsp;line&nbsp;(usually&nbsp;the&nbsp;sender&nbsp;field).</tt></dd></dl>
@ -201,10 +204,15 @@ of&nbsp;a&nbsp;line&nbsp;(usually&nbsp;the&nbsp;sender&nbsp;field).</tt></dd></d
<dl><dt><a name="InspIRCdProtocol-quitClient"><strong>quitClient</strong></a>(self, numeric, reason)</dt><dd><tt>Quits&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-removeClient"><strong>removeClient</strong></a>(self, numeric)</dt><dd><tt>Internal&nbsp;function&nbsp;to&nbsp;remove&nbsp;a&nbsp;client&nbsp;from&nbsp;our&nbsp;internal&nbsp;state.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-topicClient"><strong>topicClient</strong></a>(self, numeric, target, text)</dt><dd><tt>Sends&nbsp;a&nbsp;TOPIC&nbsp;change&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<hr>
Methods inherited from <a href="classes.html#Protocol">classes.Protocol</a>:<br>
<dl><dt><a name="InspIRCdProtocol-parseArgs"><strong>parseArgs</strong></a>(self, args)</dt><dd><tt>Parses&nbsp;a&nbsp;string&nbsp;of&nbsp;<a href="http://www.rfc-editor.org/rfc/rfc1459.txt">RFC1459</a>-style&nbsp;arguments&nbsp;split&nbsp;into&nbsp;a&nbsp;list,&nbsp;where&nbsp;":"&nbsp;may<br>
be&nbsp;used&nbsp;for&nbsp;multi-word&nbsp;arguments&nbsp;that&nbsp;last&nbsp;until&nbsp;the&nbsp;end&nbsp;of&nbsp;a&nbsp;line.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-removeClient"><strong>removeClient</strong></a>(self, numeric)</dt><dd><tt>Internal&nbsp;function&nbsp;to&nbsp;remove&nbsp;a&nbsp;client&nbsp;from&nbsp;our&nbsp;internal&nbsp;state.</tt></dd></dl>
<hr>
Data descriptors inherited from <a href="classes.html#Protocol">classes.Protocol</a>:<br>
<dl><dt><strong>__dict__</strong></dt>
@ -245,9 +253,11 @@ For&nbsp;InspIRCd,&nbsp;the&nbsp;only&nbsp;ENCAP&nbsp;command&nbsp;we&nbsp;handl
<dl><dt><a name="InspIRCdProtocol-handle_endburst"><strong>handle_endburst</strong></a>(self, numeric, command, args)</dt><dd><tt>ENDBURST&nbsp;handler;&nbsp;sends&nbsp;a&nbsp;hook&nbsp;with&nbsp;empty&nbsp;contents.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_events"><strong>handle_events</strong></a>(self, data)</dt><dd><tt>Event&nbsp;handler&nbsp;for&nbsp;the&nbsp;InspIRCd&nbsp;protocol.&nbsp;This&nbsp;passes&nbsp;most&nbsp;commands&nbsp;to<br>
the&nbsp;various&nbsp;handle_ABCD()&nbsp;functions&nbsp;elsewhere&nbsp;in&nbsp;this&nbsp;module,&nbsp;but&nbsp;also<br>
handles&nbsp;commands&nbsp;sent&nbsp;in&nbsp;the&nbsp;initial&nbsp;server&nbsp;linking&nbsp;phase.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_events"><strong>handle_events</strong></a>(self, data)</dt><dd><tt>Event&nbsp;handler&nbsp;for&nbsp;the&nbsp;InspIRCd&nbsp;protocol.<br>
&nbsp;<br>
This&nbsp;passes&nbsp;most&nbsp;commands&nbsp;to&nbsp;the&nbsp;various&nbsp;handle_ABCD()&nbsp;functions<br>
elsewhere&nbsp;in&nbsp;this&nbsp;module,&nbsp;but&nbsp;also&nbsp;handles&nbsp;commands&nbsp;sent&nbsp;in&nbsp;the<br>
initial&nbsp;server&nbsp;linking&nbsp;phase.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_fhost"><strong>handle_fhost</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;FHOST,&nbsp;used&nbsp;for&nbsp;denoting&nbsp;hostname&nbsp;changes.</tt></dd></dl>
@ -265,6 +275,8 @@ handles&nbsp;commands&nbsp;sent&nbsp;in&nbsp;the&nbsp;initial&nbsp;server&nbsp;l
<dl><dt><a name="InspIRCdProtocol-handle_invite"><strong>handle_invite</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;INVITEs.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_mode"><strong>handle_mode</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;user&nbsp;mode&nbsp;changes.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_opertype"><strong>handle_opertype</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;OPERTYPE,&nbsp;which&nbsp;is&nbsp;used&nbsp;to&nbsp;denote&nbsp;an&nbsp;oper&nbsp;up.<br>
&nbsp;<br>
This&nbsp;calls&nbsp;the&nbsp;internal&nbsp;hook&nbsp;PYLINK_CLIENT_OPERED,&nbsp;sets&nbsp;the&nbsp;internal<br>
@ -272,9 +284,11 @@ opertype&nbsp;of&nbsp;the&nbsp;client,&nbsp;and&nbsp;assumes&nbsp;setting&nbsp;u
<dl><dt><a name="InspIRCdProtocol-handle_ping"><strong>handle_ping</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;PING&nbsp;commands,&nbsp;so&nbsp;we&nbsp;don't&nbsp;time&nbsp;out.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_pong"><strong>handle_pong</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;PONG&nbsp;commands.&nbsp;This&nbsp;is&nbsp;used&nbsp;to&nbsp;keep&nbsp;track&nbsp;of&nbsp;whether<br>
the&nbsp;uplink&nbsp;is&nbsp;alive&nbsp;by&nbsp;the&nbsp;Irc()&nbsp;internals&nbsp;-&nbsp;a&nbsp;server&nbsp;that&nbsp;fails&nbsp;to&nbsp;reply<br>
to&nbsp;our&nbsp;PINGs&nbsp;eventually&nbsp;times&nbsp;out&nbsp;and&nbsp;is&nbsp;disconnected.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_pong"><strong>handle_pong</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;PONG&nbsp;commands.<br>
&nbsp;<br>
This&nbsp;is&nbsp;used&nbsp;to&nbsp;keep&nbsp;track&nbsp;of&nbsp;whether&nbsp;the&nbsp;uplink&nbsp;is&nbsp;alive&nbsp;by&nbsp;the&nbsp;Irc()<br>
internals&nbsp;-&nbsp;a&nbsp;server&nbsp;that&nbsp;fails&nbsp;to&nbsp;reply&nbsp;to&nbsp;our&nbsp;PINGs&nbsp;eventually<br>
times&nbsp;out&nbsp;and&nbsp;is&nbsp;disconnected.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_server"><strong>handle_server</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;SERVER&nbsp;commands&nbsp;(introduction&nbsp;of&nbsp;servers).</tt></dd></dl>
@ -282,7 +296,7 @@ to&nbsp;our&nbsp;PINGs&nbsp;eventually&nbsp;times&nbsp;out&nbsp;and&nbsp;is&nbsp
<dl><dt><a name="InspIRCdProtocol-inviteClient"><strong>inviteClient</strong></a>(self, numeric, target, channel)</dt><dd><tt>Sends&nbsp;an&nbsp;INVITE&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client..</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-joinClient"><strong>joinClient</strong></a>(self, client, channel)</dt><dd><tt>Joins&nbsp;an&nbsp;internal&nbsp;spawned&nbsp;client&nbsp;&lt;client&gt;&nbsp;to&nbsp;a&nbsp;channel.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-joinClient"><strong>joinClient</strong></a>(self, client, channel)</dt><dd><tt>Joins&nbsp;a&nbsp;PyLink&nbsp;client&nbsp;to&nbsp;a&nbsp;channel.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-killClient"><strong>killClient</strong></a>(self, numeric, target, reason)</dt><dd><tt>Sends&nbsp;a&nbsp;kill&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
@ -290,13 +304,13 @@ to&nbsp;our&nbsp;PINGs&nbsp;eventually&nbsp;times&nbsp;out&nbsp;and&nbsp;is&nbsp
<dl><dt><a name="InspIRCdProtocol-knockClient"><strong>knockClient</strong></a>(self, numeric, target, text)</dt><dd><tt>Sends&nbsp;a&nbsp;KNOCK&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-modeClient"><strong>modeClient</strong></a>(self, numeric, target, modes, ts=None)</dt><dd><tt>Sends&nbsp;modes&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.&nbsp;&lt;modes&gt;&nbsp;should&nbsp;be<br>
<dl><dt><a name="InspIRCdProtocol-modeClient"><strong>modeClient</strong></a>(self, numeric, target, modes, ts=None)</dt><dd><tt>Sends&nbsp;mode&nbsp;changes&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.&nbsp;&lt;modes&gt;&nbsp;should&nbsp;be<br>
a&nbsp;list&nbsp;of&nbsp;(mode,&nbsp;arg)&nbsp;tuples,&nbsp;i.e.&nbsp;the&nbsp;format&nbsp;of&nbsp;utils.parseModes()&nbsp;output.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-modeServer"><strong>modeServer</strong></a>(self, numeric, target, modes, ts=None)</dt><dd><tt>Sends&nbsp;modes&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;server.&nbsp;&lt;list&nbsp;of&nbsp;modes&gt;&nbsp;should&nbsp;be<br>
<dl><dt><a name="InspIRCdProtocol-modeServer"><strong>modeServer</strong></a>(self, numeric, target, modes, ts=None)</dt><dd><tt>Sends&nbsp;mode&nbsp;changes&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;server.&nbsp;&lt;list&nbsp;of&nbsp;modes&gt;&nbsp;should&nbsp;be<br>
a&nbsp;list&nbsp;of&nbsp;(mode,&nbsp;arg)&nbsp;tuples,&nbsp;i.e.&nbsp;the&nbsp;format&nbsp;of&nbsp;utils.parseModes()&nbsp;output.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-numericServer"><strong>numericServer</strong></a>(self, source, numeric, text)</dt></dl>
<dl><dt><a name="InspIRCdProtocol-numericServer"><strong>numericServer</strong></a>(self, source, numeric, target, text)</dt></dl>
<dl><dt><a name="InspIRCdProtocol-pingServer"><strong>pingServer</strong></a>(self, source=None, target=None)</dt><dd><tt>Sends&nbsp;a&nbsp;PING&nbsp;to&nbsp;a&nbsp;target&nbsp;server.&nbsp;Periodic&nbsp;PINGs&nbsp;are&nbsp;sent&nbsp;to&nbsp;our&nbsp;uplink<br>
automatically&nbsp;by&nbsp;the&nbsp;Irc()&nbsp;internals;&nbsp;plugins&nbsp;shouldn't&nbsp;have&nbsp;to&nbsp;use&nbsp;this.</tt></dd></dl>
@ -311,16 +325,19 @@ Example&nbsp;uses:<br>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#InspIRCdProtocol-sjoinServer">sjoinServer</a>('100',&nbsp;'#test',&nbsp;[('',&nbsp;'100AAABBC'),&nbsp;('qo',&nbsp;100AAABBB'),&nbsp;('h',&nbsp;'100AAADDD')])<br>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#InspIRCdProtocol-sjoinServer">sjoinServer</a>(self.<strong>irc</strong>.sid,&nbsp;'#test',&nbsp;[('o',&nbsp;self.<strong>irc</strong>.pseudoclient.uid)])</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-spawnClient"><strong>spawnClient</strong></a>(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None)</dt><dd><tt>Spawns&nbsp;a&nbsp;client&nbsp;with&nbsp;nick&nbsp;&lt;nick&gt;&nbsp;on&nbsp;the&nbsp;given&nbsp;IRC&nbsp;connection.<br>
<dl><dt><a name="InspIRCdProtocol-spawnClient"><strong>spawnClient</strong></a>(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None, manipulatable=False)</dt><dd><tt>Spawns&nbsp;a&nbsp;client&nbsp;with&nbsp;nick&nbsp;&lt;nick&gt;&nbsp;on&nbsp;the&nbsp;given&nbsp;IRC&nbsp;connection.<br>
&nbsp;<br>
Note:&nbsp;No&nbsp;nick&nbsp;collision&nbsp;/&nbsp;valid&nbsp;nickname&nbsp;checks&nbsp;are&nbsp;done&nbsp;here;&nbsp;it&nbsp;is<br>
up&nbsp;to&nbsp;plugins&nbsp;to&nbsp;make&nbsp;sure&nbsp;they&nbsp;don't&nbsp;introduce&nbsp;anything&nbsp;invalid.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-spawnServer"><strong>spawnServer</strong></a>(self, name, sid=None, uplink=None, desc=None)</dt><dd><tt>Spawns&nbsp;a&nbsp;server&nbsp;off&nbsp;a&nbsp;PyLink&nbsp;server.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-spawnServer"><strong>spawnServer</strong></a>(self, name, sid=None, uplink=None, desc=None)</dt><dd><tt>Spawns&nbsp;a&nbsp;server&nbsp;off&nbsp;a&nbsp;PyLink&nbsp;server.&nbsp;desc&nbsp;(server&nbsp;description)<br>
defaults&nbsp;to&nbsp;the&nbsp;one&nbsp;in&nbsp;the&nbsp;config.&nbsp;uplink&nbsp;defaults&nbsp;to&nbsp;the&nbsp;main&nbsp;PyLink<br>
server,&nbsp;and&nbsp;sid&nbsp;(the&nbsp;server&nbsp;ID)&nbsp;is&nbsp;automatically&nbsp;generated&nbsp;if&nbsp;not<br>
given.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-squitServer"><strong>squitServer</strong></a>(self, source, target, text='No reason given')</dt><dd><tt>SQUITs&nbsp;a&nbsp;PyLink&nbsp;server.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-topicServer"><strong>topicServer</strong></a>(self, numeric, target, text)</dt><dd><tt>Sends&nbsp;a&nbsp;burst&nbsp;topic&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;server.&nbsp;This&nbsp;is&nbsp;usally&nbsp;used&nbsp;on&nbsp;burst.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-topicServer"><strong>topicServer</strong></a>(self, numeric, target, text)</dt><dd><tt>Sends&nbsp;a&nbsp;topic&nbsp;change&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;server.&nbsp;This&nbsp;is&nbsp;usually&nbsp;used&nbsp;on&nbsp;burst.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-updateClient"><strong>updateClient</strong></a>(self, numeric, field, text)</dt><dd><tt>Updates&nbsp;the&nbsp;ident,&nbsp;host,&nbsp;or&nbsp;realname&nbsp;of&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
@ -332,9 +349,6 @@ Methods inherited from <a href="ts6_common.html#TS6BaseProtocol">ts6_common.TS6B
<dl><dt><a name="InspIRCdProtocol-handle_kill"><strong>handle_kill</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;KILLs.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_mode"><strong>handle_mode</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;user&nbsp;mode&nbsp;changes.&nbsp;For&nbsp;channel&nbsp;mode&nbsp;changes,<br>
TMODE&nbsp;(TS6/charybdis)&nbsp;and&nbsp;FMODE&nbsp;(InspIRCd)&nbsp;are&nbsp;used&nbsp;instead.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_nick"><strong>handle_nick</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;NICK&nbsp;changes.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_notice"><strong>handle_notice</strong></a> = handle_privmsg(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;PRIVMSG/NOTICE.</tt></dd></dl>
@ -343,7 +357,7 @@ TMODE&nbsp;(TS6/charybdis)&nbsp;and&nbsp;FMODE&nbsp;(InspIRCd)&nbsp;are&nbsp;use
<dl><dt><a name="InspIRCdProtocol-handle_privmsg"><strong>handle_privmsg</strong></a>(self, source, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;PRIVMSG/NOTICE.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_quit"><strong>handle_quit</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;QUITs.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_quit"><strong>handle_quit</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;QUIT&nbsp;commands.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-handle_save"><strong>handle_save</strong></a>(self, numeric, command, args)</dt><dd><tt>Handles&nbsp;incoming&nbsp;SAVE&nbsp;messages,&nbsp;used&nbsp;to&nbsp;handle&nbsp;nick&nbsp;collisions.</tt></dd></dl>
@ -362,9 +376,6 @@ TB&nbsp;(TS6/charybdis)&nbsp;and&nbsp;FTOPIC&nbsp;(InspIRCd)&nbsp;are&nbsp;used&
<dl><dt><a name="InspIRCdProtocol-noticeClient"><strong>noticeClient</strong></a>(self, numeric, target, text)</dt><dd><tt>Sends&nbsp;a&nbsp;NOTICE&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-parseArgs"><strong>parseArgs</strong></a>(self, args)</dt><dd><tt>Parses&nbsp;a&nbsp;string&nbsp;of&nbsp;<a href="http://www.rfc-editor.org/rfc/rfc1459.txt">RFC1459</a>-style&nbsp;arguments&nbsp;split&nbsp;into&nbsp;a&nbsp;list,&nbsp;where&nbsp;":"&nbsp;may<br>
be&nbsp;used&nbsp;for&nbsp;multi-word&nbsp;arguments&nbsp;that&nbsp;last&nbsp;until&nbsp;the&nbsp;end&nbsp;of&nbsp;a&nbsp;line.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-parseTS6Args"><strong>parseTS6Args</strong></a>(self, args)</dt><dd><tt>Similar&nbsp;to&nbsp;<a href="#InspIRCdProtocol-parseArgs">parseArgs</a>(),&nbsp;but&nbsp;stripping&nbsp;leading&nbsp;colons&nbsp;from&nbsp;the&nbsp;first&nbsp;argument<br>
of&nbsp;a&nbsp;line&nbsp;(usually&nbsp;the&nbsp;sender&nbsp;field).</tt></dd></dl>
@ -372,10 +383,15 @@ of&nbsp;a&nbsp;line&nbsp;(usually&nbsp;the&nbsp;sender&nbsp;field).</tt></dd></d
<dl><dt><a name="InspIRCdProtocol-quitClient"><strong>quitClient</strong></a>(self, numeric, reason)</dt><dd><tt>Quits&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-removeClient"><strong>removeClient</strong></a>(self, numeric)</dt><dd><tt>Internal&nbsp;function&nbsp;to&nbsp;remove&nbsp;a&nbsp;client&nbsp;from&nbsp;our&nbsp;internal&nbsp;state.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-topicClient"><strong>topicClient</strong></a>(self, numeric, target, text)</dt><dd><tt>Sends&nbsp;a&nbsp;TOPIC&nbsp;change&nbsp;from&nbsp;a&nbsp;PyLink&nbsp;client.</tt></dd></dl>
<hr>
Methods inherited from <a href="classes.html#Protocol">classes.Protocol</a>:<br>
<dl><dt><a name="InspIRCdProtocol-parseArgs"><strong>parseArgs</strong></a>(self, args)</dt><dd><tt>Parses&nbsp;a&nbsp;string&nbsp;of&nbsp;<a href="http://www.rfc-editor.org/rfc/rfc1459.txt">RFC1459</a>-style&nbsp;arguments&nbsp;split&nbsp;into&nbsp;a&nbsp;list,&nbsp;where&nbsp;":"&nbsp;may<br>
be&nbsp;used&nbsp;for&nbsp;multi-word&nbsp;arguments&nbsp;that&nbsp;last&nbsp;until&nbsp;the&nbsp;end&nbsp;of&nbsp;a&nbsp;line.</tt></dd></dl>
<dl><dt><a name="InspIRCdProtocol-removeClient"><strong>removeClient</strong></a>(self, numeric)</dt><dd><tt>Internal&nbsp;function&nbsp;to&nbsp;remove&nbsp;a&nbsp;client&nbsp;from&nbsp;our&nbsp;internal&nbsp;state.</tt></dd></dl>
<hr>
Data descriptors inherited from <a href="classes.html#Protocol">classes.Protocol</a>:<br>
<dl><dt><strong>__dict__</strong></dt>
@ -391,7 +407,6 @@ Data descriptors inherited from <a href="classes.html#Protocol">classes.Protocol
<font color="#ffffff" face="helvetica, arial"><big><strong>Data</strong></big></font></td></tr>
<tr><td bgcolor="#55aa55"><tt>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</tt></td><td>&nbsp;</td>
<td width="100%"><strong>conf</strong> = {'bot': {'loglevel': 'CRITICAL', 'nick': 'PyLink', 'realname': 'PyLink Service Client', 'serverdesc': 'PyLink unit tests', 'user': 'pylink'}, 'servers': defaultdict(&lt;function &lt;lambda&gt; at 0x7f0dbb516c80&gt;, {})}<br>
<strong>curdir</strong> = 'protocols'<br>
<td width="100%"><strong>curdir</strong> = 'protocols'<br>
<strong>log</strong> = &lt;logging.RootLogger object&gt;</td></tr></table>
</body></html>

View File

@ -1,27 +1,93 @@
This page is still incomplete. For now, see [inspircd.html](inspircd.html) for an auto-generated specification of the InspIRCd protocol module. Any camelCase `ABCServer/Client` functions are outgoing commands, and include the following:
# PyLink Protocol Module Specification
- `awayClient`
- `inviteClient`
- `joinClient`
- `kickClient`
- `kickServer`
- `killClient`
- `killServer`
- `knockClient`
- `messageClient`
- `modeClient`
- `modeServer`
- `nickClient`
- `noticeClient`
- `numericServer`
- `partClient`
- `pingServer`
- `quitClient`
- `removeClient`
- `sjoinServer`
- `spawnClient`
- `spawnServer`
- `squitServer`
- `topicClient`
- `topicServer`
- `updateClient`
In PyLink, each protocol module is a single file consisting of a protocol class, and a global `Class` attribute that is set equal to it (e.g. `Class = InspIRCdProtocol`). These classes should be based off of either [`classes.Protocol`](https://github.com/GLolol/PyLink/blob/0.4.0-dev/classes.py#L404), a boilerplate class that only defines a few variables, or [`ts6_common.TS6BaseProtocol`](https://github.com/GLolol/PyLink/blob/0.4.0-dev/protocols/ts6_common.py#L10), which includes elements of the TS6 protocol that are shared by both the InspIRCd and TS6 protocols. IRC objects initialize protocol modules by creating an instance of its class, and passing it the IRC object itself.
See also: [inspircd.html](inspircd.html) for an auto-generated specification of the InspIRCd protocol module.
## Tasks
Protocol modules have some *very* ***important*** jobs. If any of these aren't done correctly, you will be left with a very **broken, desynced** services server:
1) Handle incoming commands from the uplink IRCd.
2) Return [hook data](hooks-reference.md) for relevant commands, so that plugins can receive data from IRC.
3) Make sure channel/user states are kept correctly. Joins, quits, parts, kicks, mode changes, nick changes, etc. **must** be handled accurately.
4) Respond to both pings *and* pongs - the `irc.lastping` attribute **must** be set to the current time whenever a `PONG` is received from the uplink, so PyLink's internals don't [lag out the uplink thinking it isn't responding to our `PING`s](https://github.com/GLolol/PyLink/blob/0.4.0-dev/classes.py#L202-L204).
5) Implement a series of camelCase `commandServer/Client` functions - plugins use these for sending outgoing commands. See the `Outbound commands` section below for a list of which ones are needed.
## Core functions
The following functions *must* be implemented by any protocol module within its main class, since they are used by the IRC internals.
- **`connect`**`(self)` - Initializes a connection to a server.
- **`handle_events`**`(self, line)` - Handles inbound data (lines of text) from the uplink IRC server. Normally, this will pass commands to other command handlers within the protocol module, dropping commands that are unrecognized (wildcard handling), but it's really up to you how to structure your modules. You will want to be able to parse command arguments properly into a list: many protocols send RFC1459-style commands that can be parsed using the [`self.parseArgs(line)` function within the `Protocol` class](https://github.com/GLolol/PyLink/blob/c77d170765d20b0ac55b945fba4a6257fb15cf43/classes.py#L411).
- All of the outbound commands mentioned in the next section (minus raw numerics) should have their incoming version handled and [hook data](hooks-reference.md) returned.
### Outgoing command functions
- **`spawnClient`**`(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None)` - Spawns a client on the given IRC connection. Note that no nick collision / valid nickname checks are done here, and it is up to plugins to make sure they don't introduce anything invalid.
- `modes` is a set of `(mode char, mode arg)` tuples in the form of [`utils.parseModes()` output](using-utils.md#parseModes).
- `ident` and `host` default to "null", while `realhost` defaults to the same things as `host` if not defined.
- `realname` defaults to the real name specified in the PyLink config, if not given. `ts` defaults to the current time and `opertype` (the oper type name, if applicable) defaults to the simple text of `IRC Operator`.
- **`joinClient`**`(self, client, channel)` - Joins the client UID given to a channel.
- **`awayClient`**`(self, source, text)` - Sends an AWAY message from a PyLink client. `text` can be an empty string to unset AWAY status.
- **`inviteClient`**`(self, source, target, channel)` - Sends an INVITE from a PyLink client.
- **`kickClient`**`(self, source, channel, target, reason=None)` - Sends a kick from a PyLink client.
- **`kickServer`**`(self, source, channel, target, reason=None)` - Sends a kick from a PyLink server.
- **`killClient`**`(self, source, target, reason)` - Sends a kill from a PyLink client.
- **`killServer`**`(self, source, target, reason)` - Sends a kill from a PyLink server.
- **`knockClient`**`(self, source, target, text)` - Sends a KNOCK from a PyLink client.
- **`messageClient`**`(self, source, target, text)` - Sends a PRIVMSG from a PyLink client.
- **`modeClient`**`(self, source, target, modes, ts=None)` - Sends modes from a PyLink client. `modes` takes a set of `([+/-]mode char, mode arg)` tuples.
- **`modeServer`**`(self, source, target, modes, ts=None)` - Sends modes from a PyLink server.
- **`nickClient`**`(self, source, newnick)` - Changes the nick of a PyLink client.
- **`noticeClient`**`(self, source, target, text)` - Sends a NOTICE from a PyLink client.
- **`numericServer`**`(self, source, numeric, target, text)` - Sends a raw numeric `numeric` with `text` from the `source` server to `target`.
- **`partClient`**`(self, client, channel, reason=None)` - Sends a part from a PyLink client.
- **`pingServer`**`(self, source=None, target=None)` - Sends a PING to a target server. Periodic PINGs are sent to our uplink automatically by the [`Irc()`
internals](https://github.com/GLolol/PyLink/blob/0.4.0-dev/classes.py#L267-L272); plugins shouldn't have to use this.
- **`quitClient`**`(self, source, reason)` - Quits a PyLink client.
- **`sjoinServer`**`(self, server, channel, users, ts=None)` - Sends an SJOIN for a group of users to a channel. The sender should always be a Server ID (SID). TS is
optional, and defaults to the one we've stored in the channel state if not given. `users` is a list of `(prefix mode, UID)` pairs. Example uses:
- `sjoinServer('100', '#test', [('', '100AAABBC'), ('qo', 100AAABBB'), ('h', '100AAADDD')])`
- `sjoinServer(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)])`
- **`spawnServer`**`(self, name, sid=None, uplink=None, desc=None)` - Spawns a server off another PyLink server. `desc` (server description) defaults to the one in the config. `uplink` defaults to the main PyLink server, and `sid` (the server ID) is automatically generated if not given.
- **`squitServer`**`(self, source, target, text='No reason given')` - SQUITs a PyLink server.
- **`topicClient`**`(self, source, target, text)` - Sends a topic change from a PyLink client.
- **`topicServer`**`(self, source, target, text)` - Sends a topic change from a PyLink server. This is usually used on burst.
- **`updateClient`**`(self, source, field, text)` - Updates the ident, host, or realname of a PyLink client. `field` should be either "IDENT", "HOST", "GECOS", or
"REALNAME". If changing the field given on the IRCd isn't supported, `NotImplementedError` should be raised.
## Variables to note
A protocol module should also set the following variables in their protocol class:
- `self.casemapping`: set this to `rfc1459` (default) or `ascii` to determine which case mapping should be used.
- `self.hook_map`: map a list of non-standard command names sent by the IRCd to those more commonly used: examples in the [TS6](https://github.com/GLolol/PyLink/blob/0.4.0-dev/protocols/ts6.py#L19) and [InspIRCd](https://github.com/GLolol/PyLink/blob/0.4.0-dev/protocols/inspircd.py#L24) modules.

View File

@ -0,0 +1 @@
This page is under construction: see [utils.html ](utils.html) for autogenerated documentation for the utils module.

233
docs/technical/utils.html Normal file
View File

@ -0,0 +1,233 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html><head><title>Python: module utils</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head><body bgcolor="#f0f0f8">
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="heading">
<tr bgcolor="#7799ee">
<td valign=bottom>&nbsp;<br>
<font color="#ffffff" face="helvetica, arial">&nbsp;<br><big><big><strong>utils</strong></big></big></font></td
><td align=right valign=bottom
><font color="#ffffff" face="helvetica, arial"><a href=".">index</a><br><a href="file:/home/gl/pylink/utils.py">/home/gl/pylink/utils.py</a></font></td></tr></table>
<p></p>
<p>
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#aa55cc">
<td colspan=3 valign=bottom>&nbsp;<br>
<font color="#ffffff" face="helvetica, arial"><big><strong>Modules</strong></big></font></td></tr>
<tr><td bgcolor="#aa55cc"><tt>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</tt></td><td>&nbsp;</td>
<td width="100%"><table width="100%" summary="list"><tr><td width="25%" valign=top><a href="imp.html">imp</a><br>
<a href="inspect.html">inspect</a><br>
</td><td width="25%" valign=top><a href="re.html">re</a><br>
<a href="string.html">string</a><br>
</td><td width="25%" valign=top><a href="world.html">world</a><br>
</td><td width="25%" valign=top></td></tr></table></td></tr></table><p>
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#ee77aa">
<td colspan=3 valign=bottom>&nbsp;<br>
<font color="#ffffff" face="helvetica, arial"><big><strong>Classes</strong></big></font></td></tr>
<tr><td bgcolor="#ee77aa"><tt>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</tt></td><td>&nbsp;</td>
<td width="100%"><dl>
<dt><font face="helvetica, arial"><a href="builtins.html#Exception">builtins.Exception</a>(<a href="builtins.html#BaseException">builtins.BaseException</a>)
</font></dt><dd>
<dl>
<dt><font face="helvetica, arial"><a href="utils.html#NotAuthenticatedError">NotAuthenticatedError</a>
</font></dt></dl>
</dd>
<dt><font face="helvetica, arial"><a href="builtins.html#object">builtins.object</a>
</font></dt><dd>
<dl>
<dt><font face="helvetica, arial"><a href="utils.html#TS6SIDGenerator">TS6SIDGenerator</a>
</font></dt><dt><font face="helvetica, arial"><a href="utils.html#TS6UIDGenerator">TS6UIDGenerator</a>
</font></dt></dl>
</dd>
</dl>
<p>
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#ffc8d8">
<td colspan=3 valign=bottom>&nbsp;<br>
<font color="#000000" face="helvetica, arial"><a name="NotAuthenticatedError">class <strong>NotAuthenticatedError</strong></a>(<a href="builtins.html#Exception">builtins.Exception</a>)</font></td></tr>
<tr bgcolor="#ffc8d8"><td rowspan=2><tt>&nbsp;&nbsp;&nbsp;</tt></td>
<td colspan=2><tt>#&nbsp;This&nbsp;is&nbsp;separate&nbsp;from&nbsp;classes.py&nbsp;to&nbsp;prevent&nbsp;import&nbsp;loops.<br>&nbsp;</tt></td></tr>
<tr><td>&nbsp;</td>
<td width="100%"><dl><dt>Method resolution order:</dt>
<dd><a href="utils.html#NotAuthenticatedError">NotAuthenticatedError</a></dd>
<dd><a href="builtins.html#Exception">builtins.Exception</a></dd>
<dd><a href="builtins.html#BaseException">builtins.BaseException</a></dd>
<dd><a href="builtins.html#object">builtins.object</a></dd>
</dl>
<hr>
Data descriptors defined here:<br>
<dl><dt><strong>__weakref__</strong></dt>
<dd><tt>list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object&nbsp;(if&nbsp;defined)</tt></dd>
</dl>
<hr>
Methods inherited from <a href="builtins.html#Exception">builtins.Exception</a>:<br>
<dl><dt><a name="NotAuthenticatedError-__init__"><strong>__init__</strong></a>(self, /, *args, **kwargs)</dt><dd><tt>Initialize&nbsp;self.&nbsp;&nbsp;See&nbsp;help(type(self))&nbsp;for&nbsp;accurate&nbsp;signature.</tt></dd></dl>
<dl><dt><a name="NotAuthenticatedError-__new__"><strong>__new__</strong></a>(*args, **kwargs)<font color="#909090"><font face="helvetica, arial"> from <a href="builtins.html#type">builtins.type</a></font></font></dt><dd><tt>Create&nbsp;and&nbsp;return&nbsp;a&nbsp;new&nbsp;<a href="builtins.html#object">object</a>.&nbsp;&nbsp;See&nbsp;help(type)&nbsp;for&nbsp;accurate&nbsp;signature.</tt></dd></dl>
<hr>
Methods inherited from <a href="builtins.html#BaseException">builtins.BaseException</a>:<br>
<dl><dt><a name="NotAuthenticatedError-__delattr__"><strong>__delattr__</strong></a>(self, name, /)</dt><dd><tt>Implement&nbsp;delattr(self,&nbsp;name).</tt></dd></dl>
<dl><dt><a name="NotAuthenticatedError-__getattribute__"><strong>__getattribute__</strong></a>(self, name, /)</dt><dd><tt>Return&nbsp;getattr(self,&nbsp;name).</tt></dd></dl>
<dl><dt><a name="NotAuthenticatedError-__reduce__"><strong>__reduce__</strong></a>(...)</dt></dl>
<dl><dt><a name="NotAuthenticatedError-__repr__"><strong>__repr__</strong></a>(self, /)</dt><dd><tt>Return&nbsp;repr(self).</tt></dd></dl>
<dl><dt><a name="NotAuthenticatedError-__setattr__"><strong>__setattr__</strong></a>(self, name, value, /)</dt><dd><tt>Implement&nbsp;setattr(self,&nbsp;name,&nbsp;value).</tt></dd></dl>
<dl><dt><a name="NotAuthenticatedError-__setstate__"><strong>__setstate__</strong></a>(...)</dt></dl>
<dl><dt><a name="NotAuthenticatedError-__str__"><strong>__str__</strong></a>(self, /)</dt><dd><tt>Return&nbsp;str(self).</tt></dd></dl>
<dl><dt><a name="NotAuthenticatedError-with_traceback"><strong>with_traceback</strong></a>(...)</dt><dd><tt><a href="builtins.html#Exception">Exception</a>.<a href="#NotAuthenticatedError-with_traceback">with_traceback</a>(tb)&nbsp;--<br>
set&nbsp;self.<strong>__traceback__</strong>&nbsp;to&nbsp;tb&nbsp;and&nbsp;return&nbsp;self.</tt></dd></dl>
<hr>
Data descriptors inherited from <a href="builtins.html#BaseException">builtins.BaseException</a>:<br>
<dl><dt><strong>__cause__</strong></dt>
<dd><tt>exception&nbsp;cause</tt></dd>
</dl>
<dl><dt><strong>__context__</strong></dt>
<dd><tt>exception&nbsp;context</tt></dd>
</dl>
<dl><dt><strong>__dict__</strong></dt>
</dl>
<dl><dt><strong>__suppress_context__</strong></dt>
</dl>
<dl><dt><strong>__traceback__</strong></dt>
</dl>
<dl><dt><strong>args</strong></dt>
</dl>
</td></tr></table> <p>
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#ffc8d8">
<td colspan=3 valign=bottom>&nbsp;<br>
<font color="#000000" face="helvetica, arial"><a name="TS6SIDGenerator">class <strong>TS6SIDGenerator</strong></a>(<a href="builtins.html#object">builtins.object</a>)</font></td></tr>
<tr bgcolor="#ffc8d8"><td rowspan=2><tt>&nbsp;&nbsp;&nbsp;</tt></td>
<td colspan=2><tt>TS6&nbsp;SID&nbsp;Generator.&nbsp;&lt;query&gt;&nbsp;is&nbsp;a&nbsp;3&nbsp;character&nbsp;string&nbsp;with&nbsp;any&nbsp;combination&nbsp;of<br>
uppercase&nbsp;letters,&nbsp;digits,&nbsp;and&nbsp;#'s.&nbsp;it&nbsp;must&nbsp;contain&nbsp;at&nbsp;least&nbsp;one&nbsp;#,<br>
which&nbsp;are&nbsp;used&nbsp;by&nbsp;the&nbsp;generator&nbsp;as&nbsp;a&nbsp;wildcard.&nbsp;On&nbsp;every&nbsp;<a href="#TS6SIDGenerator-next_sid">next_sid</a>()&nbsp;call,<br>
the&nbsp;first&nbsp;available&nbsp;wildcard&nbsp;character&nbsp;(from&nbsp;the&nbsp;right)&nbsp;will&nbsp;be<br>
incremented&nbsp;to&nbsp;generate&nbsp;the&nbsp;next&nbsp;SID.<br>
&nbsp;<br>
When&nbsp;there&nbsp;are&nbsp;no&nbsp;more&nbsp;available&nbsp;SIDs&nbsp;left&nbsp;(SIDs&nbsp;are&nbsp;not&nbsp;reused,&nbsp;only<br>
incremented),&nbsp;RuntimeError&nbsp;is&nbsp;raised.<br>
&nbsp;<br>
Example&nbsp;queries:<br>
&nbsp;&nbsp;&nbsp;&nbsp;"1#A"&nbsp;would&nbsp;give:&nbsp;10A,&nbsp;11A,&nbsp;12A&nbsp;...&nbsp;19A,&nbsp;1AA,&nbsp;1BA&nbsp;...&nbsp;1ZA&nbsp;(36&nbsp;total&nbsp;results)<br>
&nbsp;&nbsp;&nbsp;&nbsp;"#BQ"&nbsp;would&nbsp;give:&nbsp;0BQ,&nbsp;1BQ,&nbsp;2BQ&nbsp;...&nbsp;9BQ&nbsp;(10&nbsp;total&nbsp;results)<br>
&nbsp;&nbsp;&nbsp;&nbsp;"6##"&nbsp;would&nbsp;give:&nbsp;600,&nbsp;601,&nbsp;602,&nbsp;...&nbsp;60Y,&nbsp;60Z,&nbsp;610,&nbsp;611,&nbsp;...&nbsp;6ZZ&nbsp;(1296&nbsp;total&nbsp;results)<br>&nbsp;</tt></td></tr>
<tr><td>&nbsp;</td>
<td width="100%">Methods defined here:<br>
<dl><dt><a name="TS6SIDGenerator-__init__"><strong>__init__</strong></a>(self, irc)</dt></dl>
<dl><dt><a name="TS6SIDGenerator-increment"><strong>increment</strong></a>(self, pos=2)</dt></dl>
<dl><dt><a name="TS6SIDGenerator-next_sid"><strong>next_sid</strong></a>(self)</dt></dl>
<hr>
Data descriptors defined here:<br>
<dl><dt><strong>__dict__</strong></dt>
<dd><tt>dictionary&nbsp;for&nbsp;instance&nbsp;variables&nbsp;(if&nbsp;defined)</tt></dd>
</dl>
<dl><dt><strong>__weakref__</strong></dt>
<dd><tt>list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object&nbsp;(if&nbsp;defined)</tt></dd>
</dl>
</td></tr></table> <p>
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#ffc8d8">
<td colspan=3 valign=bottom>&nbsp;<br>
<font color="#000000" face="helvetica, arial"><a name="TS6UIDGenerator">class <strong>TS6UIDGenerator</strong></a>(<a href="builtins.html#object">builtins.object</a>)</font></td></tr>
<tr bgcolor="#ffc8d8"><td rowspan=2><tt>&nbsp;&nbsp;&nbsp;</tt></td>
<td colspan=2><tt>TS6&nbsp;UID&nbsp;Generator&nbsp;module,&nbsp;adapted&nbsp;from&nbsp;InspIRCd&nbsp;source<br>
https://github.com/inspircd/inspircd/blob/f449c6b296ab/src/server.cpp#L85-L156<br>&nbsp;</tt></td></tr>
<tr><td>&nbsp;</td>
<td width="100%">Methods defined here:<br>
<dl><dt><a name="TS6UIDGenerator-__init__"><strong>__init__</strong></a>(self, sid)</dt></dl>
<dl><dt><a name="TS6UIDGenerator-increment"><strong>increment</strong></a>(self, pos=5)</dt></dl>
<dl><dt><a name="TS6UIDGenerator-next_uid"><strong>next_uid</strong></a>(self)</dt></dl>
<hr>
Data descriptors defined here:<br>
<dl><dt><strong>__dict__</strong></dt>
<dd><tt>dictionary&nbsp;for&nbsp;instance&nbsp;variables&nbsp;(if&nbsp;defined)</tt></dd>
</dl>
<dl><dt><strong>__weakref__</strong></dt>
<dd><tt>list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object&nbsp;(if&nbsp;defined)</tt></dd>
</dl>
</td></tr></table></td></tr></table><p>
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#eeaa77">
<td colspan=3 valign=bottom>&nbsp;<br>
<font color="#ffffff" face="helvetica, arial"><big><strong>Functions</strong></big></font></td></tr>
<tr><td bgcolor="#eeaa77"><tt>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</tt></td><td>&nbsp;</td>
<td width="100%"><dl><dt><a name="-add_cmd"><strong>add_cmd</strong></a>(func, name=None)</dt></dl>
<dl><dt><a name="-add_hook"><strong>add_hook</strong></a>(func, command)</dt><dd><tt>Add&nbsp;a&nbsp;hook&nbsp;&lt;func&gt;&nbsp;for&nbsp;command&nbsp;&lt;command&gt;.</tt></dd></dl>
<dl><dt><a name="-applyModes"><strong>applyModes</strong></a>(irc, target, changedmodes)</dt><dd><tt>Takes&nbsp;a&nbsp;list&nbsp;of&nbsp;parsed&nbsp;IRC&nbsp;modes,&nbsp;and&nbsp;applies&nbsp;them&nbsp;on&nbsp;the&nbsp;given&nbsp;target.<br>
&nbsp;<br>
The&nbsp;target&nbsp;can&nbsp;be&nbsp;either&nbsp;a&nbsp;channel&nbsp;or&nbsp;a&nbsp;user;&nbsp;this&nbsp;is&nbsp;handled&nbsp;automatically.</tt></dd></dl>
<dl><dt><a name="-checkAuthenticated"><strong>checkAuthenticated</strong></a>(irc, uid, allowAuthed=True, allowOper=True)</dt><dd><tt>Checks&nbsp;whetherthe&nbsp;given&nbsp;user&nbsp;has&nbsp;operator&nbsp;status&nbsp;on&nbsp;PyLink,&nbsp;raising<br>
<a href="#NotAuthenticatedError">NotAuthenticatedError</a>&nbsp;and&nbsp;logging&nbsp;the&nbsp;access&nbsp;denial&nbsp;if&nbsp;not.</tt></dd></dl>
<dl><dt><a name="-clientToServer"><strong>clientToServer</strong></a>(irc, numeric)</dt><dd><tt>Finds&nbsp;the&nbsp;SID&nbsp;of&nbsp;the&nbsp;server&nbsp;a&nbsp;user&nbsp;is&nbsp;on.</tt></dd></dl>
<dl><dt><a name="-getHostmask"><strong>getHostmask</strong></a>(irc, user)</dt><dd><tt>Gets&nbsp;the&nbsp;hostmask&nbsp;of&nbsp;the&nbsp;given&nbsp;user,&nbsp;if&nbsp;present.</tt></dd></dl>
<dl><dt><a name="-getProtoModule"><strong>getProtoModule</strong></a>(protoname)</dt><dd><tt>Imports&nbsp;and&nbsp;returns&nbsp;the&nbsp;protocol&nbsp;module&nbsp;requested.</tt></dd></dl>
<dl><dt><a name="-isChannel"><strong>isChannel</strong></a>(s)</dt><dd><tt>Checks&nbsp;whether&nbsp;the&nbsp;string&nbsp;given&nbsp;is&nbsp;a&nbsp;valid&nbsp;channel&nbsp;name.</tt></dd></dl>
<dl><dt><a name="-isHostmask"><strong>isHostmask</strong></a>(text)</dt><dd><tt>Returns&nbsp;whether&nbsp;the&nbsp;given&nbsp;text&nbsp;is&nbsp;a&nbsp;valid&nbsp;hostmask.</tt></dd></dl>
<dl><dt><a name="-isInternalClient"><strong>isInternalClient</strong></a>(irc, numeric)</dt><dd><tt>Checks&nbsp;whether&nbsp;the&nbsp;given&nbsp;numeric&nbsp;is&nbsp;a&nbsp;PyLink&nbsp;Client,<br>
returning&nbsp;the&nbsp;SID&nbsp;of&nbsp;the&nbsp;server&nbsp;it's&nbsp;on&nbsp;if&nbsp;so.</tt></dd></dl>
<dl><dt><a name="-isInternalServer"><strong>isInternalServer</strong></a>(irc, sid)</dt><dd><tt>Returns&nbsp;whether&nbsp;the&nbsp;given&nbsp;SID&nbsp;is&nbsp;an&nbsp;internal&nbsp;PyLink&nbsp;server.</tt></dd></dl>
<dl><dt><a name="-isManipulatableClient"><strong>isManipulatableClient</strong></a>(irc, uid)</dt><dd><tt>Returns&nbsp;whether&nbsp;the&nbsp;given&nbsp;user&nbsp;is&nbsp;marked&nbsp;as&nbsp;an&nbsp;internal,&nbsp;manipulatable<br>
client.&nbsp;Usually,&nbsp;automatically&nbsp;spawned&nbsp;services&nbsp;clients&nbsp;should&nbsp;have&nbsp;this<br>
set&nbsp;True&nbsp;to&nbsp;prevent&nbsp;interactions&nbsp;with&nbsp;opers&nbsp;(like&nbsp;mode&nbsp;changes)&nbsp;from<br>
causing&nbsp;desyncs.</tt></dd></dl>
<dl><dt><a name="-isNick"><strong>isNick</strong></a>(s, nicklen=None)</dt><dd><tt>Checks&nbsp;whether&nbsp;the&nbsp;string&nbsp;given&nbsp;is&nbsp;a&nbsp;valid&nbsp;nick.</tt></dd></dl>
<dl><dt><a name="-isOper"><strong>isOper</strong></a>(irc, uid, allowAuthed=True, allowOper=True)</dt><dd><tt>Returns&nbsp;whether&nbsp;the&nbsp;given&nbsp;user&nbsp;has&nbsp;operator&nbsp;status&nbsp;on&nbsp;PyLink.&nbsp;This&nbsp;can&nbsp;be&nbsp;achieved<br>
by&nbsp;either&nbsp;identifying&nbsp;to&nbsp;PyLink&nbsp;as&nbsp;admin&nbsp;(if&nbsp;allowAuthed&nbsp;is&nbsp;True),<br>
or&nbsp;having&nbsp;user&nbsp;mode&nbsp;+o&nbsp;set&nbsp;(if&nbsp;allowOper&nbsp;is&nbsp;True).&nbsp;At&nbsp;least&nbsp;one&nbsp;of<br>
allowAuthed&nbsp;or&nbsp;allowOper&nbsp;must&nbsp;be&nbsp;True&nbsp;for&nbsp;this&nbsp;to&nbsp;give&nbsp;any&nbsp;meaningful<br>
results.</tt></dd></dl>
<dl><dt><a name="-isServerName"><strong>isServerName</strong></a>(s)</dt><dd><tt>Checks&nbsp;whether&nbsp;the&nbsp;string&nbsp;given&nbsp;is&nbsp;a&nbsp;server&nbsp;name.</tt></dd></dl>
<dl><dt><a name="-joinModes"><strong>joinModes</strong></a>(modes)</dt><dd><tt>Takes&nbsp;a&nbsp;list&nbsp;of&nbsp;(mode,&nbsp;arg)&nbsp;tuples&nbsp;in&nbsp;<a href="#-parseModes">parseModes</a>()&nbsp;format,&nbsp;and<br>
joins&nbsp;them&nbsp;into&nbsp;a&nbsp;string.<br>
&nbsp;<br>
See&nbsp;testJoinModes&nbsp;in&nbsp;tests/test_utils.py&nbsp;for&nbsp;some&nbsp;examples.</tt></dd></dl>
<dl><dt><a name="-loadModuleFromFolder"><strong>loadModuleFromFolder</strong></a>(name, folder)</dt><dd><tt>Attempts&nbsp;an&nbsp;import&nbsp;of&nbsp;name&nbsp;from&nbsp;a&nbsp;specific&nbsp;folder,&nbsp;returning&nbsp;the&nbsp;resulting&nbsp;module.</tt></dd></dl>
<dl><dt><a name="-nickToUid"><strong>nickToUid</strong></a>(irc, nick)</dt><dd><tt>Returns&nbsp;the&nbsp;UID&nbsp;of&nbsp;a&nbsp;user&nbsp;named&nbsp;nick,&nbsp;if&nbsp;present.</tt></dd></dl>
<dl><dt><a name="-parseModes"><strong>parseModes</strong></a>(irc, target, args)</dt><dd><tt>Parses&nbsp;a&nbsp;modestring&nbsp;list&nbsp;into&nbsp;a&nbsp;list&nbsp;of&nbsp;(mode,&nbsp;argument)&nbsp;tuples.<br>
['+mitl-o',&nbsp;'3',&nbsp;'person']&nbsp;=&gt;&nbsp;[('+m',&nbsp;None),&nbsp;('+i',&nbsp;None),&nbsp;('+t',&nbsp;None),&nbsp;('+l',&nbsp;'3'),&nbsp;('-o',&nbsp;'person')]</tt></dd></dl>
<dl><dt><a name="-reverseModes"><strong>reverseModes</strong></a>(irc, target, modes, oldobj=None)</dt><dd><tt>Reverses/Inverts&nbsp;the&nbsp;mode&nbsp;string&nbsp;or&nbsp;mode&nbsp;list&nbsp;given.<br>
&nbsp;<br>
Optionally,&nbsp;an&nbsp;oldobj&nbsp;argument&nbsp;can&nbsp;be&nbsp;given&nbsp;to&nbsp;look&nbsp;at&nbsp;an&nbsp;earlier&nbsp;state&nbsp;of<br>
a&nbsp;channel/user&nbsp;<a href="builtins.html#object">object</a>,&nbsp;e.g.&nbsp;for&nbsp;checking&nbsp;the&nbsp;op&nbsp;status&nbsp;of&nbsp;a&nbsp;mode&nbsp;setter<br>
before&nbsp;their&nbsp;modes&nbsp;are&nbsp;processed&nbsp;and&nbsp;added&nbsp;to&nbsp;the&nbsp;channel&nbsp;state.<br>
&nbsp;<br>
This&nbsp;function&nbsp;allows&nbsp;both&nbsp;mode&nbsp;strings&nbsp;or&nbsp;mode&nbsp;lists.&nbsp;Example&nbsp;uses:<br>
&nbsp;&nbsp;&nbsp;&nbsp;"+mi-lk&nbsp;test&nbsp;=&gt;&nbsp;"-mi+lk&nbsp;test"<br>
&nbsp;&nbsp;&nbsp;&nbsp;"mi-k&nbsp;test&nbsp;=&gt;&nbsp;"-mi+k&nbsp;test"<br>
&nbsp;&nbsp;&nbsp;&nbsp;[('+m',&nbsp;None),&nbsp;('+r',&nbsp;None),&nbsp;('+l',&nbsp;'3'),&nbsp;('-o',&nbsp;'person')<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;=&gt;&nbsp;{('-m',&nbsp;None),&nbsp;('-r',&nbsp;None),&nbsp;('-l',&nbsp;None),&nbsp;('+o',&nbsp;'person')})<br>
&nbsp;&nbsp;&nbsp;&nbsp;{('s',&nbsp;None),&nbsp;('+o',&nbsp;'whoever')&nbsp;=&gt;&nbsp;{('-s',&nbsp;None),&nbsp;('-o',&nbsp;'whoever')})</tt></dd></dl>
<dl><dt><a name="-toLower"><strong>toLower</strong></a>(irc, text)</dt><dd><tt>Returns&nbsp;a&nbsp;lowercase&nbsp;representation&nbsp;of&nbsp;text&nbsp;based&nbsp;on&nbsp;the&nbsp;IRC&nbsp;<a href="builtins.html#object">object</a>'s<br>
casemapping&nbsp;(rfc1459&nbsp;or&nbsp;ascii).</tt></dd></dl>
</td></tr></table><p>
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#55aa55">
<td colspan=3 valign=bottom>&nbsp;<br>
<font color="#ffffff" face="helvetica, arial"><big><strong>Data</strong></big></font></td></tr>
<tr><td bgcolor="#55aa55"><tt>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</tt></td><td>&nbsp;</td>
<td width="100%"><strong>hostmaskRe</strong> = re.compile('^\\S+!\\S+@\\S+$')<br>
<strong>log</strong> = &lt;logging.RootLogger object&gt;</td></tr></table>
</body></html>

View File

@ -20,7 +20,7 @@ def spawnclient(irc, source, args):
try:
nick, ident, host = args[:3]
except ValueError:
irc.msg(source, "Error: Not enough arguments. Needs 3: nick, user, host.")
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 3: nick, user, host.")
return
irc.proto.spawnClient(nick, ident, host, manipulatable=True)
@ -33,15 +33,15 @@ def quit(irc, source, args):
try:
nick = args[0]
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 1-2: nick, reason (optional).")
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 1-2: nick, reason (optional).")
return
if irc.pseudoclient.uid == utils.nickToUid(irc, nick):
irc.msg(source, "Error: Cannot quit the main PyLink PseudoClient!")
irc.msg(irc.called_by, "Error: Cannot quit the main PyLink PseudoClient!")
return
u = utils.nickToUid(irc, nick)
quitmsg = ' '.join(args[1:]) or 'Client Quit'
if not utils.isManipulatableClient(irc, u):
irc.msg(source, "Error: Cannot force quit a protected PyLink services client.")
irc.msg(irc.called_by, "Error: Cannot force quit a protected PyLink services client.")
return
irc.proto.quitClient(u, quitmsg)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}])
@ -57,15 +57,15 @@ def joinclient(irc, source, args):
if not clist:
raise IndexError
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
return
u = utils.nickToUid(irc, nick)
if not utils.isManipulatableClient(irc, u):
irc.msg(source, "Error: Cannot force join a protected PyLink services client.")
irc.msg(irc.called_by, "Error: Cannot force join a protected PyLink services client.")
return
for channel in clist:
if not utils.isChannel(channel):
irc.msg(source, "Error: Invalid channel name %r." % channel)
irc.msg(irc.called_by, "Error: Invalid channel name %r." % channel)
return
irc.proto.joinClient(u, channel)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': channel, 'users': [u],
@ -83,16 +83,16 @@ def nick(irc, source, args):
nick = args[0]
newnick = args[1]
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 2: nick, newnick.")
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 2: nick, newnick.")
return
u = utils.nickToUid(irc, nick)
if newnick in ('0', u):
newnick = u
elif not utils.isNick(newnick):
irc.msg(source, 'Error: Invalid nickname %r.' % newnick)
irc.msg(irc.called_by, 'Error: Invalid nickname %r.' % newnick)
return
elif not utils.isManipulatableClient(irc, u):
irc.msg(source, "Error: Cannot force nick changes for a protected PyLink services client.")
irc.msg(irc.called_by, "Error: Cannot force nick changes for a protected PyLink services client.")
return
irc.proto.nickClient(u, newnick)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}])
@ -108,15 +108,15 @@ def part(irc, source, args):
clist = args[1].split(',')
reason = ' '.join(args[2:])
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.")
return
u = utils.nickToUid(irc, nick)
if not utils.isManipulatableClient(irc, u):
irc.msg(source, "Error: Cannot force part a protected PyLink services client.")
irc.msg(irc.called_by, "Error: Cannot force part a protected PyLink services client.")
return
for channel in clist:
if not utils.isChannel(channel):
irc.msg(source, "Error: Invalid channel name %r." % channel)
irc.msg(irc.called_by, "Error: Invalid channel name %r." % channel)
return
irc.proto.partClient(u, channel, reason)
irc.callHooks([u, 'PYLINK_BOTSPLUGIN_PART', {'channels': clist, 'text': reason, 'parse_as': 'PART'}])
@ -133,12 +133,12 @@ def kick(irc, source, args):
target = args[2]
reason = ' '.join(args[3:])
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 3-4: source nick, channel, target, reason (optional).")
irc.msg(irc.called_by, "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)
if not utils.isChannel(channel):
irc.msg(source, "Error: Invalid channel name %r." % channel)
irc.msg(irc.called_by, "Error: Invalid channel name %r." % channel)
return
if utils.isInternalServer(irc, u):
irc.proto.kickServer(u, channel, targetu, reason)
@ -155,20 +155,20 @@ def mode(irc, source, args):
try:
modesource, target, modes = args[0], args[1], args[2:]
except IndexError:
irc.msg(source, 'Error: Not enough arguments. Needs 3: source nick, target, modes to set.')
irc.msg(irc.called_by, 'Error: Not enough arguments. Needs 3: source nick, target, modes to set.')
return
target = utils.nickToUid(irc, target) or target
extclient = target in irc.users and not utils.isInternalClient(irc, target)
parsedmodes = utils.parseModes(irc, target, modes)
ischannel = target in irc.channels
if not (target in irc.users or ischannel):
irc.msg(source, "Error: Invalid channel or nick %r." % target)
irc.msg(irc.called_by, "Error: Invalid channel or nick %r." % target)
return
elif not parsedmodes:
irc.msg(source, "Error: No valid modes were given.")
irc.msg(irc.called_by, "Error: No valid modes were given.")
return
elif not (ischannel or utils.isManipulatableClient(irc, target)):
irc.msg(source, "Error: Can only set modes on channels or non-protected PyLink clients.")
irc.msg(irc.called_by, "Error: Can only set modes on channels or non-protected PyLink clients.")
return
if utils.isInternalServer(irc, modesource):
# Setting modes from a server.
@ -189,21 +189,21 @@ def msg(irc, source, args):
try:
msgsource, target, text = args[0], args[1], ' '.join(args[2:])
except IndexError:
irc.msg(source, 'Error: Not enough arguments. Needs 3: source nick, target, text.')
irc.msg(irc.called_by, 'Error: Not enough arguments. Needs 3: source nick, target, text.')
return
sourceuid = utils.nickToUid(irc, msgsource)
if not sourceuid:
irc.msg(source, 'Error: Unknown user %r.' % msgsource)
irc.msg(irc.called_by, 'Error: Unknown user %r.' % msgsource)
return
if not utils.isChannel(target):
real_target = utils.nickToUid(irc, target)
if real_target is None:
irc.msg(source, 'Error: Unknown user %r.' % target)
irc.msg(irc.called_by, 'Error: Unknown user %r.' % target)
return
else:
real_target = target
if not text:
irc.msg(source, 'Error: No text given.')
irc.msg(irc.called_by, 'Error: No text given.')
return
irc.proto.messageClient(sourceuid, real_target, text)
irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}])

View File

@ -2,12 +2,15 @@
import sys
import os
from time import ctime
import itertools
import gc
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils
from conf import conf
import conf
from log import log
import world
import classes
@utils.add_cmd
def status(irc, source, args):
@ -16,24 +19,27 @@ def status(irc, source, args):
Returns your current PyLink login status."""
identified = irc.users[source].identified
if identified:
irc.msg(source, 'You are identified as \x02%s\x02.' % identified)
irc.msg(irc.called_by, 'You are identified as \x02%s\x02.' % identified)
else:
irc.msg(source, 'You are not identified as anyone.')
irc.msg(source, 'Operator access: \x02%s\x02' % bool(utils.isOper(irc, source)))
irc.msg(irc.called_by, 'You are not identified as anyone.')
irc.msg(irc.called_by, 'Operator access: \x02%s\x02' % bool(utils.isOper(irc, source)))
@utils.add_cmd
def identify(irc, source, args):
"""<username> <password>
Logs in to PyLink using the configured administrator account."""
if utils.isChannel(irc.called_by):
irc.msg(irc.called_by, 'Error: This command must be sent in private. '
'(Would you really type a password inside a public channel?)')
try:
username, password = args[0], args[1]
except IndexError:
irc.msg(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']:
realuser = conf['login']['user']
if username.lower() == irc.conf['login']['user'].lower() and password == irc.conf['login']['password']:
realuser = irc.conf['login']['user']
irc.users[source].identified = realuser
irc.msg(source, 'Successfully logged in as %s.' % realuser)
log.info("(%s) Successful login to %r by %s.",
@ -48,14 +54,14 @@ def listcommands(irc, source, args):
"""takes no arguments.
Returns a list of available commands PyLink has to offer."""
cmds = list(world.bot_commands.keys())
cmds = list(world.commands.keys())
cmds.sort()
for idx, cmd in enumerate(cmds):
nfuncs = len(world.bot_commands[cmd])
nfuncs = len(world.commands[cmd])
if nfuncs > 1:
cmds[idx] = '%s(x%s)' % (cmd, nfuncs)
irc.msg(source, 'Available commands include: %s' % ', '.join(cmds))
irc.msg(source, 'To see help on a specific command, type \x02help <command>\x02.')
irc.msg(irc.called_by, 'Available commands include: %s' % ', '.join(cmds))
irc.msg(irc.called_by, 'To see help on a specific command, type \x02help <command>\x02.')
utils.add_cmd(listcommands, 'list')
@utils.add_cmd
@ -68,13 +74,13 @@ def help(irc, source, args):
except IndexError: # No argument given, just return 'list' output
listcommands(irc, source, args)
return
if command not in world.bot_commands:
if command not in world.commands:
irc.msg(source, 'Error: Unknown command %r.' % command)
return
else:
funcs = world.bot_commands[command]
funcs = world.commands[command]
if len(funcs) > 1:
irc.msg(source, 'The following \x02%s\x02 plugins bind to the \x02%s\x02 command: %s'
irc.msg(irc.called_by, '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__
@ -85,7 +91,7 @@ def help(irc, source, args):
# arguments the command takes.
lines[0] = '\x02%s %s\x02 (plugin: %r)' % (command, lines[0], mod)
for line in lines:
irc.msg(source, line.strip())
irc.msg(irc.called_by, line.strip())
else:
irc.msg(source, "Error: Command %r (from plugin %r) "
"doesn't offer any help." % (command, mod))
@ -99,14 +105,14 @@ def showuser(irc, source, args):
try:
target = args[0]
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 1: nick.")
irc.msg(irc.called_by, "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:
irc.msg(source, 'Error: Unknown user %r.' % target)
irc.msg(irc.called_by, 'Error: Unknown user %r.' % target)
return
f = lambda s: irc.msg(source, s)
@ -134,10 +140,10 @@ def showchan(irc, source, args):
try:
channel = utils.toLower(irc, args[0])
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 1: channel.")
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 1: channel.")
return
if channel not in irc.channels:
irc.msg(source, 'Error: Unknown channel %r.' % channel)
irc.msg(irc.called_by, 'Error: Unknown channel %r.' % channel)
return
f = lambda s: irc.msg(source, s)
@ -191,5 +197,154 @@ def version(irc, source, args):
"""takes no arguments.
Returns the version of the currently running PyLink instance."""
irc.msg(source, "PyLink version \x02%s\x02, released under the Mozilla Public License version 2.0." % world.version)
irc.msg(source, "The source of this program is available at \x02%s\x02." % world.source)
irc.msg(irc.called_by, "PyLink version \x02%s\x02, released under the Mozilla Public License version 2.0." % world.version)
irc.msg(irc.called_by, "The source of this program is available at \x02%s\x02." % world.source)
@utils.add_cmd
def echo(irc, source, args):
"""<text>
Echoes the text given."""
irc.msg(irc.called_by, ' '.join(args))
def load(irc, source, args):
"""<plugin name>.
Loads a plugin from the plugin folder."""
utils.checkAuthenticated(irc, source, allowOper=False)
try:
name = args[0]
except IndexError:
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 1: plugin name.")
return
if name in world.plugins:
irc.msg(irc.called_by, "Error: %r is already loaded." % name)
return
try:
world.plugins[name] = pl = utils.loadModuleFromFolder(name, world.plugins_folder)
except ImportError as e:
if str(e) == ('No module named %r' % name):
log.exception('Failed to load plugin %r: The plugin could not be found.', name)
else:
log.exception('Failed to load plugin %r: ImportError.', name)
raise
else:
if hasattr(pl, 'main'):
log.debug('Calling main() function of plugin %r', pl)
pl.main(irc)
irc.msg(irc.called_by, "Loaded plugin %r." % name)
utils.add_cmd(load)
def unload(irc, source, args):
"""<plugin name>.
Unloads a currently loaded plugin."""
utils.checkAuthenticated(irc, source, allowOper=False)
try:
name = args[0]
except IndexError:
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 1: plugin name.")
return
if name == 'commands':
irc.msg(irc.called_by, "Error: Cannot unload the commands plugin!")
return
elif name in world.plugins:
pl = world.plugins[name]
log.debug('sys.getrefcount of plugin %s is %s', pl, sys.getrefcount(pl))
# Remove any command functions set by the plugin.
for cmdname, cmdfuncs in world.commands.copy().items():
log.debug('cmdname=%s, cmdfuncs=%s', cmdname, cmdfuncs)
for cmdfunc in cmdfuncs:
log.debug('__module__ of cmdfunc %s is %s', cmdfunc, cmdfunc.__module__)
if cmdfunc.__module__ == name:
log.debug('Removing %s from world.commands[%s]', cmdfunc, cmdname)
world.commands[cmdname].remove(cmdfunc)
# If the cmdfunc list is empty, remove it.
if not cmdfuncs:
log.debug("Removing world.commands[%s] (it's empty now)", cmdname)
del world.commands[cmdname]
# Remove any command hooks set by the plugin.
for hookname, hookfuncs in world.hooks.copy().items():
for hookfunc in hookfuncs:
if hookfunc.__module__ == name:
world.hooks[hookname].remove(hookfunc)
# If the hookfuncs list is empty, remove it.
if not hookfuncs:
del world.hooks[hookname]
# Remove whois handlers too.
for f in world.whois_handlers:
if f.__module__ == name:
world.whois_handlers.remove(f)
# Call the die() function in the plugin, if present.
if hasattr(pl, 'die'):
try:
pl.die(irc)
except: # But don't allow it to crash the server.
log.exception('(%s) Error occurred in die() of plugin %s, skipping...', irc.name, pl)
# Delete it from memory (hopefully).
del world.plugins[name]
if name in sys.modules:
del sys.modules[name]
if name in globals():
del globals()[name]
# Garbage collect.
gc.collect()
irc.msg(irc.called_by, "Unloaded plugin %r." % name)
return True # We succeeded, make it clear (this status is used by reload() below)
else:
irc.msg(irc.called_by, "Unknown plugin %r." % name)
utils.add_cmd(unload)
@utils.add_cmd
def reload(irc, source, args):
"""<plugin name>.
Loads a plugin from the plugin folder."""
try:
name = args[0]
except IndexError:
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 1: plugin name.")
return
if unload(irc, source, args):
load(irc, source, args)
@utils.add_cmd
def rehash(irc, source, args):
"""takes no arguments.
Reloads the configuration file for PyLink, (dis)connecting added/removed networks.
Plugins must be manually reloaded."""
utils.checkAuthenticated(irc, source, allowOper=False)
old_conf = conf.conf.copy()
fname = conf.fname
try:
new_conf = conf.validateConf(conf.loadConf(fname))
except Exception as e: # Something went wrong, abort.
log.exception("Error REHASH'ing config: ")
irc.msg(irc.called_by, "Error loading configuration file: %s: %s", type(e).__name__, e)
return
conf.conf = new_conf
for network, ircobj in world.networkobjects.copy().items():
# Server was removed from the config file, disconnect them.
log.debug('(%s) rehash: checking if %r is in new conf still.', irc.name, network)
if network not in new_conf['servers']:
# Disable autoconnect first.
log.debug('(%s) rehash: removing connection to %r (removed from config).', irc.name, network)
ircobj.serverdata['autoconnect'] = -1
ircobj.aborted.set()
del world.networkobjects[network]
else:
ircobj.conf = new_conf
ircobj.serverdata = new_conf['servers'][network]
for network, sdata in new_conf['servers'].items():
# New server was added. Connect them if not already connected.
if network not in world.networkobjects:
proto = utils.getProtoModule(sdata['protocol'])
world.networkobjects[network] = classes.Irc(network, proto, new_conf)
irc.msg(irc.called_by, "Done.")

View File

@ -6,6 +6,9 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils
from log import log
# Easier access to world through eval/exec.
import world
def _exec(irc, source, args):
"""<code>
@ -14,7 +17,7 @@ def _exec(irc, source, args):
utils.checkAuthenticated(irc, source, allowOper=False)
args = ' '.join(args)
if not args.strip():
irc.msg(source, 'No code entered!')
irc.msg(irc.called_by, 'No code entered!')
return
log.info('(%s) Executing %r for %s', irc.name, args, utils.getHostmask(irc, source))
exec(args, globals(), locals())
@ -28,8 +31,8 @@ def _eval(irc, source, args):
utils.checkAuthenticated(irc, source, allowOper=False)
args = ' '.join(args)
if not args.strip():
irc.msg(source, 'No code entered!')
irc.msg(irc.called_by, 'No code entered!')
return
log.info('(%s) Evaluating %r for %s', irc.name, args, utils.getHostmask(irc, source))
irc.msg(source, eval(args))
irc.msg(irc.called_by, eval(args))
utils.add_cmd(_eval, 'eval')

33
plugins/fantasy.py Executable file
View File

@ -0,0 +1,33 @@
# fantasy.py: Adds FANTASY command support, to allow calling commands in channels
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils
from log import log
def handle_fantasy(irc, source, command, args):
"""Fantasy command handler."""
try:
prefix = irc.botdata["prefix"]
except KeyError:
log.warning("(%s) Fantasy prefix was not set in configuration - "
"fantasy commands will not work!", self.name)
return
channel = args['target']
text = args['text']
# Conditions:
# 1) Message target is a channel,
# 2) Message starts with our fantasy prefix,
# 3) The main PyLink client is in the channel.
# 4) The sender is NOT a PyLink client (prevents message loops).
if utils.isChannel(channel) and text.startswith(prefix) and \
irc.pseudoclient.uid in irc.channels[channel].users and not \
utils.isInternalClient(irc, source):
# Cut off the length of the prefix from the text.
text = text[len(prefix):]
# Set the last called in variable to the channel, so replies (from
# supporting plugins) get forwarded to it.
irc.called_by = channel
irc.callCommand(source, text)
utils.add_hook(handle_fantasy, 'PRIVMSG')

View File

@ -22,12 +22,12 @@ def disconnect(irc, source, args):
netname = args[0]
network = world.networkobjects[netname]
except IndexError: # No argument given.
irc.msg(source, 'Error: Not enough arguments (needs 1: network name (case sensitive)).')
irc.msg(irc.called_by, 'Error: Not enough arguments (needs 1: network name (case sensitive)).')
return
except KeyError: # Unknown network.
irc.msg(source, 'Error: No such network "%s" (case sensitive).' % netname)
irc.msg(irc.called_by, 'Error: No such network "%s" (case sensitive).' % netname)
return
irc.msg(source, "Done.")
irc.msg(irc.called_by, "Done.")
# Abort the connection! Simple as that.
network.aborted.set()
@ -41,18 +41,18 @@ def connect(irc, source, args):
netname = args[0]
network = world.networkobjects[netname]
except IndexError: # No argument given.
irc.msg(source, 'Error: Not enough arguments (needs 1: network name (case sensitive)).')
irc.msg(irc.called_by, 'Error: Not enough arguments (needs 1: network name (case sensitive)).')
return
except KeyError: # Unknown network.
irc.msg(source, 'Error: No such network "%s" (case sensitive).' % netname)
irc.msg(irc.called_by, 'Error: No such network "%s" (case sensitive).' % netname)
return
if network.connection_thread.is_alive():
irc.msg(source, 'Error: Network "%s" seems to be already connected.' % netname)
irc.msg(irc.called_by, 'Error: Network "%s" seems to be already connected.' % netname)
else: # Reconnect the network!
network.initVars()
network.connection_thread = threading.Thread(target=network.connect)
network.connection_thread.start()
irc.msg(source, "Done.")
irc.msg(irc.called_by, "Done.")
@utils.add_cmd
def autoconnect(irc, source, args):
@ -66,13 +66,13 @@ def autoconnect(irc, source, args):
seconds = float(args[1])
network = world.networkobjects[netname]
except IndexError: # Arguments not given.
irc.msg(source, 'Error: Not enough arguments (needs 2: network name (case sensitive), autoconnect time (in seconds)).')
irc.msg(irc.called_by, 'Error: Not enough arguments (needs 2: network name (case sensitive), autoconnect time (in seconds)).')
return
except KeyError: # Unknown network.
irc.msg(source, 'Error: No such network "%s" (case sensitive).' % netname)
irc.msg(irc.called_by, 'Error: No such network "%s" (case sensitive).' % netname)
return
except ValueError:
irc.msg(source, 'Error: Invalid argument "%s" for <seconds>.' % seconds)
irc.msg(irc.called_by, 'Error: Invalid argument "%s" for <seconds>.' % seconds)
return
network.serverdata['autoconnect'] = seconds
irc.msg(source, "Done.")
irc.msg(irc.called_by, "Done.")

View File

@ -26,6 +26,7 @@ spawnlocks = defaultdict(threading.RLock)
spawnlocks_servers = defaultdict(threading.RLock)
savecache = ExpiringDict(max_len=5, max_age_seconds=10)
killcache = ExpiringDict(max_len=5, max_age_seconds=10)
relay_started = True
### INTERNAL FUNCTIONS
@ -38,8 +39,10 @@ def initializeAll(irc):
network, channel = link
initializeChannel(irc, channel)
def main():
def main(irc=None):
"""Main function, called during plugin loading at start."""
global relay_started
relay_started = True
loadDB()
world.schedulers['relaydb'] = scheduler = sched.scheduler()
scheduler.enter(30, 1, exportDB, argument=(True,))
@ -48,6 +51,23 @@ def main():
thread = threading.Thread(target=scheduler.run)
thread.daemon = True
thread.start()
if irc is not None:
for ircobj in world.networkobjects.values():
initializeAll(ircobj)
def die(sourceirc):
"""Deinitialize PyLink Relay by quitting all relay clients."""
global relay_started
relay_started = False
for irc in world.networkobjects.values():
for user in irc.users.copy():
if isRelayClient(irc, user):
irc.proto.quitClient(user, "Relay plugin unloaded.")
for server, sobj in irc.servers.copy().items():
if hasattr(sobj, 'remote'):
irc.proto.squitServer(irc.sid, server, text="Relay plugin unloaded.")
relayservers.clear()
relayusers.clear()
def normalizeNick(irc, netname, nick, separator=None, uid=''):
"""Creates a normalized nickname for the given nick suitable for
@ -115,7 +135,7 @@ def exportDB(reschedule=False):
"""Exports the relay database, optionally creating a loop to do this
automatically."""
scheduler = world.schedulers.get('relaydb')
if reschedule and scheduler:
if reschedule and scheduler and relay_started:
scheduler.enter(30, 1, exportDB, argument=(True,))
log.debug("Relay: exporting links database to %s", dbname)
with open(dbname, 'wb') as f:
@ -144,6 +164,12 @@ def getPrefixModes(irc, remoteirc, channel, user, mlist=None):
def getRemoteSid(irc, remoteirc):
"""Gets the remote server SID representing remoteirc on irc, spawning
it if it doesn't exist."""
try:
spawnservers = irc.conf['relay']['spawn_servers']
except KeyError:
spawnservers = True
if not spawnservers:
return irc.sid
with spawnlocks_servers[irc.name]:
try:
sid = relayservers[irc.name][remoteirc.name]
@ -156,7 +182,9 @@ def getRemoteSid(irc, remoteirc):
except ValueError: # Network not initialized yet.
log.exception('(%s) Failed to spawn server for %r:',
irc.name, remoteirc.name)
return
irc.aborted.set()
else:
irc.servers[sid].remote = remoteirc.name
relayservers[irc.name][remoteirc.name] = sid
return sid
@ -223,16 +251,11 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
else:
realhost = None
ip = '0.0.0.0'
try:
u = remoteirc.proto.spawnClient(nick, ident=ident,
u = remoteirc.proto.spawnClient(nick, ident=ident,
host=host, realname=realname,
modes=modes, ts=userobj.ts,
opertype=opertype, server=rsid,
ip=ip, realhost=realhost).uid
except ValueError:
log.exception('(%s) Failed to spawn relay user %s on %s.', irc.name,
nick, remoteirc.name)
return
remoteirc.users[u].remote = (irc.name, user)
remoteirc.users[u].opertype = opertype
away = userobj.away
@ -660,11 +683,11 @@ def handle_part(irc, numeric, command, args):
del relayusers[(irc.name, numeric)][remoteirc.name]
utils.add_hook(handle_part, 'PART')
def handle_privmsg(irc, numeric, command, args):
notice = (command == 'NOTICE')
def handle_messages(irc, numeric, command, args):
notice = (command in ('NOTICE', 'PYLINK_SELF_NOTICE'))
target = args['target']
text = args['text']
if target == irc.pseudoclient.uid:
if utils.isInternalClient(irc, numeric) and utils.isInternalClient(irc, target):
return
relay = getRelay((irc.name, target))
remoteusers = relayusers[(irc.name, numeric)]
@ -685,11 +708,11 @@ def handle_privmsg(irc, numeric, command, args):
'messages over the relay.' % target, notice=True)
return
if utils.isChannel(target):
for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = world.networkobjects[netname]
for name, remoteirc in world.networkobjects.items():
real_target = getRemoteChan(irc, remoteirc, target)
if not real_target:
if irc.name == name or not remoteirc.connected.is_set() or not real_target:
continue
user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
real_target = prefix + real_target
if notice:
remoteirc.proto.noticeClient(user, real_target, text)
@ -715,8 +738,8 @@ def handle_privmsg(irc, numeric, command, args):
remoteirc.proto.noticeClient(user, real_target, text)
else:
remoteirc.proto.messageClient(user, real_target, text)
utils.add_hook(handle_privmsg, 'PRIVMSG')
utils.add_hook(handle_privmsg, 'NOTICE')
for cmd in ('PRIVMSG', 'NOTICE', 'PYLINK_SELF_NOTICE', 'PYLINK_SELF_PRIVMSG'):
utils.add_hook(handle_messages, cmd)
def handle_kick(irc, source, command, args):
channel = args['channel']
@ -977,7 +1000,11 @@ def handle_disconnect(irc, numeric, command, args):
if irc.name in v:
del relayusers[k][irc.name]
if k[0] == irc.name:
del relayusers[k]
try:
handle_quit(irc, k[1], 'PYLINK_DISCONNECT', {'text': 'Home network lost connection.'})
del relayusers[k]
except KeyError:
pass
for name, ircobj in world.networkobjects.items():
if name != irc.name:
rsid = getRemoteSid(ircobj, irc)
@ -1033,22 +1060,22 @@ def create(irc, source, args):
try:
channel = utils.toLower(irc, args[0])
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 1: channel.")
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 1: channel.")
return
if not utils.isChannel(channel):
irc.msg(source, 'Error: Invalid channel %r.' % channel)
irc.msg(irc.called_by, 'Error: Invalid channel %r.' % channel)
return
if source not in irc.channels[channel].users:
irc.msg(source, 'Error: You must be in %r to complete this operation.' % channel)
irc.msg(irc.called_by, 'Error: You must be in %r to complete this operation.' % channel)
return
utils.checkAuthenticated(irc, source)
localentry = getRelay((irc.name, channel))
if localentry:
irc.msg(source, 'Error: Channel %r is already part of a relay.' % channel)
irc.msg(irc.called_by, 'Error: Channel %r is already part of a relay.' % channel)
return
db[(irc.name, channel)] = {'claim': [irc.name], 'links': set(), 'blocked_nets': set()}
initializeChannel(irc, channel)
irc.msg(source, 'Done.')
irc.msg(irc.called_by, 'Done.')
@utils.add_cmd
def destroy(irc, source, args):
@ -1058,10 +1085,10 @@ def destroy(irc, source, args):
try:
channel = utils.toLower(irc, args[0])
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 1: channel.")
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 1: channel.")
return
if not utils.isChannel(channel):
irc.msg(source, 'Error: Invalid channel %r.' % channel)
irc.msg(irc.called_by, 'Error: Invalid channel %r.' % channel)
return
utils.checkAuthenticated(irc, source)
@ -1071,9 +1098,9 @@ def destroy(irc, source, args):
removeChannel(world.networkobjects.get(link[0]), link[1])
removeChannel(irc, channel)
del db[entry]
irc.msg(source, 'Done.')
irc.msg(irc.called_by, 'Done.')
else:
irc.msg(source, 'Error: No such relay %r exists.' % channel)
irc.msg(irc.called_by, 'Error: No such relay %r exists.' % channel)
return
@utils.add_cmd
@ -1086,7 +1113,7 @@ def link(irc, source, args):
channel = utils.toLower(irc, args[1])
remotenet = args[0].lower()
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 2-3: remote netname, channel, local channel name (optional).")
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 2-3: remote netname, channel, local channel name (optional).")
return
try:
localchan = utils.toLower(irc, args[2])
@ -1094,37 +1121,37 @@ def link(irc, source, args):
localchan = channel
for c in (channel, localchan):
if not utils.isChannel(c):
irc.msg(source, 'Error: Invalid channel %r.' % c)
irc.msg(irc.called_by, 'Error: Invalid channel %r.' % c)
return
if source not in irc.channels[localchan].users:
irc.msg(source, 'Error: You must be in %r to complete this operation.' % localchan)
irc.msg(irc.called_by, 'Error: You must be in %r to complete this operation.' % localchan)
return
utils.checkAuthenticated(irc, source)
if remotenet not in world.networkobjects:
irc.msg(source, 'Error: No network named %r exists.' % remotenet)
irc.msg(irc.called_by, 'Error: No network named %r exists.' % remotenet)
return
localentry = getRelay((irc.name, localchan))
if localentry:
irc.msg(source, 'Error: Channel %r is already part of a relay.' % localchan)
irc.msg(irc.called_by, 'Error: Channel %r is already part of a relay.' % localchan)
return
try:
entry = db[(remotenet, channel)]
except KeyError:
irc.msg(source, 'Error: No such relay %r exists.' % channel)
irc.msg(irc.called_by, 'Error: No such relay %r exists.' % channel)
return
else:
if irc.name in entry['blocked_nets']:
irc.msg(source, 'Error: Access denied (network is banned from linking to this channel).')
irc.msg(irc.called_by, 'Error: Access denied (network is banned from linking to this channel).')
return
for link in entry['links']:
if link[0] == irc.name:
irc.msg(source, "Error: Remote channel '%s%s' is already"
irc.msg(irc.called_by, "Error: Remote channel '%s%s' is already"
" linked here as %r." % (remotenet,
channel, link[1]))
return
entry['links'].add((irc.name, localchan))
initializeChannel(irc, localchan)
irc.msg(source, 'Done.')
irc.msg(irc.called_by, 'Done.')
@utils.add_cmd
def delink(irc, source, args):
@ -1135,7 +1162,7 @@ def delink(irc, source, args):
try:
channel = utils.toLower(irc, args[0])
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 1-2: channel, remote netname (optional).")
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 1-2: channel, remote netname (optional).")
return
try:
remotenet = args[1].lower()
@ -1143,13 +1170,13 @@ def delink(irc, source, args):
remotenet = None
utils.checkAuthenticated(irc, source)
if not utils.isChannel(channel):
irc.msg(source, 'Error: Invalid channel %r.' % channel)
irc.msg(irc.called_by, 'Error: Invalid channel %r.' % channel)
return
entry = getRelay((irc.name, channel))
if entry:
if entry[0] == irc.name: # We own this channel.
if not remotenet:
irc.msg(source, "Error: You must select a network to "
irc.msg(irc.called_by, "Error: You must select a network to "
"delink, or use the 'destroy' command to remove "
"this relay entirely (it was created on the current "
"network).")
@ -1162,9 +1189,9 @@ def delink(irc, source, args):
else:
removeChannel(irc, channel)
db[entry]['links'].remove((irc.name, channel))
irc.msg(source, 'Done.')
irc.msg(irc.called_by, 'Done.')
else:
irc.msg(source, 'Error: No such relay %r.' % channel)
irc.msg(irc.called_by, 'Error: No such relay %r.' % channel)
@utils.add_cmd
def linked(irc, source, args):
@ -1206,37 +1233,37 @@ def linkacl(irc, source, args):
cmd = args[0].lower()
channel = utils.toLower(irc, args[1])
except IndexError:
irc.msg(source, missingargs)
irc.msg(irc.called_by, missingargs)
return
if not utils.isChannel(channel):
irc.msg(source, 'Error: Invalid channel %r.' % channel)
irc.msg(irc.called_by, 'Error: Invalid channel %r.' % channel)
return
relay = getRelay((irc.name, channel))
if not relay:
irc.msg(source, 'Error: No such relay %r exists.' % channel)
irc.msg(irc.called_by, '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)')
irc.msg(source, s)
irc.msg(irc.called_by, s)
return
try:
remotenet = args[2]
except IndexError:
irc.msg(source, missingargs)
irc.msg(irc.called_by, missingargs)
return
if cmd == 'deny':
db[relay]['blocked_nets'].add(remotenet)
irc.msg(source, 'Done.')
irc.msg(irc.called_by, 'Done.')
elif cmd == 'allow':
try:
db[relay]['blocked_nets'].remove(remotenet)
except KeyError:
irc.msg(source, 'Error: Network %r is not on the blacklist for %r.' % (remotenet, channel))
irc.msg(irc.called_by, 'Error: Network %r is not on the blacklist for %r.' % (remotenet, channel))
else:
irc.msg(source, 'Done.')
irc.msg(irc.called_by, 'Done.')
else:
irc.msg(source, 'Error: Unknown subcommand %r: valid ones are ALLOW, DENY, and LIST.' % cmd)
irc.msg(irc.called_by, 'Error: Unknown subcommand %r: valid ones are ALLOW, DENY, and LIST.' % cmd)
@utils.add_cmd
def showuser(irc, source, args):
@ -1281,7 +1308,7 @@ def save(irc, source, args):
Saves the relay database to disk."""
utils.checkAuthenticated(irc, source)
exportDB()
irc.msg(source, 'Done.')
irc.msg(irc.called_by, 'Done.')
@utils.add_cmd
def claim(irc, source, args):
@ -1294,25 +1321,25 @@ def claim(irc, source, args):
try:
channel = utils.toLower(irc, args[0])
except IndexError:
irc.msg(source, "Error: Not enough arguments. Needs 1-2: channel, list of networks (optional).")
irc.msg(irc.called_by, "Error: Not enough arguments. Needs 1-2: channel, list of networks (optional).")
return
# We override getRelay() here to limit the search to the current network.
relay = (irc.name, channel)
if relay not in db:
irc.msg(source, 'Error: No such relay %r exists.' % channel)
irc.msg(irc.called_by, 'Error: No such relay %r exists.' % channel)
return
claimed = db[relay]["claim"]
try:
nets = args[1].strip()
except IndexError: # No networks given.
irc.msg(source, 'Channel \x02%s\x02 is claimed by: %s' %
(channel, ', '.join(claimed) or '\x1D(none)\x1D'))
irc.msg(irc.called_by, 'Channel \x02%s\x02 is claimed by: %s' %
(channel, ', '.join(claimed) or '\x1D(none)\x1D'))
else:
if nets == '-' or not nets:
claimed = set()
else:
claimed = set(nets.split(','))
db[relay]["claim"] = claimed
irc.msg(source, 'CLAIM for channel \x02%s\x02 set to: %s' %
(channel, ', '.join(claimed) or '\x1D(none)\x1D'))
irc.msg(irc.called_by, 'CLAIM for channel \x02%s\x02 set to: %s' %
(channel, ', '.join(claimed) or '\x1D(none)\x1D'))

View File

@ -15,7 +15,7 @@ from ts6_common import TS6BaseProtocol
class InspIRCdProtocol(TS6BaseProtocol):
def __init__(self, irc):
super(InspIRCdProtocol, self).__init__(irc)
# Set our case mapping (rfc1459 maps "\" and "|" together, for example".
# Set our case mapping (rfc1459 maps "\" and "|" together, for example).
self.casemapping = 'rfc1459'
# Raw commands sent from servers vary from protocol to protocol. Here, we map
@ -190,24 +190,28 @@ class InspIRCdProtocol(TS6BaseProtocol):
raise LookupError('No such PyLink PseudoServer exists.')
self._sendModes(numeric, target, modes, ts=ts)
def _sendKill(self, numeric, target, reason):
self._send(numeric, 'KILL %s :%s' % (target, reason))
# We only need to call removeClient here if the target is one of our
# clients, since any remote servers will send a QUIT from
# their target if the command succeeds.
if utils.isInternalClient(self.irc, target):
self.removeClient(target)
def killServer(self, numeric, target, reason):
"""Sends a kill from a PyLink server."""
if not utils.isInternalServer(self.irc, numeric):
raise LookupError('No such PyLink PseudoServer exists.')
self._send(numeric, 'KILL %s :%s' % (target, reason))
# We don't need to call removeClient here, since the remote server
# will send a QUIT from the target if the command succeeds.
self._sendKill(numeric, target, reason)
def killClient(self, numeric, target, reason):
"""Sends a kill from a PyLink client."""
if not utils.isInternalClient(self.irc, numeric):
raise LookupError('No such PyLink PseudoClient exists.')
self._send(numeric, 'KILL %s :%s' % (target, reason))
# We don't need to call removeClient here, since the remote server
# will send a QUIT from the target if the command succeeds.
self._sendKill(numeric, target, reason)
def topicServer(self, numeric, target, text):
"""Sends a topic change from a PyLink server. This is usally used on burst."""
"""Sends a topic change from a PyLink server. This is usually used on burst."""
if not utils.isInternalServer(self.irc, numeric):
raise LookupError('No such PyLink PseudoServer exists.')
ts = int(time.time())
@ -251,7 +255,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
if not (target is None or source is None):
self._send(source, 'PING %s %s' % (source, target))
def numericServer(self, source, numeric, text):
def numericServer(self, source, numeric, target, text):
raise NotImplementedError("Numeric sending is not yet implemented by this "
"protocol module. WHOIS requests are handled "
"locally by InspIRCd servers, so there is no "
@ -266,7 +270,12 @@ class InspIRCdProtocol(TS6BaseProtocol):
self._send(source, 'AWAY')
def spawnServer(self, name, sid=None, uplink=None, desc=None):
"""Spawns a server off a PyLink server."""
"""
Spawns a server off a PyLink server. desc (server description)
defaults to the one in the config. uplink defaults to the main PyLink
server, and sid (the server ID) is automatically generated if not
given.
"""
# -> :0AL SERVER test.server * 1 0AM :some silly pseudoserver
uplink = uplink or self.irc.sid
name = name.lower()

View File

@ -183,7 +183,7 @@ class TS6Protocol(TS6BaseProtocol):
assert target in self.irc.users, "Unknown target %r for killServer!" % target
self._send(numeric, 'KILL %s :Killed (%s)' % (target, reason))
removeClient(self.irc, target)
self.removeClient(target)
def killClient(self, numeric, target, reason):
"""Sends a kill from a PyLink client."""
@ -191,10 +191,10 @@ class TS6Protocol(TS6BaseProtocol):
raise LookupError('No such PyLink PseudoClient exists.')
assert target in self.irc.users, "Unknown target %r for killClient!" % target
self._send(numeric, 'KILL %s :Killed (%s)' % (target, reason))
removeClient(self.irc, target)
self.removeClient(target)
def topicServer(self, numeric, target, text):
"""Sends a topic change from a PyLink server. This is usally used on burst."""
"""Sends a topic change from a PyLink server. This is usually used on burst."""
if not utils.isInternalServer(self.irc, numeric):
raise LookupError('No such PyLink PseudoServer exists.')
# TB
@ -259,7 +259,12 @@ class TS6Protocol(TS6BaseProtocol):
self._send(source, 'AWAY')
def spawnServer(self, name, sid=None, uplink=None, desc=None):
"""Spawns a server off a PyLink server."""
"""
Spawns a server off a PyLink server. desc (server description)
defaults to the one in the config. uplink defaults to the main PyLink
server, and sid (the server ID) is automatically generated if not
given.
"""
# -> :0AL SID test.server 1 0XY :some silly pseudoserver
uplink = uplink or self.irc.sid
name = name.lower()

View File

@ -12,29 +12,6 @@ class TS6BaseProtocol(Protocol):
"""Sends a TS6-style raw command from a source numeric to the self.irc connection given."""
self.irc.send(':%s %s' % (source, msg))
def parseArgs(self, args):
"""Parses a string of RFC1459-style arguments split into a list, where ":" may
be used for multi-word arguments that last until the end of a line.
"""
real_args = []
for idx, arg in enumerate(args):
real_args.append(arg)
# If the argument starts with ':' and ISN'T the first argument.
# The first argument is used for denoting the source UID/SID.
if arg.startswith(':') and idx != 0:
# : is used for multi-word arguments that last until the end
# of the message. We can use list splicing here to turn them all
# into one argument.
# Set the last arg to a joined version of the remaining args
arg = args[idx:]
arg = ' '.join(arg)[1:]
# Cut the original argument list right before the multi-word arg,
# and then append the multi-word arg.
real_args = args[:idx]
real_args.append(arg)
break
return real_args
def parseTS6Args(self, args):
"""Similar to parseArgs(), but stripping leading colons from the first argument
of a line (usually the sender field)."""
@ -74,21 +51,6 @@ class TS6BaseProtocol(Protocol):
self._send(numeric, 'NICK %s %s' % (newnick, int(time.time())))
self.irc.users[numeric].nick = newnick
def removeClient(self, numeric):
"""Internal function to remove a client from our internal state."""
for c, v in self.irc.channels.copy().items():
v.removeuser(numeric)
# Clear empty non-permanent channels.
if not (self.irc.channels[c].users or ((self.irc.cmodes.get('permanent'), None) in self.irc.channels[c].modes)):
del self.irc.channels[c]
assert numeric not in v.users, "IrcChannel's removeuser() is broken!"
sid = numeric[:3]
log.debug('Removing client %s from self.irc.users', numeric)
del self.irc.users[numeric]
log.debug('Removing client %s from self.irc.servers[%s].users', numeric, sid)
self.irc.servers[sid].users.discard(numeric)
def partClient(self, client, channel, reason=None):
"""Sends a part from a PyLink client."""
channel = utils.toLower(self.irc, channel)

25
pylink
View File

@ -12,14 +12,11 @@ world.testing = False
import conf
from log import log
import classes
import utils
import coreplugin
if __name__ == '__main__':
log.info('PyLink %s starting...', world.version)
if conf.conf['login']['password'] == 'changeme':
log.critical("You have not set the login details correctly! Exiting...")
sys.exit(2)
protocols_folder = [os.path.join(os.getcwd(), 'protocols')]
# Write a PID file.
with open('%s.pid' % conf.confname, 'w') as f:
@ -27,16 +24,12 @@ if __name__ == '__main__':
# Import plugins first globally, because they can listen for events
# that happen before the connection phase.
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
# dynamically depending on which were configured.
for plugin in to_load:
try:
moduleinfo = imp.find_module(plugin, plugins_folder)
pl = imp.load_source(plugin, moduleinfo[1])
world.plugins.append(pl)
world.plugins[plugin] = pl = utils.loadModuleFromFolder(plugin, world.plugins_folder)
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)
@ -48,17 +41,7 @@ if __name__ == '__main__':
pl.main()
for network in conf.conf['servers']:
protoname = conf.conf['servers'][network]['protocol']
try:
moduleinfo = imp.find_module(protoname, protocols_folder)
proto = imp.load_source(protoname, moduleinfo[1])
except ImportError as e:
if str(e) == ('No module named %r' % protoname):
log.critical('Failed to load protocol module %r: The file could not be found.', protoname)
else:
log.critical('Failed to load protocol module: ImportError: %s', protoname, str(e))
sys.exit(2)
else:
world.networkobjects[network] = classes.Irc(network, proto)
proto = utils.getProtoModule(conf.conf['servers'][network]['protocol'])
world.networkobjects[network] = classes.Irc(network, proto, conf.conf)
world.started.set()
log.info("loaded plugins: %s", world.plugins)

View File

@ -10,7 +10,7 @@ cd "$WRAPPER_DIR"
if [[ ! -z "$(which cpulimit)" ]]; then
# -z makes cpulimit exit when PyLink dies.
cpulimit -l $LIMIT -z ./pylink
cpulimit -l $LIMIT -z ./pylink $*
echo "PyLink has been started (daemonized) under cpulimit, and will automatically be throttled if it goes over the CPU limit of ${LIMIT}%."
echo "To kill the process manually, run ./kill.sh"
else

View File

@ -26,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', world.bot_commands)
self.assertIn('test', world.bot_commands)
self.assertNotIn('TEST', world.bot_commands)
self.assertIn('dummyf', world.commands)
self.assertIn('test', world.commands)
self.assertNotIn('TEST', world.commands)
def test_add_hook(self):
utils.add_hook(dummyf, 'join')
self.assertIn('JOIN', world.command_hooks)
self.assertIn('JOIN', world.hooks)
# Command names stored in uppercase.
self.assertNotIn('join', world.command_hooks)
self.assertIn(dummyf, world.command_hooks['JOIN'])
self.assertNotIn('join', world.hooks)
self.assertIn(dummyf, world.hooks['JOIN'])
def testIsNick(self):
self.assertFalse(utils.isNick('abcdefgh', nicklen=3))

View File

@ -1,6 +1,7 @@
import string
import re
import inspect
import imp
from log import log
import world
@ -106,12 +107,12 @@ def add_cmd(func, name=None):
if name is None:
name = func.__name__
name = name.lower()
world.bot_commands[name].append(func)
world.commands[name].append(func)
def add_hook(func, command):
"""Add a hook <func> for command <command>."""
command = command.upper()
world.command_hooks[command].append(func)
world.hooks[command].append(func)
def toLower(irc, text):
"""Returns a lowercase representation of text based on the IRC object's
@ -158,7 +159,8 @@ def isServerName(s):
hostmaskRe = re.compile(r'^\S+!\S+@\S+$')
def isHostmask(text):
"""Returns whether the given text is a valid hostmask."""
return bool(hostmaskRe.match(text))
# Band-aid patch here to prevent bad bans set by Janus forwarding people into invalid channels.
return hostmaskRe.match(text) and '#' not in text
def parseModes(irc, target, args):
"""Parses a modestring list into a list of (mode, argument) tuples.
@ -176,10 +178,12 @@ def parseModes(irc, target, args):
args = args[1:]
if usermodes:
log.debug('(%s) Using irc.umodes for this query: %s', irc.name, irc.umodes)
assert target in irc.users, "Unknown user %r." % target
supported_modes = irc.umodes
oldmodes = irc.users[target].modes
else:
log.debug('(%s) Using irc.cmodes for this query: %s', irc.name, irc.cmodes)
assert target in irc.channels, "Unknown channel %r." % target
supported_modes = irc.cmodes
oldmodes = irc.channels[target].modes
res = []
@ -216,7 +220,8 @@ def parseModes(irc, target, args):
arg = nickToUid(irc, arg) or arg
if arg not in irc.users: # Target doesn't exist, skip it.
log.debug('(%s) Skipping setting mode "%s %s"; the '
'target doesn\'t seem to exist!')
'target doesn\'t seem to exist!', self.name,
mode, arg)
continue
elif prefix == '+' and mode in supported_modes['*C']:
# Only has parameter when setting.
@ -494,3 +499,13 @@ def getHostmask(irc, user):
except AttributeError:
host = '<unknown host>'
return '%s!%s@%s' % (nick, ident, host)
def loadModuleFromFolder(name, folder):
"""Attempts an import of name from a specific folder, returning the resulting module."""
moduleinfo = imp.find_module(name, folder)
m = imp.load_source(name, moduleinfo[1])
return m
def getProtoModule(protoname):
"""Imports and returns the protocol module requested."""
return loadModuleFromFolder(protoname, world.protocols_folder)

View File

@ -3,21 +3,25 @@
from collections import defaultdict
import threading
import subprocess
import os
# Global variable to indicate whether we're being ran directly, or imported
# for a testcase.
testing = True
global bot_commands, command_hooks
global commands, hooks
# This should be a mapping of command names to functions
bot_commands = defaultdict(list)
command_hooks = defaultdict(list)
commands = defaultdict(list)
hooks = defaultdict(list)
networkobjects = {}
schedulers = {}
plugins = []
plugins = {}
whois_handlers = []
started = threading.Event()
plugins_folder = [os.path.join(os.getcwd(), 'plugins')]
protocols_folder = [os.path.join(os.getcwd(), 'protocols')]
version = "<unknown>"
source = "https://github.com/GLolol/PyLink" # CHANGE THIS IF YOU'RE FORKING!!